From 1e2bdd5047c1e75a5719b46840a03d1fd5d24998 Mon Sep 17 00:00:00 2001 From: azu Date: Sun, 25 Feb 2024 15:30:20 +0900 Subject: [PATCH] refactor(web): Suspense + Streaming --- web/app/client/CompositionInput.tsx | 8 +- web/app/client/SearchMore.tsx | 11 +- web/app/page.tsx | 30 +++-- web/app/server/SearchResult.tsx | 177 +++++++++++++--------------- web/app/server/search.ts | 3 +- web/package.json | 11 +- web/yarn.lock | 86 +++----------- 7 files changed, 132 insertions(+), 194 deletions(-) diff --git a/web/app/client/CompositionInput.tsx b/web/app/client/CompositionInput.tsx index bc8857a..9f223b4 100644 --- a/web/app/client/CompositionInput.tsx +++ b/web/app/client/CompositionInput.tsx @@ -1,11 +1,11 @@ "use client"; -import { CSSProperties, useCallback, useState } from "react"; +import React, { CSSProperties, useCallback, useState } from "react"; export function CompositionInput(props: { style?: CSSProperties; value: string; onInput: (value: string) => void }) { const [inputValue, setInputValue] = useState(props.value); const [isComposing, setIsComposing] = useState(false); const onInput = useCallback( - (event) => { + (event: React.FormEvent) => { const value = event.currentTarget.value; setInputValue(value); if (!isComposing) { @@ -14,10 +14,10 @@ export function CompositionInput(props: { style?: CSSProperties; value: string; }, [isComposing] ); - const onCompositionStart = useCallback((e) => { + const onCompositionStart = useCallback(() => { setIsComposing(true); }, []); - const onCompositionEnd = useCallback((event) => { + const onCompositionEnd = useCallback((event: React.FormEvent) => { setIsComposing(false); const value = event.currentTarget.value; setInputValue(value); diff --git a/web/app/client/SearchMore.tsx b/web/app/client/SearchMore.tsx index f346e06..a2b513e 100644 --- a/web/app/client/SearchMore.tsx +++ b/web/app/client/SearchMore.tsx @@ -1,6 +1,6 @@ "use client"; -import { LineTweetResponse } from "../server/search"; -import { useMemo, useTransition } from "react"; +import { FetchS3SelectResult, LineTweetResponse } from "../server/search"; +import { use, useMemo, useTransition } from "react"; import { useTypeUrlSearchParams } from "../lib/useTypeUrlSearchParams"; import { HomPageSearchParam } from "../page"; @@ -27,8 +27,11 @@ export const useSearchMore = (props: { searchResults: LineTweetResponse[] }) => isLoadingMore } as const; }; -export const SearchMore = (props: { searchResults: LineTweetResponse[] }) => { - const { handlers, isLoadingMore } = useSearchMore(props); +export const SearchMore = (props: { retPromise: Promise }) => { + const { results } = use(props.retPromise); + const { handlers, isLoadingMore } = useSearchMore({ + searchResults: results + }); return (
}) => { + const ret = use(props.retPromise); + return Hit: {ret.results.length}; +}; + async function HomePage({ searchParams }: { // https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional searchParams: HomPageSearchParam; }) { - const ret = await fetchS3Select({ + const retPromise = fetchS3Select({ max: searchParams.max ? Number(searchParams.max) : 20, query: searchParams.q ?? "", afterTimestamp: searchParams.timestamp ? Number(searchParams.timestamp) : undefined @@ -60,18 +65,23 @@ async function HomePage({
-
{/*Hit: {sortedSearchResults.length}*/}
+
+ Hit: …}> + + +
- + Loading…}> + + - + + + ); } + export default HomePage; diff --git a/web/app/server/SearchResult.tsx b/web/app/server/SearchResult.tsx index 88622ee..1391824 100644 --- a/web/app/server/SearchResult.tsx +++ b/web/app/server/SearchResult.tsx @@ -1,9 +1,9 @@ -import { LineTweetResponse } from "./search"; +import { FetchS3SelectResult, LineTweetResponse } from "./search"; import { FaSpinner } from "react-icons/fa"; import dayjs from "dayjs"; import { MdPerson, MdUpdate } from "react-icons/md"; import utc from "dayjs/plugin/utc"; -import { ReactElement } from "react"; +import { ReactElement, use } from "react"; dayjs.extend(utc); const StatusLink = (props: { itemId: string; children: ReactElement }) => { @@ -26,106 +26,93 @@ const StatusLink = (props: { itemId: string; children: ReactElement }) => { ); }; -export function SearchResultContent(props: { - isFetching: boolean; - searchResults: LineTweetResponse[]; - screenName: string; -}) { +export function SearchResultContent(props: { retPromise: Promise; screenName: string }) { + const { results: searchResults } = use(props.retPromise); return (
- {props.isFetching ? ( - - ) : ( -
    - {props.searchResults.map((item) => { - const day = dayjs.utc(item.timestamp); - const isTwitter = !item.id.startsWith("at://"); - return ( -
  • + {searchResults.map((item) => { + const day = dayjs.utc(item.timestamp); + const isTwitter = !item.id.startsWith("at://"); + return ( +
  • + - - - - - {isTwitter && ( - <> - - - + + + + {isTwitter && ( + <> + + + - - - - - )} - -

    -

  • - ); - })} -
- )} + + + + + )} + +

+ + ); + })} +

); } diff --git a/web/app/server/search.ts b/web/app/server/search.ts index 04a225b..7e47d57 100644 --- a/web/app/server/search.ts +++ b/web/app/server/search.ts @@ -59,6 +59,7 @@ const escapeLike = (s: string) => { ); }; +export type FetchS3SelectResult = { results: LineTweetResponse[]; stats: StatsEvent["Details"] }; export const fetchS3Select = async ({ query, max, @@ -67,7 +68,7 @@ export const fetchS3Select = async ({ query: string; max: number; afterTimestamp?: number; -}): Promise<{ results: LineTweetResponse[]; stats: StatsEvent["Details"] }> => { +}): Promise => { const queries = query .split(/\s+/) .map((query) => query.trim()) diff --git a/web/package.json b/web/package.json index 2ea1a22..a2a2162 100644 --- a/web/package.json +++ b/web/package.json @@ -14,16 +14,11 @@ "next": "^14.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^5.0.1", - "regex-combiner": "^1.0.1", - "split2": "^3.2.2", - "sqlstring": "^2.3.3", - "use-debounce": "^6.0.1" + "react-icons": "^5.0.1" }, "devDependencies": { - "@types/react": "^17.0.11", - "@types/split2": "^3.2.0", - "@types/sqlstring": "^2.3.0", + "@types/react": "^18.2.58", + "@types/node": "^20.11.20", "dotenv": "^10.0.0", "typescript": "^5.3.3" } diff --git a/web/yarn.lock b/web/yarn.lock index 7bd7818..0530ebd 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1131,20 +1131,22 @@ dependencies: tslib "^2.4.0" -"@types/node@*": - version "14.14.41" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.41.tgz#d0b939d94c1d7bd53d04824af45f1139b8c45615" - integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g== +"@types/node@^20.11.20": + version "20.11.20" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.20.tgz#f0a2aee575215149a62784210ad88b3a34843659" + integrity sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg== + dependencies: + undici-types "~5.26.4" "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== -"@types/react@^17.0.11": - version "17.0.11" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" - integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA== +"@types/react@^18.2.58": + version "18.2.58" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.58.tgz#22082d12898d11806f4a1aefb5583116a047493d" + integrity sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -1155,18 +1157,6 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== -"@types/split2@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/split2/-/split2-3.2.0.tgz#a14f5acb1719aca5e6bcd3706f0df5c5b986802b" - integrity sha512-Wb9kp2BW5Qs38oyAS36t+wDN9eE6bFt8fpJ0DpYX6R5Og5tY003m8v1H2cEv8bpF9vLpPXago5pgsCgPka8BVQ== - dependencies: - "@types/node" "*" - -"@types/sqlstring@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@types/sqlstring/-/sqlstring-2.3.0.tgz#5960ade0166cfaaa4673fc74ec8157a08d06c89e" - integrity sha512-kMFecDYYFk/f5fljO0UFrSPwU1JxY4mIjX6ic7MHv5nD6sEd3NYLoWcOV/3s6Drs7RHdCwTQdD5NdgVl0I2zzg== - bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -1216,11 +1206,6 @@ graceful-fs@^4.2.11: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1295,25 +1280,6 @@ react@^18.2.0: dependencies: loose-envify "^1.1.0" -readable-stream@^3.0.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -regex-combiner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/regex-combiner/-/regex-combiner-1.0.1.tgz#2f55d0ac4a1ac471153207cadde8e1e2b89fab18" - integrity sha1-L1XQrEoaxHEVMgfK3ejh4rifqxg= - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -1326,30 +1292,11 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -split2@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" - integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== - dependencies: - readable-stream "^3.0.0" - -sqlstring@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" - integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== - streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - strnum@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" @@ -1377,15 +1324,10 @@ typescript@^5.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== -use-debounce@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-6.0.1.tgz#ed1eb2b30189408fb9792ea2887f4c6c3cb401a3" - integrity sha512-kpvIxpa0vOLz/2I2sfNJ72mUeaT2CMNCu5BT1f2HkV9qZK27UVSOFf1sSSu+wjJE4TcR2VTXS2SM569+m3TN7Q== - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== uuid@^8.3.2: version "8.3.2"