tempest-react-sdk 0.84.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-tempest-app.mjs +10 -1
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +258 -187
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js +29 -28
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +6 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +195 -160
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/payments/PixCheckout.cjs +1 -1
- package/dist/payments/PixCheckout.cjs.map +1 -1
- package/dist/payments/PixCheckout.js +1 -1
- package/dist/payments/PixCheckout.js.map +1 -1
- package/dist/payments/mercado-pago.cjs.map +1 -1
- package/dist/payments/mercado-pago.js.map +1 -1
- package/dist/payments/outcome.cjs +1 -1
- package/dist/payments/outcome.cjs.map +1 -1
- package/dist/payments/outcome.js +8 -5
- package/dist/payments/outcome.js.map +1 -1
- package/dist/payments/use-mercado-pago-card.cjs +1 -1
- package/dist/payments/use-mercado-pago-card.cjs.map +1 -1
- package/dist/payments/use-mercado-pago-card.js +69 -61
- package/dist/payments/use-mercado-pago-card.js.map +1 -1
- package/dist/payments/use-pix-charge.cjs +1 -1
- package/dist/payments/use-pix-charge.cjs.map +1 -1
- package/dist/payments/use-pix-charge.js +36 -28
- package/dist/payments/use-pix-charge.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +100 -54
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/tempest-react-sdk.d.ts +245 -15
- package/package.json +7 -1
- package/template/package.json +6 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.cjs","names":["ownerFingerprint","createOfflineStore","isDevBuild","dehydrate","require_dev_mode","require_owner_fingerprint","require_create_offline_store","_tanstack_react_query","pendingClears","ANONYMOUS_SNAPSHOT_OWNER","currentOwner","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","slot","ended","warnedEnded","warnedRebound","boundOwner","timer","inFlight","afterPendingClear","pending","warnEnded","warnRebound","save","snapshot","queryClient","foreign","owner","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint, and on cache changes until the handle is bound.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * The handle is bound to the first signed-in owner it sees on a cache\n * change, a write or a restore — the cache change is what pins the owner of\n * data that a throttled write would otherwise stamp with whoever is signed\n * in a second later (#580). Once bound, a write under another owner —\n * nobody, because the token expired without `clear()`, or a second account\n * on the same tab — deletes the snapshot instead of stamping the bound\n * owner's cached responses with the new owner (#563); the same owner coming\n * back writes again. Writes\n * before anyone signs in stay anonymous and are restored while nobody is,\n * so an app that persists logged-out data keeps doing so. An anonymous\n * snapshot written by 0.82.0 is not restored (it may hold an expired\n * session's cache): one cold start, once.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\n/**\n * Owner stamp of a snapshot written while nobody was signed in.\n *\n * Not a fingerprint (`ownerFingerprint` only yields `p:` + 16 hex characters),\n * so it never matches a signed-in owner. It replaces the `null` stamp of 0.82.0\n * on purpose: a `null` row from 0.82.0 may hold the cache of a session that\n * expired before the write (#563), and nothing in it tells that row apart from\n * genuinely anonymous data, so `restore()` skips it — one cold start, once.\n */\nconst ANONYMOUS_SNAPSHOT_OWNER = \"p:anonymous\";\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /**\n * Owner fingerprint, or {@link ANONYMOUS_SNAPSHOT_OWNER}, when `getPrincipal`\n * was set at write time; absent otherwise.\n */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let warnedRebound = false;\n let boundOwner: string | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n function warnRebound(): void {\n if (warnedRebound || !isDevBuild()) return;\n warnedRebound = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: another account signed in on a \" +\n \"handle bound to the previous one; its snapshot is deleted instead of written. \" +\n \"Create a new handle after login to keep persisting.\",\n );\n }\n\n /**\n * Write the snapshot, or delete the row when the session it holds is over.\n *\n * With `getPrincipal`, the handle is bound to the first signed-in owner it\n * sees on a cache change, a write or a restore. A write under any other owner — nobody, after a token\n * expired, or a second account signing in on the same tab — deletes the row\n * instead of stamping it: the in-memory cache still holds the bound owner's\n * responses, and stamping them with the new owner is what handed them to the\n * next account (#563). The same owner coming back writes again.\n *\n * A second account (not an expiry) means the app kept the handle across a\n * login, which stops persistence for that account in silence; a dev build\n * logs one `console.warn` saying so, as for a handle ended by `clear()`.\n */\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n let foreign = false;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n foreign = boundOwner !== null && owner !== boundOwner;\n if (foreign && owner !== null) warnRebound();\n boundOwner ??= owner;\n snapshot.owner = owner ?? ANONYMOUS_SNAPSHOT_OWNER;\n }\n const write = foreign ? store.delete(key) : store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n /**\n * Schedule a write for cache activity, or detach once the handle has ended.\n *\n * With `getPrincipal`, an unbound handle is bound here, to whoever is\n * signed in when the cache changes — not only by a write or a restore. The\n * write is throttled, so the principal it reads can already be someone\n * else's: a session that expired inside the first window used to stamp the\n * previous user's responses `p:anonymous`, which the next logged-out boot\n * hydrated and the next account re-stamped as its own; an account that\n * signed in inside it got them stamped as its own directly (#580). Binding\n * at event time pins the owner of the data, so that write deletes instead.\n * An event while nobody is signed in leaves the handle unbound, which keeps\n * the cookie app whose principal arrives after `/me` persisting.\n */\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n if (getPrincipal) boundOwner ??= currentOwner(getPrincipal);\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n if (snapshot.owner !== (owner ?? ANONYMOUS_SNAPSHOT_OWNER)) return;\n boundOwner ??= owner;\n }\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.cjs","names":["ownerFingerprint","createOfflineStore","isDevBuild","sessionIdentityOf","dehydrate","require_dev_mode","require_session_identity","require_owner_fingerprint","require_create_offline_store","_tanstack_react_query","pendingClears","principalKnown","isPrincipalKnown","clientSessions","recordSession","lineage","current","clientIsEmpty","queryClient","ANONYMOUS_SNAPSHOT_OWNER","currentOwner","getPrincipal","persistQueryClientOffline","options","appPrincipal","auth","session","key","databaseName","store","slot","ended","detached","warnedEnded","warnedRebound","boundOwner","hydratedAnonymous","timer","inFlight","afterPendingClear","pending","warnEnded","warnRebound","entry","observeSession","unsubscribeSession","save","schedule","snapshot","foreign","owner","write","cancelTimer","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { sessionIdentityOf } from \"../auth/session-identity\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/**\n * The session a {@link persistQueryClientOffline} handle follows — what\n * `createTempestAuth` returns satisfies it, so pass the preset as is.\n *\n * Structural on purpose: the query module does not import the auth preset,\n * so an app that persists its cache without it pays nothing for it.\n */\nexport interface OfflineQuerySession {\n /** The key of the session signed in now, or `null` when nobody is. */\n sessionKey: () => string | null;\n /** Whether that session is settled, see `TempestAuth.isSessionKnown`. */\n isSessionKnown: () => boolean;\n}\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint, and on cache changes until the handle is bound.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * The handle is bound to the first signed-in owner it sees on a cache\n * change, a write or a restore — the cache change is what pins the owner of\n * data that a throttled write would otherwise stamp with whoever is signed\n * in a second later (#580). Once bound, a write under another owner —\n * nobody, because the token expired without `clear()`, or a second account\n * on the same tab — deletes the snapshot instead of stamping the bound\n * owner's cached responses with the new owner (#563); the same owner coming\n * back writes again. Writes\n * before anyone signs in stay anonymous and are restored while nobody is,\n * so an app that persists logged-out data keeps doing so. An anonymous\n * snapshot written by 0.82.0 is not restored (it may hold an expired\n * session's cache): one cold start, once.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n *\n * `null` and `undefined` both mean **nobody is signed in** — the same\n * contract as every other `getPrincipal` in the SDK, so `() => user?.id`\n * keeps working. \"Not known yet\" (a cookie session before `/me` answers) is\n * a different state, and this getter cannot express it: pass\n * {@link OfflineQueryPersistenceOptions.isPrincipalKnown} for it. Without\n * that, a write that runs before `/me` stamps the signed-in user's\n * responses as anonymous, and a logged-out boot hydrates them (#590).\n *\n * A handle that hydrated an anonymous snapshot deletes it, instead of\n * writing, once someone signs in on it: the anonymous row may hold a\n * previous account's responses written before its principal was known, and\n * stamping them with the account now signed in is what handed them to it\n * (#590). To keep persisting, end it with `clear({ resetClient: true })`\n * and create a new handle: the in-memory client still holds what the\n * deleted row held, and a new handle on it would stamp that with the\n * account signed in. With {@link OfflineQueryPersistenceOptions.auth} a new\n * handle on such a client keeps deleting until the client is reset; with\n * `getPrincipal` alone, resetting it is up to the app.\n */\n getPrincipal?: () => string | number | null | undefined;\n /**\n * Whether `getPrincipal` already reflects the real session. Only read with\n * `getPrincipal`; omitted, the principal is always known, as before.\n *\n * For an app that learns who is signed in only after a round trip — a\n * cookie session resolved by `/me`, a token restored asynchronously —\n * return `false` until that answer arrives, whatever it is. While it is\n * `false`:\n *\n * - a write does not run and is re-armed for the next throttle window, so\n * the responses already in the cache are stamped with the real owner, not\n * as anonymous;\n * - a cache change does not bind the handle;\n * - `restore()` hydrates nothing — call it again once the principal is\n * known.\n *\n * Without it, `null` before `/me` reads as \"nobody\": a session that ends\n * (tab closed) inside the first throttle window leaves the signed-in user's\n * responses stamped anonymous, for the next logged-out boot to hydrate\n * (#590).\n *\n * Read synchronously; a throw reads as not known, so it fails closed.\n *\n * With {@link OfflineQueryPersistenceOptions.auth} there is no need for\n * it; when both are given, both must say known.\n */\n isPrincipalKnown?: () => boolean;\n /**\n * The auth preset whose session owns the cache — pass what\n * `createTempestAuth` returned. The safe wiring for an app built on it:\n *\n * - the principal is `auth.sessionKey()` unless\n * {@link OfflineQueryPersistenceOptions.getPrincipal} is given, so the\n * cache belongs to one sign-in and survives reloads and token rotations\n * of it; a new sign-in of the same account starts cold;\n * - the principal is known only while `auth.isSessionKnown()` says the\n * session is settled — not while a `login()`, `restore()` or the boot\n * `refresh()` is still deciding it — and, with a `getPrincipal` of your\n * own, only once it names someone whenever a session is installed (a\n * user id read from a profile that has not loaded yet is not \"nobody\");\n * - every session installed while the `QueryClient`'s cache lives is\n * recorded — the preset notifies each install, and every cache change,\n * write and restore reads `sessionKey()` too — not only the one a cache\n * change happens to see. Once a second session, or nobody after one,\n * was installed — a sign-out in another tab, a `logout()` without\n * `clear()`, a second `login()` on top —, every write deletes the row\n * and `restore()` hydrates nothing: the cache may hold the ended\n * session's responses, including one the api client replayed with the\n * bearer of a `login()` or boot `refresh()` that started after the\n * request left with no token and ended before it landed (security audit\n * run-12, C1). Nobody followed by a session is not an end, and a\n * token rotation keeps the key;\n * - that record belongs to the `QueryClient`, not to the handle, so a new\n * handle on the same client keeps deleting. Only a reset drops it:\n * `clear({ resetClient: true })`, after which nothing of the ended\n * session stays in memory, or creating a handle on a client with empty\n * caches. `clear()` without `resetClient` leaves the client as it is,\n * so it marks the record ended instead: no handle on that client\n * persists until it is reset (security audit run-13, C1);\n * - a handle only records sessions while it observes them: after\n * `unsubscribe()` its `flush()` writes nothing, and a handle created on\n * a client that already holds responses cannot know which session\n * fetched them — create it before the first query, at boot;\n * A session that is not this preset (an object of your own) is only\n * read on cache changes, writes and restores, so a session installed\n * and ended between two of them goes unseen;\n * - a `sessionKey()` that throws reads as not known, so nothing is written.\n *\n * It does not call `clear()` on session end by itself: `clear()` ends the\n * handle, and the row is already deleted on the next write. Keep calling\n * `clear()` on logout, which also stops persisting at once.\n *\n * Before it, a cookie app had to wire `isPrincipalKnown` by hand, and the\n * doc saying so was the only guard: without it, a tab closed before `/me`\n * answered left the signed-in account's responses stamped anonymous for\n * the next signed-out boot to hydrate (#596). Call `restore()` after the\n * boot `auth.refresh()` settles; called before, it hydrates only what\n * nobody owns.\n */\n auth?: OfflineQuerySession;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n *\n * With `auth`, only the reset lets a new handle on the same client persist\n * the next session. Without it the client still holds the ended session's\n * responses, so `clear()` marks the client's session record ended and\n * every handle on it deletes its row instead of writing, until the client\n * is reset or a handle is created on it with empty caches. Before 0.85.0,\n * `clear()` dropped that record, and the handle created at login stamped\n * the previous account's responses with the new one (security audit\n * run-13, C1).\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal or while\n * `isPrincipalKnown` returns `false`; call it again once it is known.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called, and,\n * with `auth`, once {@link OfflineQueryPersistence.unsubscribe} has.\n */\n flush: () => Promise<void>;\n /**\n * Stop persisting and drop the pending write. Idempotent.\n *\n * With `auth`, a later `flush()` writes nothing: the handle no longer sees\n * sessions installed and ended, so it cannot tell whose responses the\n * cache holds (security audit run-13, N3).\n */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * With `auth`, the reset decides whether the next session persists on the\n * same client. With it, the client's session record is dropped after\n * `queryClient.clear()`, and a handle created at login persists the new\n * account. Without it, the client still holds this session's responses,\n * so the record is marked ended — never dropped — and every handle on the\n * client, a new one or one on another `key`, deletes its row instead of\n * writing until the client is reset or a handle is created on it with\n * empty caches (security audit run-13, C1).\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\n/**\n * Whether the app says its principal is settled.\n *\n * @param isPrincipalKnown - The app's getter, if any.\n * @returns `true` without a getter; `false` when it returns anything but\n * `true` or throws, so an unknown principal fails closed.\n */\nfunction principalKnown(isPrincipalKnown: (() => boolean) | undefined): boolean {\n if (!isPrincipalKnown) return true;\n try {\n return isPrincipalKnown() === true;\n } catch {\n return false;\n }\n}\n\n/**\n * The sessions whose responses a `QueryClient`'s in-memory cache may hold, as\n * the `auth` handles on it saw them.\n *\n * Kept per client, not per handle, because the cache is the client's: a handle\n * created after another one stopped persisting (the advice of the dev warning)\n * still sits on the cache that holds the ended session's responses, and used to\n * stamp them with the session signed in when it was created (security audit\n * run-12, hardening). The entry is dropped only once the client is reset: by\n * `clear({ resetClient: true })`, after `queryClient.clear()`, and when a\n * handle is created on a client whose caches are empty. `clear()` without the\n * reset marks it ended instead, because the client still holds the session's\n * responses, and it may be shared by a handle on another `key` (security audit\n * run-13, C1).\n */\ninterface ClientSessions {\n /** The first session seen installed, or `null` while none has been. */\n session: string | null;\n /** Whether a handle hydrated an anonymous snapshot into this client. */\n hydratedAnonymous: boolean;\n /**\n * Whether the cache may hold a session that is not the one installed now:\n * a second session, or nobody after one. Never reset while the entry lives.\n */\n ended: boolean;\n}\n\nconst clientSessions = new WeakMap<QueryClient, ClientSessions>();\n\n/**\n * Record the session installed now on a client's lineage.\n *\n * Every session installed while the cache lives counts, not only the one a\n * cache change happens to see: the api client replays a request that left\n * without a token with the bearer of a `login()` or boot `refresh()` still in\n * flight, so a response can belong to a session that was installed and ended\n * between two cache events, with nobody signed in at both (security audit\n * run-12, C1). The lineage ends when a second session or nobody follows the\n * first one, and when someone signs in on a client holding a hydrated\n * anonymous snapshot, which may be a previous account's (#590). Nobody\n * followed by a session is not an end: responses fetched while nobody was\n * signed in carry no account.\n *\n * @param lineage - The client's entry.\n * @param current - The key of the session installed now, or `null`.\n */\nfunction recordSession(lineage: ClientSessions, current: string | null): void {\n if (lineage.ended) return;\n if (current === null) {\n lineage.ended = lineage.session !== null;\n return;\n }\n if (lineage.hydratedAnonymous || (lineage.session !== null && lineage.session !== current)) {\n lineage.ended = true;\n return;\n }\n lineage.session = current;\n}\n\n/**\n * Whether the client holds no query and no mutation — nothing any session\n * could have left in it.\n *\n * @param queryClient - The client.\n * @returns `true` when both caches are empty.\n */\nfunction clientIsEmpty(queryClient: QueryClient): boolean {\n return (\n queryClient.getQueryCache().getAll().length === 0 &&\n queryClient.getMutationCache().getAll().length === 0\n );\n}\n\n/**\n * Owner stamp of a snapshot written while nobody was signed in.\n *\n * Not a fingerprint (`ownerFingerprint` only yields `p:` + 16 hex characters),\n * so it never matches a signed-in owner. It replaces the `null` stamp of 0.82.0\n * on purpose: a `null` row from 0.82.0 may hold the cache of a session that\n * expired before the write (#563), and nothing in it tells that row apart from\n * genuinely anonymous data, so `restore()` skips it — one cold start, once.\n */\nconst ANONYMOUS_SNAPSHOT_OWNER = \"p:anonymous\";\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /**\n * Owner fingerprint, or {@link ANONYMOUS_SNAPSHOT_OWNER}, when `getPrincipal`\n * was set at write time; absent otherwise.\n */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, auth } = options;\n const appPrincipal = options.getPrincipal;\n const getPrincipal = appPrincipal ?? auth?.sessionKey;\n const isPrincipalKnown = auth\n ? (): boolean => {\n const session = auth.sessionKey();\n return (\n principalKnown(options.isPrincipalKnown) &&\n auth.isSessionKnown() &&\n (appPrincipal === undefined ||\n session === null ||\n currentOwner(appPrincipal) !== null)\n );\n }\n : options.isPrincipalKnown;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let detached = false;\n let warnedEnded = false;\n let warnedRebound = false;\n let boundOwner: string | null = null;\n let hydratedAnonymous = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n function warnRebound(): void {\n if (warnedRebound || !isDevBuild()) return;\n warnedRebound = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: an account signed in on a \" +\n \"handle holding another session's cache; its snapshot is deleted instead of \" +\n \"written. Call clear({ resetClient: true }), then create a new handle.\",\n );\n }\n\n if (auth && clientIsEmpty(queryClient)) clientSessions.delete(queryClient);\n\n function lineage(): ClientSessions {\n let entry = clientSessions.get(queryClient);\n if (!entry) {\n entry = { session: null, hydratedAnonymous: false, ended: false };\n clientSessions.set(queryClient, entry);\n }\n return entry;\n }\n\n /**\n * Record on the client's lineage the session `auth` holds now. Runs on\n * every auth state change the preset notifies, and on every cache change,\n * write and restore. A `sessionKey()` that throws records nothing.\n *\n * @returns Whether the client's cache may hold a session that has ended.\n */\n function observeSession(): boolean {\n if (!auth || ended) return false;\n const entry = lineage();\n try {\n recordSession(entry, auth.sessionKey());\n } catch {\n return entry.ended;\n }\n return entry.ended;\n }\n\n const unsubscribeSession = auth\n ? (sessionIdentityOf(auth.sessionKey)?.subscribe(observeSession) ?? null)\n : null;\n observeSession();\n\n /**\n * Write the snapshot, or delete the row when the session it holds is over.\n *\n * With `getPrincipal`, the handle is bound to the first signed-in owner it\n * sees on a cache change, a write or a restore. A write under any other owner — nobody, after a token\n * expired, or a second account signing in on the same tab — deletes the row\n * instead of stamping it: the in-memory cache still holds the bound owner's\n * responses, and stamping them with the new owner is what handed them to the\n * next account (#563). The same owner coming back writes again.\n *\n * A second account (not an expiry) means the app kept the handle across a\n * login, which stops persistence for that account in silence; a dev build\n * logs one `console.warn` saying so, as for a handle ended by `clear()`.\n * Someone signing in on a handle that hydrated an anonymous snapshot is\n * treated the same way: that row may hold an account's responses written\n * before its principal was known, so it is deleted, not re-stamped (#590).\n *\n * While `isPrincipalKnown` says the principal is not settled, nothing is\n * written and the write is re-armed for the next window (#590).\n *\n * With `auth`, a client whose lineage ended — a second session, or nobody\n * after one, was installed while its cache lived — deletes the row as\n * well, whatever the handle is bound to (security audit run-12, C1).\n */\n async function save(): Promise<void> {\n if (ended || (auth && detached)) return;\n await afterPendingClear();\n if (ended || (auth && detached)) return;\n if (getPrincipal && !principalKnown(isPrincipalKnown)) {\n schedule();\n return;\n }\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n let foreign = false;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n foreign =\n observeSession() ||\n (boundOwner !== null && owner !== boundOwner) ||\n (hydratedAnonymous && owner !== null);\n if (foreign && owner !== null) warnRebound();\n boundOwner ??= owner;\n snapshot.owner = owner ?? ANONYMOUS_SNAPSHOT_OWNER;\n }\n const write = foreign ? store.delete(key) : store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n /**\n * Schedule a write for cache activity, or detach once the handle has ended.\n *\n * With `getPrincipal`, an unbound handle is bound here, to whoever is\n * signed in when the cache changes — not only by a write or a restore. The\n * write is throttled, so the principal it reads can already be someone\n * else's: a session that expired inside the first window used to stamp the\n * previous user's responses `p:anonymous`, which the next logged-out boot\n * hydrated and the next account re-stamped as its own; an account that\n * signed in inside it got them stamped as its own directly (#580). Binding\n * at event time pins the owner of the data, so that write deletes instead.\n * An event while nobody is signed in leaves the handle unbound, which keeps\n * the cookie app whose principal arrives after `/me` persisting; so does\n * an event while `isPrincipalKnown` says the principal is not settled.\n * With `auth`, every event also records the session installed now on the\n * client's lineage, see `recordSession`.\n */\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n observeSession();\n if (getPrincipal && principalKnown(isPrincipalKnown)) {\n boundOwner ??= currentOwner(getPrincipal);\n }\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n /**\n * Wait for the writes in flight, delete the row and, with `resetClient`,\n * reset the client. Only that reset drops the client's lineage, after\n * `queryClient.clear()`: nothing of an ended session is left in memory to\n * stamp, so the next session starts a new one.\n *\n * @param resetClient - Whether to clear the in-memory `QueryClient`.\n */\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) {\n queryClient.clear();\n if (auth) clientSessions.delete(queryClient);\n }\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal) {\n if (!principalKnown(isPrincipalKnown) || observeSession()) return;\n const owner = currentOwner(getPrincipal);\n if (snapshot.owner !== (owner ?? ANONYMOUS_SNAPSHOT_OWNER)) return;\n boundOwner ??= owner;\n if (owner === null) {\n hydratedAnonymous = true;\n if (auth) lineage().hydratedAnonymous = true;\n }\n }\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n detached = true;\n cancelTimer();\n unsubscribeCache();\n unsubscribeSession?.();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n unsubscribeSession?.();\n if (auth) lineage().ended = true;\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,104 +1,150 @@
|
|
|
1
1
|
import { isDevBuild as e } from "../utils/dev-mode.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { sessionIdentityOf as t } from "../auth/session-identity.js";
|
|
3
|
+
import { ownerFingerprint as n } from "../utils/owner-fingerprint.js";
|
|
4
|
+
import { createOfflineStore as r } from "../offline/create-offline-store.js";
|
|
5
|
+
import { dehydrate as i, hydrate as a } from "@tanstack/react-query";
|
|
5
6
|
//#region src/query/offline-persistence.ts
|
|
6
|
-
var
|
|
7
|
+
var o = /* @__PURE__ */ new Map();
|
|
7
8
|
function s(e) {
|
|
9
|
+
if (!e) return !0;
|
|
8
10
|
try {
|
|
9
|
-
return
|
|
11
|
+
return e() === !0;
|
|
12
|
+
} catch {
|
|
13
|
+
return !1;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
var c = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
function l(e, t) {
|
|
18
|
+
e.ended || (t === null ? e.ended = e.session !== null : e.hydratedAnonymous || e.session !== null && e.session !== t ? e.ended = !0 : e.session = t);
|
|
19
|
+
}
|
|
20
|
+
function u(e) {
|
|
21
|
+
return e.getQueryCache().getAll().length === 0 && e.getMutationCache().getAll().length === 0;
|
|
22
|
+
}
|
|
23
|
+
var d = "p:anonymous";
|
|
24
|
+
function f(e) {
|
|
25
|
+
try {
|
|
26
|
+
return n("p", e());
|
|
10
27
|
} catch {
|
|
11
28
|
return null;
|
|
12
29
|
}
|
|
13
30
|
}
|
|
14
|
-
function
|
|
15
|
-
let { queryClient:
|
|
16
|
-
|
|
31
|
+
function p(n) {
|
|
32
|
+
let { queryClient: p, throttleMs: m = 1e3, auth: h } = n, g = n.getPrincipal, _ = g ?? h?.sessionKey, v = h ? () => {
|
|
33
|
+
let e = h.sessionKey();
|
|
34
|
+
return s(n.isPrincipalKnown) && h.isSessionKnown() && (g === void 0 || e === null || f(g) !== null);
|
|
35
|
+
} : n.isPrincipalKnown, y = n.key ?? "tanstack-query", b = n.databaseName ?? "TempestQueryCache", x = r({
|
|
36
|
+
databaseName: b,
|
|
17
37
|
version: 1,
|
|
18
38
|
tableName: "cache",
|
|
19
39
|
indexes: "&id",
|
|
20
40
|
keyPath: "id"
|
|
21
|
-
}),
|
|
22
|
-
async function
|
|
23
|
-
let e =
|
|
41
|
+
}), S = `${b}\u0000${y}`, C = !1, w = !1, T = !1, E = !1, D = null, O = !1, k = null, A = /* @__PURE__ */ new Set();
|
|
42
|
+
async function j() {
|
|
43
|
+
let e = o.get(S);
|
|
24
44
|
e && await e;
|
|
25
45
|
}
|
|
26
|
-
function
|
|
27
|
-
!
|
|
46
|
+
function M() {
|
|
47
|
+
!T && e() && (T = !0, console.warn("[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); create a new one after login to keep persisting."));
|
|
28
48
|
}
|
|
29
|
-
function
|
|
30
|
-
!
|
|
49
|
+
function N() {
|
|
50
|
+
!E && e() && (E = !0, console.warn("[tempest-react-sdk] persistQueryClientOffline: an account signed in on a handle holding another session's cache; its snapshot is deleted instead of written. Call clear({ resetClient: true }), then create a new handle."));
|
|
31
51
|
}
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
h && u(p) && c.delete(p);
|
|
53
|
+
function P() {
|
|
54
|
+
let e = c.get(p);
|
|
55
|
+
return e || (e = {
|
|
56
|
+
session: null,
|
|
57
|
+
hydratedAnonymous: !1,
|
|
58
|
+
ended: !1
|
|
59
|
+
}, c.set(p, e)), e;
|
|
60
|
+
}
|
|
61
|
+
function F() {
|
|
62
|
+
if (!h || C) return !1;
|
|
63
|
+
let e = P();
|
|
64
|
+
try {
|
|
65
|
+
l(e, h.sessionKey());
|
|
66
|
+
} catch {
|
|
67
|
+
return e.ended;
|
|
68
|
+
}
|
|
69
|
+
return e.ended;
|
|
70
|
+
}
|
|
71
|
+
let I = h ? t(h.sessionKey)?.subscribe(F) ?? null : null;
|
|
72
|
+
F();
|
|
73
|
+
async function L() {
|
|
74
|
+
if (C || h && w || (await j(), C || h && w)) return;
|
|
75
|
+
if (_ && !s(v)) {
|
|
76
|
+
z();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
34
79
|
let e = {
|
|
35
|
-
id:
|
|
36
|
-
state:
|
|
80
|
+
id: y,
|
|
81
|
+
state: i(p)
|
|
37
82
|
}, t = !1;
|
|
38
|
-
if (
|
|
39
|
-
let n =
|
|
40
|
-
t =
|
|
83
|
+
if (_) {
|
|
84
|
+
let n = f(_);
|
|
85
|
+
t = F() || D !== null && n !== D || O && n !== null, t && n !== null && N(), D ??= n, e.owner = n ?? d;
|
|
41
86
|
}
|
|
42
|
-
let n = t ?
|
|
43
|
-
|
|
87
|
+
let n = t ? x.delete(y) : x.put(e);
|
|
88
|
+
A.add(n);
|
|
44
89
|
try {
|
|
45
90
|
await n;
|
|
46
91
|
} finally {
|
|
47
|
-
|
|
92
|
+
A.delete(n);
|
|
48
93
|
}
|
|
49
94
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
95
|
+
function R() {
|
|
96
|
+
k &&= (clearTimeout(k), null);
|
|
52
97
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
98
|
+
function z() {
|
|
99
|
+
k || C || (k = setTimeout(() => {
|
|
100
|
+
k = null, L();
|
|
101
|
+
}, m));
|
|
57
102
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
103
|
+
function B(e) {
|
|
104
|
+
C ? (e.type === "added" || e.type === "updated") && (V(), M()) : (F(), _ && s(v) && (D ??= f(_)), z());
|
|
60
105
|
}
|
|
61
|
-
let
|
|
62
|
-
async function
|
|
106
|
+
let V = p.getQueryCache().subscribe(B);
|
|
107
|
+
async function H(e) {
|
|
63
108
|
try {
|
|
64
|
-
await Promise.allSettled([...
|
|
109
|
+
await Promise.allSettled([...A]), await x.delete(y);
|
|
65
110
|
} finally {
|
|
66
|
-
e &&
|
|
111
|
+
e && (p.clear(), h && c.delete(p));
|
|
67
112
|
}
|
|
68
113
|
}
|
|
69
114
|
return {
|
|
70
115
|
async restore() {
|
|
71
|
-
if (
|
|
72
|
-
|
|
116
|
+
if (C) {
|
|
117
|
+
M();
|
|
73
118
|
return;
|
|
74
119
|
}
|
|
75
|
-
if (await
|
|
76
|
-
let e = await
|
|
77
|
-
if (e && !
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
120
|
+
if (await j(), C) return;
|
|
121
|
+
let e = await x.get(y);
|
|
122
|
+
if (e && !C) {
|
|
123
|
+
if (_) {
|
|
124
|
+
if (!s(v) || F()) return;
|
|
125
|
+
let t = f(_);
|
|
126
|
+
if (e.owner !== (t ?? d)) return;
|
|
127
|
+
D ??= t, t === null && (O = !0, h && (P().hydratedAnonymous = !0));
|
|
82
128
|
}
|
|
83
|
-
|
|
129
|
+
a(p, e.state);
|
|
84
130
|
}
|
|
85
131
|
},
|
|
86
132
|
async flush() {
|
|
87
|
-
|
|
133
|
+
C ? M() : (R(), await L());
|
|
88
134
|
},
|
|
89
135
|
unsubscribe() {
|
|
90
|
-
|
|
136
|
+
w = !0, R(), V(), I?.();
|
|
91
137
|
},
|
|
92
138
|
async clear(e = {}) {
|
|
93
|
-
|
|
94
|
-
let t =
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
C = !0, R(), I?.(), h && (P().ended = !0);
|
|
140
|
+
let t = o.get(S), n = H(e.resetClient ?? !1), r = Promise.allSettled([t, n]);
|
|
141
|
+
o.set(S, r), r.then(() => {
|
|
142
|
+
o.get(S) === r && o.delete(S);
|
|
97
143
|
}), await n;
|
|
98
144
|
}
|
|
99
145
|
};
|
|
100
146
|
}
|
|
101
147
|
//#endregion
|
|
102
|
-
export {
|
|
148
|
+
export { p as persistQueryClientOffline };
|
|
103
149
|
|
|
104
150
|
//# sourceMappingURL=offline-persistence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-persistence.js","names":["pendingClears","ANONYMOUS_SNAPSHOT_OWNER","currentOwner","ownerFingerprint","getPrincipal","persistQueryClientOffline","options","key","databaseName","store","createOfflineStore","slot","ended","warnedEnded","warnedRebound","boundOwner","timer","inFlight","afterPendingClear","pending","warnEnded","isDevBuild","warnRebound","save","snapshot","dehydrate","queryClient","foreign","owner","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint, and on cache changes until the handle is bound.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * The handle is bound to the first signed-in owner it sees on a cache\n * change, a write or a restore — the cache change is what pins the owner of\n * data that a throttled write would otherwise stamp with whoever is signed\n * in a second later (#580). Once bound, a write under another owner —\n * nobody, because the token expired without `clear()`, or a second account\n * on the same tab — deletes the snapshot instead of stamping the bound\n * owner's cached responses with the new owner (#563); the same owner coming\n * back writes again. Writes\n * before anyone signs in stay anonymous and are restored while nobody is,\n * so an app that persists logged-out data keeps doing so. An anonymous\n * snapshot written by 0.82.0 is not restored (it may hold an expired\n * session's cache): one cold start, once.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n */\n getPrincipal?: () => string | number | null | undefined;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called.\n */\n flush: () => Promise<void>;\n /** Stop persisting and drop the pending write. Idempotent. */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\n/**\n * Owner stamp of a snapshot written while nobody was signed in.\n *\n * Not a fingerprint (`ownerFingerprint` only yields `p:` + 16 hex characters),\n * so it never matches a signed-in owner. It replaces the `null` stamp of 0.82.0\n * on purpose: a `null` row from 0.82.0 may hold the cache of a session that\n * expired before the write (#563), and nothing in it tells that row apart from\n * genuinely anonymous data, so `restore()` skips it — one cold start, once.\n */\nconst ANONYMOUS_SNAPSHOT_OWNER = \"p:anonymous\";\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /**\n * Owner fingerprint, or {@link ANONYMOUS_SNAPSHOT_OWNER}, when `getPrincipal`\n * was set at write time; absent otherwise.\n */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, getPrincipal } = options;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let warnedEnded = false;\n let warnedRebound = false;\n let boundOwner: string | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n function warnRebound(): void {\n if (warnedRebound || !isDevBuild()) return;\n warnedRebound = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: another account signed in on a \" +\n \"handle bound to the previous one; its snapshot is deleted instead of written. \" +\n \"Create a new handle after login to keep persisting.\",\n );\n }\n\n /**\n * Write the snapshot, or delete the row when the session it holds is over.\n *\n * With `getPrincipal`, the handle is bound to the first signed-in owner it\n * sees on a cache change, a write or a restore. A write under any other owner — nobody, after a token\n * expired, or a second account signing in on the same tab — deletes the row\n * instead of stamping it: the in-memory cache still holds the bound owner's\n * responses, and stamping them with the new owner is what handed them to the\n * next account (#563). The same owner coming back writes again.\n *\n * A second account (not an expiry) means the app kept the handle across a\n * login, which stops persistence for that account in silence; a dev build\n * logs one `console.warn` saying so, as for a handle ended by `clear()`.\n */\n async function save(): Promise<void> {\n if (ended) return;\n await afterPendingClear();\n if (ended) return;\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n let foreign = false;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n foreign = boundOwner !== null && owner !== boundOwner;\n if (foreign && owner !== null) warnRebound();\n boundOwner ??= owner;\n snapshot.owner = owner ?? ANONYMOUS_SNAPSHOT_OWNER;\n }\n const write = foreign ? store.delete(key) : store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n /**\n * Schedule a write for cache activity, or detach once the handle has ended.\n *\n * With `getPrincipal`, an unbound handle is bound here, to whoever is\n * signed in when the cache changes — not only by a write or a restore. The\n * write is throttled, so the principal it reads can already be someone\n * else's: a session that expired inside the first window used to stamp the\n * previous user's responses `p:anonymous`, which the next logged-out boot\n * hydrated and the next account re-stamped as its own; an account that\n * signed in inside it got them stamped as its own directly (#580). Binding\n * at event time pins the owner of the data, so that write deletes instead.\n * An event while nobody is signed in leaves the handle unbound, which keeps\n * the cookie app whose principal arrives after `/me` persisting.\n */\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n if (getPrincipal) boundOwner ??= currentOwner(getPrincipal);\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) queryClient.clear();\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n if (snapshot.owner !== (owner ?? ANONYMOUS_SNAPSHOT_OWNER)) return;\n boundOwner ??= owner;\n }\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n cancelTimer();\n unsubscribeCache();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"offline-persistence.js","names":["pendingClears","principalKnown","isPrincipalKnown","clientSessions","recordSession","lineage","current","clientIsEmpty","queryClient","ANONYMOUS_SNAPSHOT_OWNER","currentOwner","ownerFingerprint","getPrincipal","persistQueryClientOffline","options","appPrincipal","auth","session","key","databaseName","store","createOfflineStore","slot","ended","detached","warnedEnded","warnedRebound","boundOwner","hydratedAnonymous","timer","inFlight","afterPendingClear","pending","warnEnded","isDevBuild","warnRebound","entry","observeSession","unsubscribeSession","sessionIdentityOf","save","snapshot","dehydrate","foreign","owner","write","cancelTimer","schedule","throttleMs","onCacheEvent","event","unsubscribeCache","endSession","resetClient","previous","run","clearOptions","settled"],"sources":["../../src/query/offline-persistence.ts"],"sourcesContent":["import {\n dehydrate,\n hydrate,\n type QueryCacheNotifyEvent,\n type QueryClient,\n} from \"@tanstack/react-query\";\nimport { sessionIdentityOf } from \"../auth/session-identity\";\nimport { createOfflineStore } from \"../offline/create-offline-store\";\nimport { isDevBuild } from \"../utils/dev-mode\";\nimport { ownerFingerprint } from \"../utils/owner-fingerprint\";\n\n/**\n * Clears still running, per `databaseName` + `key` slot, across every handle.\n *\n * A handle's `clear()` deletes by key with no ordering against other handles,\n * so a new handle created on login while the previous one is still clearing\n * could `restore()` the previous user's snapshot before the delete lands, or\n * have its first write deleted by it. Every handle's `restore()` and writes\n * wait for the slot's pending clear first; the entry is dropped once settled.\n */\nconst pendingClears = new Map<string, Promise<unknown>>();\n\n/**\n * The session a {@link persistQueryClientOffline} handle follows — what\n * `createTempestAuth` returns satisfies it, so pass the preset as is.\n *\n * Structural on purpose: the query module does not import the auth preset,\n * so an app that persists its cache without it pays nothing for it.\n */\nexport interface OfflineQuerySession {\n /** The key of the session signed in now, or `null` when nobody is. */\n sessionKey: () => string | null;\n /** Whether that session is settled, see `TempestAuth.isSessionKnown`. */\n isSessionKnown: () => boolean;\n}\n\n/** Options for {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistenceOptions {\n /** The `QueryClient` to snapshot and restore. */\n queryClient: QueryClient;\n /** IndexedDB database name for the snapshot. Default `\"TempestQueryCache\"`. */\n databaseName?: string;\n /** Row key under which the snapshot is stored. Default `\"tanstack-query\"`. */\n key?: string;\n /**\n * Minimum gap between snapshot writes, in ms — cache churn is coalesced into\n * one trailing write per window. Default `1000`.\n */\n throttleMs?: number;\n /**\n * Who is signed in right now — a stable, non-secret id such as the user id;\n * `null`/`undefined` when nobody is. Read on every write, which stamps the\n * snapshot with a one-way fingerprint of it (never the id itself), on\n * `restore()`, which hydrates only a snapshot stamped with the same\n * fingerprint, and on cache changes until the handle is bound.\n *\n * `clear()` on logout already deletes the snapshot; this covers the session\n * that ends without it — a token that expired, a crash, a tab closed while\n * signed in. Without it, the next account to boot on the browser is shown\n * the previous account's cached responses (#531). A snapshot written before\n * this option was set carries no stamp and is not restored either; it is a\n * cache, so the cost is one cold start.\n *\n * A foreign snapshot is skipped, not deleted: a cookie app that only learns\n * the principal after a `/me` round trip would otherwise wipe its own cache\n * on every cold start. The next write replaces it.\n *\n * The handle is bound to the first signed-in owner it sees on a cache\n * change, a write or a restore — the cache change is what pins the owner of\n * data that a throttled write would otherwise stamp with whoever is signed\n * in a second later (#580). Once bound, a write under another owner —\n * nobody, because the token expired without `clear()`, or a second account\n * on the same tab — deletes the snapshot instead of stamping the bound\n * owner's cached responses with the new owner (#563); the same owner coming\n * back writes again. Writes\n * before anyone signs in stay anonymous and are restored while nobody is,\n * so an app that persists logged-out data keeps doing so. An anonymous\n * snapshot written by 0.82.0 is not restored (it may hold an expired\n * session's cache): one cold start, once.\n *\n * Read synchronously. A non-string, non-number value (the `Promise` of an\n * `async` getter) or a getter that throws reads as nobody. Omit it to restore\n * every snapshot, as before 0.82.0.\n *\n * `null` and `undefined` both mean **nobody is signed in** — the same\n * contract as every other `getPrincipal` in the SDK, so `() => user?.id`\n * keeps working. \"Not known yet\" (a cookie session before `/me` answers) is\n * a different state, and this getter cannot express it: pass\n * {@link OfflineQueryPersistenceOptions.isPrincipalKnown} for it. Without\n * that, a write that runs before `/me` stamps the signed-in user's\n * responses as anonymous, and a logged-out boot hydrates them (#590).\n *\n * A handle that hydrated an anonymous snapshot deletes it, instead of\n * writing, once someone signs in on it: the anonymous row may hold a\n * previous account's responses written before its principal was known, and\n * stamping them with the account now signed in is what handed them to it\n * (#590). To keep persisting, end it with `clear({ resetClient: true })`\n * and create a new handle: the in-memory client still holds what the\n * deleted row held, and a new handle on it would stamp that with the\n * account signed in. With {@link OfflineQueryPersistenceOptions.auth} a new\n * handle on such a client keeps deleting until the client is reset; with\n * `getPrincipal` alone, resetting it is up to the app.\n */\n getPrincipal?: () => string | number | null | undefined;\n /**\n * Whether `getPrincipal` already reflects the real session. Only read with\n * `getPrincipal`; omitted, the principal is always known, as before.\n *\n * For an app that learns who is signed in only after a round trip — a\n * cookie session resolved by `/me`, a token restored asynchronously —\n * return `false` until that answer arrives, whatever it is. While it is\n * `false`:\n *\n * - a write does not run and is re-armed for the next throttle window, so\n * the responses already in the cache are stamped with the real owner, not\n * as anonymous;\n * - a cache change does not bind the handle;\n * - `restore()` hydrates nothing — call it again once the principal is\n * known.\n *\n * Without it, `null` before `/me` reads as \"nobody\": a session that ends\n * (tab closed) inside the first throttle window leaves the signed-in user's\n * responses stamped anonymous, for the next logged-out boot to hydrate\n * (#590).\n *\n * Read synchronously; a throw reads as not known, so it fails closed.\n *\n * With {@link OfflineQueryPersistenceOptions.auth} there is no need for\n * it; when both are given, both must say known.\n */\n isPrincipalKnown?: () => boolean;\n /**\n * The auth preset whose session owns the cache — pass what\n * `createTempestAuth` returned. The safe wiring for an app built on it:\n *\n * - the principal is `auth.sessionKey()` unless\n * {@link OfflineQueryPersistenceOptions.getPrincipal} is given, so the\n * cache belongs to one sign-in and survives reloads and token rotations\n * of it; a new sign-in of the same account starts cold;\n * - the principal is known only while `auth.isSessionKnown()` says the\n * session is settled — not while a `login()`, `restore()` or the boot\n * `refresh()` is still deciding it — and, with a `getPrincipal` of your\n * own, only once it names someone whenever a session is installed (a\n * user id read from a profile that has not loaded yet is not \"nobody\");\n * - every session installed while the `QueryClient`'s cache lives is\n * recorded — the preset notifies each install, and every cache change,\n * write and restore reads `sessionKey()` too — not only the one a cache\n * change happens to see. Once a second session, or nobody after one,\n * was installed — a sign-out in another tab, a `logout()` without\n * `clear()`, a second `login()` on top —, every write deletes the row\n * and `restore()` hydrates nothing: the cache may hold the ended\n * session's responses, including one the api client replayed with the\n * bearer of a `login()` or boot `refresh()` that started after the\n * request left with no token and ended before it landed (security audit\n * run-12, C1). Nobody followed by a session is not an end, and a\n * token rotation keeps the key;\n * - that record belongs to the `QueryClient`, not to the handle, so a new\n * handle on the same client keeps deleting. Only a reset drops it:\n * `clear({ resetClient: true })`, after which nothing of the ended\n * session stays in memory, or creating a handle on a client with empty\n * caches. `clear()` without `resetClient` leaves the client as it is,\n * so it marks the record ended instead: no handle on that client\n * persists until it is reset (security audit run-13, C1);\n * - a handle only records sessions while it observes them: after\n * `unsubscribe()` its `flush()` writes nothing, and a handle created on\n * a client that already holds responses cannot know which session\n * fetched them — create it before the first query, at boot;\n * A session that is not this preset (an object of your own) is only\n * read on cache changes, writes and restores, so a session installed\n * and ended between two of them goes unseen;\n * - a `sessionKey()` that throws reads as not known, so nothing is written.\n *\n * It does not call `clear()` on session end by itself: `clear()` ends the\n * handle, and the row is already deleted on the next write. Keep calling\n * `clear()` on logout, which also stops persisting at once.\n *\n * Before it, a cookie app had to wire `isPrincipalKnown` by hand, and the\n * doc saying so was the only guard: without it, a tab closed before `/me`\n * answered left the signed-in account's responses stamped anonymous for\n * the next signed-out boot to hydrate (#596). Call `restore()` after the\n * boot `auth.refresh()` settles; called before, it hydrates only what\n * nobody owns.\n */\n auth?: OfflineQuerySession;\n}\n\n/** Options for {@link OfflineQueryPersistence.clear}. */\nexport interface OfflineQueryClearOptions {\n /**\n * Also clear the in-memory `QueryClient` (`queryClient.clear()`) once\n * persistence has stopped and the snapshot is deleted — the one-call logout.\n * Default `false`: the client belongs to the app, which may want to keep\n * public queries (a catalog, feature flags) across sessions and drop only\n * the user-scoped keys with `removeQueries`.\n *\n * With `auth`, only the reset lets a new handle on the same client persist\n * the next session. Without it the client still holds the ended session's\n * responses, so `clear()` marks the client's session record ended and\n * every handle on it deletes its row instead of writing, until the client\n * is reset or a handle is created on it with empty caches. Before 0.85.0,\n * `clear()` dropped that record, and the handle created at login stamped\n * the previous account's responses with the new one (security audit\n * run-13, C1).\n */\n resetClient?: boolean;\n}\n\n/** Handle returned by {@link persistQueryClientOffline}. */\nexport interface OfflineQueryPersistence {\n /**\n * Load the persisted snapshot into the `QueryClient`. Call once on boot,\n * before rendering queries, so cached data is available offline. A no-op\n * once {@link OfflineQueryPersistence.clear} has been called, and — with\n * `getPrincipal` — for a snapshot written under another principal or while\n * `isPrincipalKnown` returns `false`; call it again once it is known.\n */\n restore: () => Promise<void>;\n /**\n * Write the current cache snapshot immediately (bypassing the throttle). A\n * no-op once {@link OfflineQueryPersistence.clear} has been called, and,\n * with `auth`, once {@link OfflineQueryPersistence.unsubscribe} has.\n */\n flush: () => Promise<void>;\n /**\n * Stop persisting and drop the pending write. Idempotent.\n *\n * With `auth`, a later `flush()` writes nothing: the handle no longer sees\n * sessions installed and ended, so it cannot tell whose responses the\n * cache holds (security audit run-13, N3).\n */\n unsubscribe: () => void;\n /**\n * End the persisted session (e.g. on logout) and delete the snapshot.\n *\n * Terminal for this handle: it cancels the pending throttled write, stops\n * persisting cache changes, waits for any write already in flight and only\n * then deletes the row — so neither a write pending in the last throttle\n * window nor cache activity after logout (the next user's first query) can\n * re-persist the previous user's responses. Afterwards `restore`/`flush`\n * are no-ops; to persist a new session, call\n * {@link persistQueryClientOffline} again after login. In a dev build\n * (`isDevBuild()`), the first `restore`, `flush` or added/updated query on\n * an ended handle logs one `console.warn` saying so, because a handle kept\n * at module scope would otherwise stop persisting in silence.\n *\n * Ordered against other handles on the same `databaseName` + `key`: until\n * this clear settles, a new handle's `restore()` and writes wait for it, so\n * a handle created on login without awaiting the logout neither hydrates\n * the previous snapshot nor has its first write deleted.\n *\n * If the delete rejects, `resetClient` still clears the client and the\n * rejection reaches the caller.\n *\n * The in-memory `QueryClient` is cleared only with `resetClient: true`, and\n * then only after persistence has stopped, so the reset itself cannot\n * trigger a write. It is opt-in because the app owns the client and the\n * shape of its logout: it may keep public queries and drop only the\n * user-scoped keys with `removeQueries`. Because persistence is already\n * off, that manual call can come before or after this one.\n *\n * With `auth`, the reset decides whether the next session persists on the\n * same client. With it, the client's session record is dropped after\n * `queryClient.clear()`, and a handle created at login persists the new\n * account. Without it, the client still holds this session's responses,\n * so the record is marked ended — never dropped — and every handle on the\n * client, a new one or one on another `key`, deletes its row instead of\n * writing until the client is reset or a handle is created on it with\n * empty caches (security audit run-13, C1).\n *\n * Idempotent and safe to call concurrently; every call deletes the row\n * again.\n *\n * @param options - `resetClient` also clears the in-memory `QueryClient`.\n */\n clear: (options?: OfflineQueryClearOptions) => Promise<void>;\n}\n\n/**\n * Whether the app says its principal is settled.\n *\n * @param isPrincipalKnown - The app's getter, if any.\n * @returns `true` without a getter; `false` when it returns anything but\n * `true` or throws, so an unknown principal fails closed.\n */\nfunction principalKnown(isPrincipalKnown: (() => boolean) | undefined): boolean {\n if (!isPrincipalKnown) return true;\n try {\n return isPrincipalKnown() === true;\n } catch {\n return false;\n }\n}\n\n/**\n * The sessions whose responses a `QueryClient`'s in-memory cache may hold, as\n * the `auth` handles on it saw them.\n *\n * Kept per client, not per handle, because the cache is the client's: a handle\n * created after another one stopped persisting (the advice of the dev warning)\n * still sits on the cache that holds the ended session's responses, and used to\n * stamp them with the session signed in when it was created (security audit\n * run-12, hardening). The entry is dropped only once the client is reset: by\n * `clear({ resetClient: true })`, after `queryClient.clear()`, and when a\n * handle is created on a client whose caches are empty. `clear()` without the\n * reset marks it ended instead, because the client still holds the session's\n * responses, and it may be shared by a handle on another `key` (security audit\n * run-13, C1).\n */\ninterface ClientSessions {\n /** The first session seen installed, or `null` while none has been. */\n session: string | null;\n /** Whether a handle hydrated an anonymous snapshot into this client. */\n hydratedAnonymous: boolean;\n /**\n * Whether the cache may hold a session that is not the one installed now:\n * a second session, or nobody after one. Never reset while the entry lives.\n */\n ended: boolean;\n}\n\nconst clientSessions = new WeakMap<QueryClient, ClientSessions>();\n\n/**\n * Record the session installed now on a client's lineage.\n *\n * Every session installed while the cache lives counts, not only the one a\n * cache change happens to see: the api client replays a request that left\n * without a token with the bearer of a `login()` or boot `refresh()` still in\n * flight, so a response can belong to a session that was installed and ended\n * between two cache events, with nobody signed in at both (security audit\n * run-12, C1). The lineage ends when a second session or nobody follows the\n * first one, and when someone signs in on a client holding a hydrated\n * anonymous snapshot, which may be a previous account's (#590). Nobody\n * followed by a session is not an end: responses fetched while nobody was\n * signed in carry no account.\n *\n * @param lineage - The client's entry.\n * @param current - The key of the session installed now, or `null`.\n */\nfunction recordSession(lineage: ClientSessions, current: string | null): void {\n if (lineage.ended) return;\n if (current === null) {\n lineage.ended = lineage.session !== null;\n return;\n }\n if (lineage.hydratedAnonymous || (lineage.session !== null && lineage.session !== current)) {\n lineage.ended = true;\n return;\n }\n lineage.session = current;\n}\n\n/**\n * Whether the client holds no query and no mutation — nothing any session\n * could have left in it.\n *\n * @param queryClient - The client.\n * @returns `true` when both caches are empty.\n */\nfunction clientIsEmpty(queryClient: QueryClient): boolean {\n return (\n queryClient.getQueryCache().getAll().length === 0 &&\n queryClient.getMutationCache().getAll().length === 0\n );\n}\n\n/**\n * Owner stamp of a snapshot written while nobody was signed in.\n *\n * Not a fingerprint (`ownerFingerprint` only yields `p:` + 16 hex characters),\n * so it never matches a signed-in owner. It replaces the `null` stamp of 0.82.0\n * on purpose: a `null` row from 0.82.0 may hold the cache of a session that\n * expired before the write (#563), and nothing in it tells that row apart from\n * genuinely anonymous data, so `restore()` skips it — one cold start, once.\n */\nconst ANONYMOUS_SNAPSHOT_OWNER = \"p:anonymous\";\n\ninterface Snapshot {\n id: string;\n state: ReturnType<typeof dehydrate>;\n /**\n * Owner fingerprint, or {@link ANONYMOUS_SNAPSHOT_OWNER}, when `getPrincipal`\n * was set at write time; absent otherwise.\n */\n owner?: string | null;\n}\n\n/**\n * Fingerprint whoever `getPrincipal` says is signed in.\n *\n * @param getPrincipal - The app's principal getter.\n * @returns The fingerprint, or `null` for nobody, a non-identity value or a throw.\n */\nfunction currentOwner(getPrincipal: () => unknown): string | null {\n try {\n return ownerFingerprint(\"p\", getPrincipal());\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a TanStack `QueryClient` cache to IndexedDB (via\n * {@link createOfflineStore}) and restore it on boot — so a reload or a cold\n * offline start shows the last-known data instead of empty screens.\n *\n * Self-contained: uses `dehydrate`/`hydrate` from `@tanstack/react-query`\n * directly, so no `@tanstack/react-query-persist-client` dependency is needed.\n * Writes are throttled and subscribe to the query cache; call `unsubscribe` on\n * teardown. Dexie is a peer dependency of the offline store — install it.\n *\n * @param options - The client plus storage/throttle configuration.\n * @returns A handle with `restore`, `flush`, `clear` and `unsubscribe`.\n *\n * `clear()` is terminal for the handle (see {@link OfflineQueryPersistence.clear}):\n * a logout that only deleted the row used to be undone by the pending write or\n * by the next cache event, restoring the previous user's data on the next boot\n * (#440).\n *\n * @example\n * const persistence = persistQueryClientOffline({ queryClient });\n * await persistence.restore();\n *\n * async function logout(): Promise<void> {\n * await persistence.clear({ resetClient: true });\n * }\n */\nexport function persistQueryClientOffline(\n options: OfflineQueryPersistenceOptions,\n): OfflineQueryPersistence {\n const { queryClient, throttleMs = 1000, auth } = options;\n const appPrincipal = options.getPrincipal;\n const getPrincipal = appPrincipal ?? auth?.sessionKey;\n const isPrincipalKnown = auth\n ? (): boolean => {\n const session = auth.sessionKey();\n return (\n principalKnown(options.isPrincipalKnown) &&\n auth.isSessionKnown() &&\n (appPrincipal === undefined ||\n session === null ||\n currentOwner(appPrincipal) !== null)\n );\n }\n : options.isPrincipalKnown;\n const key = options.key ?? \"tanstack-query\";\n const databaseName = options.databaseName ?? \"TempestQueryCache\";\n const store = createOfflineStore<Snapshot, string>({\n databaseName,\n version: 1,\n tableName: \"cache\",\n indexes: \"&id\",\n keyPath: \"id\",\n });\n\n const slot = `${databaseName}\\u0000${key}`;\n let ended = false;\n let detached = false;\n let warnedEnded = false;\n let warnedRebound = false;\n let boundOwner: string | null = null;\n let hydratedAnonymous = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const inFlight = new Set<Promise<unknown>>();\n\n async function afterPendingClear(): Promise<void> {\n const pending = pendingClears.get(slot);\n if (pending) await pending;\n }\n\n function warnEnded(): void {\n if (warnedEnded || !isDevBuild()) return;\n warnedEnded = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: handle ended by clear(); \" +\n \"create a new one after login to keep persisting.\",\n );\n }\n\n function warnRebound(): void {\n if (warnedRebound || !isDevBuild()) return;\n warnedRebound = true;\n console.warn(\n \"[tempest-react-sdk] persistQueryClientOffline: an account signed in on a \" +\n \"handle holding another session's cache; its snapshot is deleted instead of \" +\n \"written. Call clear({ resetClient: true }), then create a new handle.\",\n );\n }\n\n if (auth && clientIsEmpty(queryClient)) clientSessions.delete(queryClient);\n\n function lineage(): ClientSessions {\n let entry = clientSessions.get(queryClient);\n if (!entry) {\n entry = { session: null, hydratedAnonymous: false, ended: false };\n clientSessions.set(queryClient, entry);\n }\n return entry;\n }\n\n /**\n * Record on the client's lineage the session `auth` holds now. Runs on\n * every auth state change the preset notifies, and on every cache change,\n * write and restore. A `sessionKey()` that throws records nothing.\n *\n * @returns Whether the client's cache may hold a session that has ended.\n */\n function observeSession(): boolean {\n if (!auth || ended) return false;\n const entry = lineage();\n try {\n recordSession(entry, auth.sessionKey());\n } catch {\n return entry.ended;\n }\n return entry.ended;\n }\n\n const unsubscribeSession = auth\n ? (sessionIdentityOf(auth.sessionKey)?.subscribe(observeSession) ?? null)\n : null;\n observeSession();\n\n /**\n * Write the snapshot, or delete the row when the session it holds is over.\n *\n * With `getPrincipal`, the handle is bound to the first signed-in owner it\n * sees on a cache change, a write or a restore. A write under any other owner — nobody, after a token\n * expired, or a second account signing in on the same tab — deletes the row\n * instead of stamping it: the in-memory cache still holds the bound owner's\n * responses, and stamping them with the new owner is what handed them to the\n * next account (#563). The same owner coming back writes again.\n *\n * A second account (not an expiry) means the app kept the handle across a\n * login, which stops persistence for that account in silence; a dev build\n * logs one `console.warn` saying so, as for a handle ended by `clear()`.\n * Someone signing in on a handle that hydrated an anonymous snapshot is\n * treated the same way: that row may hold an account's responses written\n * before its principal was known, so it is deleted, not re-stamped (#590).\n *\n * While `isPrincipalKnown` says the principal is not settled, nothing is\n * written and the write is re-armed for the next window (#590).\n *\n * With `auth`, a client whose lineage ended — a second session, or nobody\n * after one, was installed while its cache lived — deletes the row as\n * well, whatever the handle is bound to (security audit run-12, C1).\n */\n async function save(): Promise<void> {\n if (ended || (auth && detached)) return;\n await afterPendingClear();\n if (ended || (auth && detached)) return;\n if (getPrincipal && !principalKnown(isPrincipalKnown)) {\n schedule();\n return;\n }\n const snapshot: Snapshot = { id: key, state: dehydrate(queryClient) };\n let foreign = false;\n if (getPrincipal) {\n const owner = currentOwner(getPrincipal);\n foreign =\n observeSession() ||\n (boundOwner !== null && owner !== boundOwner) ||\n (hydratedAnonymous && owner !== null);\n if (foreign && owner !== null) warnRebound();\n boundOwner ??= owner;\n snapshot.owner = owner ?? ANONYMOUS_SNAPSHOT_OWNER;\n }\n const write = foreign ? store.delete(key) : store.put(snapshot);\n inFlight.add(write);\n try {\n await write;\n } finally {\n inFlight.delete(write);\n }\n }\n\n function cancelTimer(): void {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function schedule(): void {\n if (timer || ended) return;\n timer = setTimeout(() => {\n timer = null;\n void save();\n }, throttleMs);\n }\n\n /**\n * Schedule a write for cache activity, or detach once the handle has ended.\n *\n * With `getPrincipal`, an unbound handle is bound here, to whoever is\n * signed in when the cache changes — not only by a write or a restore. The\n * write is throttled, so the principal it reads can already be someone\n * else's: a session that expired inside the first window used to stamp the\n * previous user's responses `p:anonymous`, which the next logged-out boot\n * hydrated and the next account re-stamped as its own; an account that\n * signed in inside it got them stamped as its own directly (#580). Binding\n * at event time pins the owner of the data, so that write deletes instead.\n * An event while nobody is signed in leaves the handle unbound, which keeps\n * the cookie app whose principal arrives after `/me` persisting; so does\n * an event while `isPrincipalKnown` says the principal is not settled.\n * With `auth`, every event also records the session installed now on the\n * client's lineage, see `recordSession`.\n */\n function onCacheEvent(event: QueryCacheNotifyEvent): void {\n if (!ended) {\n observeSession();\n if (getPrincipal && principalKnown(isPrincipalKnown)) {\n boundOwner ??= currentOwner(getPrincipal);\n }\n schedule();\n return;\n }\n if (event.type !== \"added\" && event.type !== \"updated\") return;\n unsubscribeCache();\n warnEnded();\n }\n\n const unsubscribeCache = queryClient.getQueryCache().subscribe(onCacheEvent);\n\n /**\n * Wait for the writes in flight, delete the row and, with `resetClient`,\n * reset the client. Only that reset drops the client's lineage, after\n * `queryClient.clear()`: nothing of an ended session is left in memory to\n * stamp, so the next session starts a new one.\n *\n * @param resetClient - Whether to clear the in-memory `QueryClient`.\n */\n async function endSession(resetClient: boolean): Promise<void> {\n try {\n await Promise.allSettled([...inFlight]);\n await store.delete(key);\n } finally {\n if (resetClient) {\n queryClient.clear();\n if (auth) clientSessions.delete(queryClient);\n }\n }\n }\n\n return {\n async restore() {\n if (ended) {\n warnEnded();\n return;\n }\n await afterPendingClear();\n if (ended) return;\n const snapshot = await store.get(key);\n if (!snapshot || ended) return;\n if (getPrincipal) {\n if (!principalKnown(isPrincipalKnown) || observeSession()) return;\n const owner = currentOwner(getPrincipal);\n if (snapshot.owner !== (owner ?? ANONYMOUS_SNAPSHOT_OWNER)) return;\n boundOwner ??= owner;\n if (owner === null) {\n hydratedAnonymous = true;\n if (auth) lineage().hydratedAnonymous = true;\n }\n }\n hydrate(queryClient, snapshot.state);\n },\n async flush() {\n if (ended) {\n warnEnded();\n return;\n }\n cancelTimer();\n await save();\n },\n unsubscribe() {\n detached = true;\n cancelTimer();\n unsubscribeCache();\n unsubscribeSession?.();\n },\n async clear(clearOptions = {}) {\n ended = true;\n cancelTimer();\n unsubscribeSession?.();\n if (auth) lineage().ended = true;\n const previous = pendingClears.get(slot);\n const run = endSession(clearOptions.resetClient ?? false);\n const settled = Promise.allSettled([previous, run]);\n pendingClears.set(slot, settled);\n void settled.then(() => {\n if (pendingClears.get(slot) === settled) pendingClears.delete(slot);\n });\n await run;\n },\n };\n}\n"],"mappings":"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"}
|