@oxy.so/services 8.4.0 → 9.1.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/lib/commonjs/ui/components/BenefitList.js +1 -2
- package/lib/commonjs/ui/components/BenefitList.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +2 -2
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +4 -0
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/accountQueryPersistence.js +110 -80
- package/lib/commonjs/ui/hooks/accountQueryPersistence.js.map +1 -1
- package/lib/commonjs/ui/hooks/persistedQueryCache.js +101 -0
- package/lib/commonjs/ui/hooks/persistedQueryCache.js.map +1 -0
- package/lib/commonjs/ui/hooks/queryClient.js +43 -80
- package/lib/commonjs/ui/hooks/queryClient.js.map +1 -1
- package/lib/module/ui/components/BenefitList.js +1 -2
- package/lib/module/ui/components/BenefitList.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +2 -2
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +4 -0
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/accountQueryPersistence.js +109 -80
- package/lib/module/ui/hooks/accountQueryPersistence.js.map +1 -1
- package/lib/module/ui/hooks/persistedQueryCache.js +94 -0
- package/lib/module/ui/hooks/persistedQueryCache.js.map +1 -0
- package/lib/module/ui/hooks/queryClient.js +44 -81
- package/lib/module/ui/hooks/queryClient.js.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts +31 -13
- package/lib/typescript/commonjs/ui/hooks/accountQueryPersistence.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/persistedQueryCache.d.ts +44 -0
- package/lib/typescript/commonjs/ui/hooks/persistedQueryCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts +15 -20
- package/lib/typescript/commonjs/ui/hooks/queryClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts +31 -13
- package/lib/typescript/module/ui/hooks/accountQueryPersistence.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/persistedQueryCache.d.ts +44 -0
- package/lib/typescript/module/ui/hooks/persistedQueryCache.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/queryClient.d.ts +15 -20
- package/lib/typescript/module/ui/hooks/queryClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
- package/package.json +4 -4
- package/src/ui/components/BenefitList.tsx +1 -1
- package/src/ui/components/OxyProvider.tsx +2 -2
- package/src/ui/context/OxyContext.tsx +4 -0
- package/src/ui/hooks/accountQueryPersistence.ts +138 -91
- package/src/ui/hooks/persistedQueryCache.ts +126 -0
- package/src/ui/hooks/queryClient.ts +55 -98
|
@@ -5,34 +5,28 @@
|
|
|
5
5
|
* - TanStack Query with `networkMode: 'offlineFirst'` for queries and mutations
|
|
6
6
|
* so cached data is served immediately and mutations are queued (paused) while
|
|
7
7
|
* the browser/device reports offline.
|
|
8
|
-
* - `
|
|
9
|
-
*
|
|
8
|
+
* - `attachQueryPersistence(...)` so that query results AND paused mutations
|
|
9
|
+
* survive a cold restart (kill-and-relaunch); see `persistedQueryCache.ts`.
|
|
10
10
|
* - `onlineManager` resume hook so paused mutations replay the moment the
|
|
11
11
|
* network is reported back, even if the host app swapped in a custom
|
|
12
12
|
* onlineManager implementation.
|
|
13
13
|
*
|
|
14
|
-
* Storage layer:
|
|
15
|
-
*
|
|
16
|
-
* - Web -> localStorage via `createSyncStoragePersister` (wrapped in the
|
|
17
|
-
* async persister API for a single call site).
|
|
18
|
-
* - Both adapters are content-shape compatible with our `StorageInterface`.
|
|
14
|
+
* Storage layer: the platform `StorageInterface` (AsyncStorage on native,
|
|
15
|
+
* localStorage on web).
|
|
19
16
|
*
|
|
20
17
|
* Whitelist policy:
|
|
21
18
|
* - Persist every account/user/session/privacy query and every queued mutation.
|
|
22
19
|
* - DO NOT persist large list queries (e.g. activity feeds) — they go stale
|
|
23
20
|
* fast and would balloon storage. Add new keys to `PERSISTED_QUERY_PREFIXES`
|
|
24
|
-
* when introducing reads that should survive restart.
|
|
21
|
+
* (`persistedQueryCache.ts`) when introducing reads that should survive restart.
|
|
25
22
|
*/
|
|
26
23
|
|
|
27
|
-
import { QueryClient,
|
|
28
|
-
import {
|
|
29
|
-
persistQueryClient,
|
|
30
|
-
type PersistedClient,
|
|
31
|
-
} from '@tanstack/react-query-persist-client';
|
|
32
|
-
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
|
|
24
|
+
import { QueryClient, onlineManager, type Mutation, type Query } from '@tanstack/react-query';
|
|
25
|
+
import type { PersistedClient } from '@tanstack/react-query-persist-client';
|
|
33
26
|
import { isDev } from '@oxy.so/core';
|
|
34
27
|
import type { StorageInterface } from '../utils/storageHelpers';
|
|
35
|
-
import {
|
|
28
|
+
import { accountQueryOwnership, type AccountQueriesConfig } from './accountQueryPersistence';
|
|
29
|
+
import { PERSISTED_QUERY_PREFIXES, hydrateSnapshot, subscribeSnapshots } from './persistedQueryCache';
|
|
36
30
|
|
|
37
31
|
const QUERY_CACHE_KEY = 'oxy_query_cache_v3';
|
|
38
32
|
const QUERY_CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
@@ -55,59 +49,18 @@ export function shouldRetryQuery(failureCount: number, error: unknown): boolean
|
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
/**
|
|
58
|
-
*
|
|
59
|
-
* is
|
|
60
|
-
*
|
|
61
|
-
* Mutations are persisted independently (always) so the offline write queue
|
|
62
|
-
* works regardless of the read whitelist.
|
|
63
|
-
*/
|
|
64
|
-
const PERSISTED_QUERY_PREFIXES: ReadonlyArray<string> = [
|
|
65
|
-
'accounts',
|
|
66
|
-
'users',
|
|
67
|
-
'sessions',
|
|
68
|
-
'devices',
|
|
69
|
-
'privacy',
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Adapt our `StorageInterface` (which always returns `null` for missing keys)
|
|
74
|
-
* to TanStack's `AsyncStorage` shape. The two are structurally identical
|
|
75
|
-
* apart from naming; this also gives us a single place to add error
|
|
76
|
-
* suppression if a host platform's storage throws.
|
|
77
|
-
*/
|
|
78
|
-
function adaptStorage(storage: StorageInterface): {
|
|
79
|
-
getItem: (key: string) => Promise<string | null>;
|
|
80
|
-
setItem: (key: string, value: string) => Promise<void>;
|
|
81
|
-
removeItem: (key: string) => Promise<void>;
|
|
82
|
-
} {
|
|
83
|
-
return {
|
|
84
|
-
getItem: (key) => storage.getItem(key),
|
|
85
|
-
setItem: (key, value) => storage.setItem(key, value),
|
|
86
|
-
removeItem: (key) => storage.removeItem(key),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Decide whether a given query should be written to persistent storage.
|
|
92
|
-
*
|
|
93
|
-
* Two gates:
|
|
94
|
-
* 1. The query must be in our prefix whitelist.
|
|
95
|
-
* 2. The query must have completed successfully at least once — there's no
|
|
96
|
-
* point persisting a `pending`/`error` state, and persisting `error` would
|
|
97
|
-
* leak failure objects across restarts.
|
|
52
|
+
* Whether a query belongs in the account-agnostic cache: successful (a
|
|
53
|
+
* `pending`/`error` state is not worth keeping, and an error would leak
|
|
54
|
+
* failure objects across restarts) and under one of the SDK's own prefixes.
|
|
98
55
|
*/
|
|
99
|
-
function
|
|
56
|
+
function isSharedQuery(query: Query): boolean {
|
|
100
57
|
if (query.state.status !== 'success') {
|
|
101
58
|
return false;
|
|
102
59
|
}
|
|
103
60
|
const head = query.queryKey[0];
|
|
104
|
-
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
return PERSISTED_QUERY_PREFIXES.includes(head);
|
|
61
|
+
return typeof head === 'string' && PERSISTED_QUERY_PREFIXES.includes(head);
|
|
108
62
|
}
|
|
109
63
|
|
|
110
|
-
|
|
111
64
|
/**
|
|
112
65
|
* Create a QueryClient with offline-first defaults.
|
|
113
66
|
*
|
|
@@ -174,10 +127,13 @@ export interface AttachPersistenceResult {
|
|
|
174
127
|
}
|
|
175
128
|
|
|
176
129
|
/**
|
|
177
|
-
*
|
|
130
|
+
* Restore, then keep writing, the account-agnostic cache.
|
|
178
131
|
*
|
|
179
|
-
*
|
|
180
|
-
* `
|
|
132
|
+
* Queries written by another build are dropped; queued mutations are kept
|
|
133
|
+
* (`persistedQueryCache.ts`). Whatever the app declared account-scoped
|
|
134
|
+
* (`accountQueries`) is left to `createAccountQueryPersistence`, even under
|
|
135
|
+
* one of the SDK's prefixes, so private rows never land in the shared blob and
|
|
136
|
+
* a paused mutation cannot replay twice or under another account.
|
|
181
137
|
*
|
|
182
138
|
* Safe to no-op if `storage` is null/undefined (e.g. server-side render
|
|
183
139
|
* with no host storage).
|
|
@@ -185,12 +141,7 @@ export interface AttachPersistenceResult {
|
|
|
185
141
|
export const attachQueryPersistence = (
|
|
186
142
|
queryClient: QueryClient,
|
|
187
143
|
storage: StorageInterface | null | undefined,
|
|
188
|
-
|
|
189
|
-
* Mutation keys the app declared account-scoped (`accountQueries`). Those are
|
|
190
|
-
* persisted per account by `createAccountQueryPersistence`, never here, so a
|
|
191
|
-
* paused mutation cannot replay twice or under another account.
|
|
192
|
-
*/
|
|
193
|
-
accountMutationKeys: readonly QueryKey[] = [],
|
|
144
|
+
accountQueries?: AccountQueriesConfig,
|
|
194
145
|
): AttachPersistenceResult => {
|
|
195
146
|
if (!storage) {
|
|
196
147
|
return {
|
|
@@ -199,38 +150,44 @@ export const attachQueryPersistence = (
|
|
|
199
150
|
};
|
|
200
151
|
}
|
|
201
152
|
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const accountScoped = new Set(accountMutationKeys.map((key) => hashKey(key)));
|
|
211
|
-
const shouldDehydrateMutation = (mutation: Mutation): boolean => {
|
|
212
|
-
const key = mutation.options.mutationKey;
|
|
213
|
-
return key === undefined || !accountScoped.has(hashKey(key));
|
|
153
|
+
const account = accountQueries ? accountQueryOwnership(accountQueries) : null;
|
|
154
|
+
const filters = {
|
|
155
|
+
shouldDehydrateQuery: (query: Query) =>
|
|
156
|
+
isSharedQuery(query) && !(account?.ownsQuery(query) ?? false),
|
|
157
|
+
// Every other mutation, whatever its status: paused ones are exactly the
|
|
158
|
+
// ones that must survive a restart to replay when online.
|
|
159
|
+
shouldDehydrateMutation: (mutation: Mutation) =>
|
|
160
|
+
!(account?.ownsMutation(mutation) ?? false),
|
|
214
161
|
};
|
|
215
162
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
shouldDehydrateMutation,
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
restored.catch((error) => {
|
|
228
|
-
if (isDev()) {
|
|
229
|
-
console.warn('[QueryClient] Failed to restore persisted cache', error);
|
|
163
|
+
let stopped = false;
|
|
164
|
+
let writer: ReturnType<typeof subscribeSnapshots> | null = null;
|
|
165
|
+
const restored = (async () => {
|
|
166
|
+
const raw = await storage.getItem(QUERY_CACHE_KEY);
|
|
167
|
+
if (stopped) return;
|
|
168
|
+
if (!hydrateSnapshot(queryClient, raw, QUERY_CACHE_MAX_AGE) && raw) {
|
|
169
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
230
170
|
}
|
|
231
|
-
})
|
|
171
|
+
})()
|
|
172
|
+
.catch((error) => {
|
|
173
|
+
if (isDev()) {
|
|
174
|
+
console.warn('[QueryClient] Failed to restore persisted cache', error);
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
.then(() => {
|
|
178
|
+
if (stopped) return;
|
|
179
|
+
writer = subscribeSnapshots(queryClient, filters, QUERY_PERSIST_THROTTLE_MS, (serialized) => {
|
|
180
|
+
void Promise.resolve(storage.setItem(QUERY_CACHE_KEY, serialized)).catch(() => undefined);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
232
183
|
|
|
233
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
restored,
|
|
186
|
+
unsubscribe: () => {
|
|
187
|
+
stopped = true;
|
|
188
|
+
writer?.unsubscribe();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
234
191
|
};
|
|
235
192
|
|
|
236
193
|
/**
|