diff --git a/packages/web/package.json b/packages/web/package.json index 5db6b87bf3..8807807930 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -102,6 +102,7 @@ "fuse.js": "^6.5.3", "highcharts": "^9.1.0", "highcharts-react-official": "^3.0.0", + "idb-keyval": "^6.2.1", "launchdarkly-react-client-sdk": "^3.0.6", "lightweight-charts": "^4.1.4", "lottie-react": "^2.4.0", diff --git a/packages/web/utils/trpc.ts b/packages/web/utils/trpc.ts index 77b1146a0e..ec0bf0682a 100644 --- a/packages/web/utils/trpc.ts +++ b/packages/web/utils/trpc.ts @@ -1,13 +1,17 @@ import { superjson } from "@osmosis-labs/server"; -import { makeIndexedKVStore } from "@osmosis-labs/stores"; import { createTRPCRouter, localLink, makeSkipBatchLink, } from "@osmosis-labs/trpc"; -import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; +import { noop } from "@osmosis-labs/utils"; import { QueryClient } from "@tanstack/react-query"; -import { persistQueryClient } from "@tanstack/react-query-persist-client"; +import { + PersistedClient, + Persister, + persistQueryClient, + Promisable, +} from "@tanstack/react-query-persist-client"; import { loggerLink } from "@trpc/client"; import { createTRPCNext } from "@trpc/next"; import type { @@ -16,6 +20,7 @@ import type { inferRouterInputs, inferRouterOutputs, } from "@trpc/server"; +import { get, set, values } from "idb-keyval"; import { AssetLists } from "~/config/generated/asset-lists"; import { ChainList } from "~/config/generated/chain-list"; @@ -42,25 +47,186 @@ const trpcLocalRouter = createTRPCRouter({ local: localRouter, }); +interface AsyncThrottleOptions { + interval?: number; + onError?: (error: unknown) => void; +} + +function asyncThrottle( + func: (...args: Args) => Promise, + { interval = 1000, onError = noop }: AsyncThrottleOptions = {} +) { + if (typeof func !== "function") throw new Error("argument is not function."); + + let running = false; + let lastTime = 0; + let timeout: ReturnType; + let currentArgs: Args | null = null; + + const execFunc = async () => { + if (currentArgs) { + const args = currentArgs; + currentArgs = null; + try { + running = true; + await func(...args); + } catch (error) { + onError(error); + } finally { + lastTime = Date.now(); // this line must after 'func' executed to avoid two 'func' running in concurrent. + running = false; + } + } + }; + + const delayFunc = async () => { + clearTimeout(timeout); + timeout = setTimeout(() => { + if (running) { + delayFunc(); // Will come here when 'func' execution time is greater than the interval. + } else { + execFunc(); + } + }, interval); + }; + + return (...args: Args) => { + currentArgs = args; + + const tooSoon = Date.now() - lastTime < interval; + if (running || tooSoon) { + delayFunc(); + } else { + execFunc(); + } + }; +} + +interface AsyncStorage { + getItem: (key: string) => Promise; + setItem: (key: string, value: string) => Promise; + removeItem: (key: string) => Promise; + values: () => Promise; +} + +type AsyncPersistRetryer = (props: { + persistedClient: PersistedClient; + error: Error; + errorCount: number; +}) => Promisable; + +interface CreateAsyncStoragePersisterOptions { + /** The storage client used for setting and retrieving items from cache. + * For SSR pass in `undefined`. Note that window.localStorage can be + * `null` in Android WebViews depending on how they are configured. + */ + storage: AsyncStorage | undefined | null; + /** The key to use when storing the cache */ + key?: string; + /** To avoid spamming, + * pass a time in ms to throttle saving the cache to disk */ + throttleTime?: number; + retry?: AsyncPersistRetryer; +} + +const createAsyncStoragePersister = ({ + storage, + key = `REACT_QUERY_OFFLINE_CACHE`, + throttleTime = 1000, + retry, +}: CreateAsyncStoragePersisterOptions): Persister => { + if (storage) { + const trySave = async ( + persistedClient: PersistedClient + ): Promise => { + try { + for (const query of persistedClient.clientState.queries) { + storage.setItem(query.queryKey.join("-"), superjson.stringify(query)); + } + + const clientState = { + buster: persistedClient.buster, + timestamp: persistedClient.timestamp, + }; + + await storage.setItem(key, JSON.stringify(clientState)); + return; + } catch (error) { + return error as Error; + } + }; + + return { + persistClient: asyncThrottle( + async (persistedClient) => { + let client: PersistedClient | undefined = persistedClient; + let error = await trySave(client); + let errorCount = 0; + while (error && client) { + errorCount++; + client = await retry?.({ + persistedClient: client, + error, + errorCount, + }); + + if (client) { + error = await trySave(client); + } + } + }, + { interval: throttleTime } + ), + restoreClient: async () => { + const cacheClientStateString = await storage.getItem(key); + + if (!cacheClientStateString) { + return; + } + + const queriesString = await storage.values(); + const queries = queriesString.map( + superjson.parse + ); + const client: PersistedClient = JSON.parse(cacheClientStateString); + + return { + ...client, + clientState: { + queries, + mutations: [], + }, + } as PersistedClient; + }, + removeClient: () => storage.removeItem(key), + }; + } + + return { + persistClient: noop, + restoreClient: () => Promise.resolve(undefined), + removeClient: noop, + }; +}; + /** A set of type-safe react-query hooks for your tRPC API. */ export const api = createTRPCNext({ config() { - const storage = makeIndexedKVStore("tanstack-query-cache"); + /* const storage = makeIndexedKVStore("tanstack-query-cache"); */ const localStoragePersister = createAsyncStoragePersister({ storage: typeof window !== "undefined" ? { getItem: async (key) => { - const item: string | null | undefined = await storage.get(key); + const item: string | null | undefined = await get(key); return item ?? null; }, - setItem: (key, value) => storage.set(key, value), - removeItem: (key) => storage.set(key, undefined), + setItem: (key, value) => set(key, value), + removeItem: (key) => set(key, undefined), + values: () => values(), } : undefined, - serialize: (client) => superjson.stringify(client), - deserialize: (cachedString) => superjson.parse(cachedString), }); const queryClient = new QueryClient({ diff --git a/yarn.lock b/yarn.lock index 68ff9b29e9..b1b0d2647c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4458,7 +4458,29 @@ buffer "^6.0.3" delay "^4.4.0" -"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/cosmos@0.12.12", "@keplr-wallet/cosmos@0.12.28": +"@keplr-wallet/common@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.12.tgz#55030d985b729eac582c0d7203190e25ea2cb3ec" + integrity sha512-AxpwmXdqs083lMvA8j0/V30oTGyobsefNaCou+lP4rCyDdYuXSEux+x2+1AGL9xB3yZfN+4jvEEKJdMwHYEHcQ== + dependencies: + "@keplr-wallet/crypto" "0.12.12" + "@keplr-wallet/types" "0.12.12" + buffer "^6.0.3" + delay "^4.4.0" + mobx "^6.1.7" + +"@keplr-wallet/common@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/common/-/common-0.12.28.tgz#1d5d985070aced31a34a6426c9ac4b775081acca" + integrity sha512-ESQorPZw8PRiUXhsrxED+E1FEWkAdc6Kwi3Az7ce204gMBQDI2j0XJtTd4uCUp+C24Em9fk0samdHzdoB4caIg== + dependencies: + "@keplr-wallet/crypto" "0.12.28" + "@keplr-wallet/types" "0.12.28" + buffer "^6.0.3" + delay "^4.4.0" + mobx "^6.1.7" + +"@keplr-wallet/cosmos@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/cosmos/-/cosmos-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-/A/wHyYo5gQIW5YkAQYZadEv/12EcAuDclO0KboIb9ti4XFJW6S4VY8LnA16R7DZyBx1cnQknyDm101fUrJfJQ== @@ -4475,6 +4497,40 @@ long "^4.0.0" protobufjs "^6.11.2" +"@keplr-wallet/cosmos@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.12.tgz#72c0505d2327bbf2f5cb51502acaf399b88b4ae3" + integrity sha512-9TLsefUIAuDqqf1WHBt9Bk29rPlkezmLM8P1eEsXGUaHBfuqUrO+RwL3eLA3HGcgNvdy9s8e0p/4CMInH/LLLQ== + dependencies: + "@ethersproject/address" "^5.6.0" + "@keplr-wallet/common" "0.12.12" + "@keplr-wallet/crypto" "0.12.12" + "@keplr-wallet/proto-types" "0.12.12" + "@keplr-wallet/simple-fetch" "0.12.12" + "@keplr-wallet/types" "0.12.12" + "@keplr-wallet/unit" "0.12.12" + bech32 "^1.1.4" + buffer "^6.0.3" + long "^4.0.0" + protobufjs "^6.11.2" + +"@keplr-wallet/cosmos@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/cosmos/-/cosmos-0.12.28.tgz#d56e73468256e7276a66bb41f145449dbf11efa1" + integrity sha512-IuqmSBgKgIeWBA0XGQKKs28IXFeFMCrfadCbtiZccNc7qnNr5Y/Cyyk01BPC8Dd1ZyEyAByoICgrxvtGN0GGvA== + dependencies: + "@ethersproject/address" "^5.6.0" + "@keplr-wallet/common" "0.12.28" + "@keplr-wallet/crypto" "0.12.28" + "@keplr-wallet/proto-types" "0.12.28" + "@keplr-wallet/simple-fetch" "0.12.28" + "@keplr-wallet/types" "0.12.28" + "@keplr-wallet/unit" "0.12.28" + bech32 "^1.1.4" + buffer "^6.0.3" + long "^4.0.0" + protobufjs "^6.11.2" + "@keplr-wallet/crypto@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/crypto/-/crypto-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -4547,7 +4603,7 @@ resolved "https://registry.npmjs.org/@keplr-wallet/popup/-/popup-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-Q/teyV6vdmpH3SySGd1xrNc/mVGK/tCP5vFEG2I3Y4FDCSV1yD7vcVgUy+tN19Z8EM3goR57V2QlarSOidtdjQ== -"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/proto-types@0.12.12": +"@keplr-wallet/proto-types@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/proto-types/-/proto-types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-fLUJEtDadYJIMBzhMSZpEDTvXqk8wW68TwnUCRAcAooEQEtXPwY5gfo3hcekQEiCYtIu8XqzJ9fg01rp2Z4d3w== @@ -4555,6 +4611,22 @@ long "^4.0.0" protobufjs "^6.11.2" +"@keplr-wallet/proto-types@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.12.tgz#24e0530af7604a90f33a397a82fe500865c76154" + integrity sha512-iAqqNlJpxu/8j+SwOXEH2ymM4W0anfxn+eNeWuqz2c/0JxGTWeLURioxQmCtewtllfHdDHHcoQ7/S+NmXiaEgQ== + dependencies: + long "^4.0.0" + protobufjs "^6.11.2" + +"@keplr-wallet/proto-types@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/proto-types/-/proto-types-0.12.28.tgz#2fb2c37749ce7db974f01d07387e966c9b99027d" + integrity sha512-ukti/eCTltPUP64jxtk5TjtwJogyfKPqlBIT3KGUCGzBLIPeYMsffL5w5aoHsMjINzOITjYqzXyEF8LTIK/fmw== + dependencies: + long "^4.0.0" + protobufjs "^6.11.2" + "@keplr-wallet/provider-extension@^0.12.95": version "0.12.107" resolved "https://registry.yarnpkg.com/@keplr-wallet/provider-extension/-/provider-extension-0.12.107.tgz#98a0fb42cb0c54d4e681e60e6b1145429a6e3e23" @@ -4630,12 +4702,32 @@ deepmerge "^4.2.2" long "^4.0.0" -"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/router@0.12.12", "@keplr-wallet/router@0.12.96": +"@keplr-wallet/router@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/router/-/router-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-bt9weexlbhlh8KsOvbDrvHJ8jtUXrXgB2LX+hEAwjclHQt7PMUhx9a5z0Obd19/ive5G/1M7/ccdPIWxRBpKQw== -"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix", "@keplr-wallet/types@0.12.107", "@keplr-wallet/types@0.12.12", "@keplr-wallet/types@0.12.96", "@keplr-wallet/types@^0.12.95": +"@keplr-wallet/router@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.12.tgz#92a2c006aec6945ed313575af6b0801f8e84e315" + integrity sha512-Aa1TiVRIEPaqs1t27nCNs5Kz6Ty4CLarVdfqcRWlFQL6zFq33GT46s6K9U4Lz2swVCwdmerSXaq308K/GJHTlw== + +"@keplr-wallet/router@0.12.96": + version "0.12.96" + resolved "https://registry.yarnpkg.com/@keplr-wallet/router/-/router-0.12.96.tgz#6a20ed2c90ba3ed4f3fc43ed7513f72d7055482d" + integrity sha512-O8izj032ZKQIoTus96BFqem+w6NpYHU3j6NEnSaQBh6Zncj9fgjoOVs0CKK+jsuLYUsOHx2t86BxMSKESsR0Ug== + +"@keplr-wallet/simple-fetch@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.12.tgz#aacc5c3f22b7ab2804b39e864725294a32f858fd" + integrity sha512-lCOsaI8upMpbusfwJqEK8VIEX77+QE8+8MJVRqoCYwjOTqKGdUH7D1ieZWh+pzvzOnVgedM3lxqdmCvdgU91qw== + +"@keplr-wallet/simple-fetch@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.28.tgz#44225df5b329c823076280df1ec9930a21b1373e" + integrity sha512-T2CiKS2B5n0ZA7CWw0CA6qIAH0XYI1siE50MP+i+V0ZniCGBeL+BMcDw64vFJUcEH+1L5X4sDAzV37fQxGwllA== + +"@keplr-wallet/types@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.10.24-ibc.go.v7.hot.fix.tgz" integrity sha512-3KUjDMUCscYkvKnC+JsJh9+X0NHlsvBgAghP/uy2p5OGtiULqPBAjWiO+hnBbhis3ZEkzGcCROnnBOoccKd3CQ== @@ -4646,6 +4738,41 @@ long "^4.0.0" secretjs "^0.17.0" +"@keplr-wallet/types@0.12.107": + version "0.12.107" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.107.tgz#8d6726d86e17a79131b4b6f4f114052d6384aa58" + integrity sha512-jBpjJO+nNL8cgsJLjZYoq84n+7nXHDdztTgRMVnnomFb+Vy0FVIEI8VUl89ImmHDUImDd0562ywsvA496/0yCA== + dependencies: + long "^4.0.0" + +"@keplr-wallet/types@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.12.tgz#f4bd9e710d5e53504f6b53330abb45bedd9c20ae" + integrity sha512-fo6b8j9EXnJukGvZorifJWEm1BPIrvaTLuu5PqaU5k1ANDasm/FL1NaUuaTBVvhRjINtvVXqYpW/rVUinA9MBA== + dependencies: + long "^4.0.0" + +"@keplr-wallet/types@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.28.tgz#eac3c2c9d4560856c5c403a87e67925992a04fbf" + integrity sha512-EcM9d46hYDm3AO4lf4GUbTSLRySONtTmhKb7p88q56OQOgJN3MMjRacEo2p9jX9gpPe7gRIjMUalhAfUiFpZoQ== + dependencies: + long "^4.0.0" + +"@keplr-wallet/types@0.12.96": + version "0.12.96" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.96.tgz#a7735051b1f7cbcdf9b8c29010b1c3c45d195c19" + integrity sha512-tr4tPjMrJCsfRXXhhmqnpb9DqH9auJp3uuj8SvDB3pQTTaYJNxkdonLv1tYmXZZ6J9oWtk9WVEDTVgBQN/wisw== + dependencies: + long "^4.0.0" + +"@keplr-wallet/types@^0.12.95": + version "0.12.157" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.157.tgz#5aa15c09c5e18ab3a1381839368a6725c4037818" + integrity sha512-CWRg6AK2x7sXZ0rhr9NSL8z9sIdnNHgrce3nyOBAJkXJ9XtrrJoOtopnkFaQmFOanysMHUrKMCbZFiaYrBmqew== + dependencies: + long "^4.0.0" + "@keplr-wallet/unit@0.10.24-ibc.go.v7.hot.fix": version "0.10.24-ibc.go.v7.hot.fix" resolved "https://registry.npmjs.org/@keplr-wallet/unit/-/unit-0.10.24-ibc.go.v7.hot.fix.tgz" @@ -4655,6 +4782,24 @@ big-integer "^1.6.48" utility-types "^3.10.0" +"@keplr-wallet/unit@0.12.12": + version "0.12.12" + resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.12.tgz#2d7f2e38df4e09c8123dcc0784ffc4b5f4166217" + integrity sha512-fayJcfXWKUnbDZiRJHyuA9GMVS9DymjRlCzlpAJ0+xV0c4Kun/f+9FajL9OQAdPPhnJ7A3KevMI4VHZsd9Yw+A== + dependencies: + "@keplr-wallet/types" "0.12.12" + big-integer "^1.6.48" + utility-types "^3.10.0" + +"@keplr-wallet/unit@0.12.28": + version "0.12.28" + resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.28.tgz#907c7fa0b49a729cda207fca14fc0a38871cc6c4" + integrity sha512-kpXigHDBJGOmhtPkv9hqsQid9zkFo7OQPeKgO2n8GUlOINIXW6kWG5LXYTi/Yg9Uiw1CQF69gFMuZCJ8IzVHlA== + dependencies: + "@keplr-wallet/types" "0.12.28" + big-integer "^1.6.48" + utility-types "^3.10.0" + "@keplr-wallet/wc-client@^0.12.95": version "0.12.96" resolved "https://registry.yarnpkg.com/@keplr-wallet/wc-client/-/wc-client-0.12.96.tgz#a56995172dcdc73d32b24d5a704a954062befc2a"