@agent-native/core 0.207.1 → 0.207.2-nightly-20261013011333
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/dist/agent/production-agent.js +2 -0
- package/dist/cli/design-connect.d.ts +6 -0
- package/dist/cli/design-connect.js +880 -9
- package/dist/client/action-query-cache.d.ts +3 -1
- package/dist/client/action-query-cache.js +628 -161
- package/dist/client/action-query-cache.session-test-fixture.d.ts +49 -0
- package/dist/client/action-query-cache.session-test-fixture.js +151 -0
- package/dist/client/action-query-cache.test-fixtures.d.ts +2 -0
- package/dist/client/action-query-cache.test-fixtures.js +56 -0
- package/dist/client/motion-text.css +3 -0
- package/dist/client/motion-text.d.ts +38 -0
- package/dist/client/motion-text.js +551 -0
- package/dist/client/org/hooks.d.ts +3 -1
- package/dist/client/org/hooks.js +8 -2
- package/dist/client/route-chunk-recovery.d.ts +2 -0
- package/dist/client/route-chunk-recovery.js +11 -0
- package/dist/client/use-action.d.ts +10 -2
- package/dist/client/use-action.js +81 -12
- package/dist/client/use-settings-shortcut.js +8 -3
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/extensions/routes.js +7 -3
- package/dist/extensions/web-content.js +17 -2
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/mcp/build-server.js +5 -0
- package/dist/observability/routes.d.ts +4 -4
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +5 -5
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/visual-edit-bridge-capability.d.ts +26 -0
- package/dist/server/visual-edit-bridge-capability.js +233 -0
- package/dist/shared/motion-easing.d.ts +6 -0
- package/dist/shared/motion-easing.js +109 -0
- package/dist/shared/motion-source-node-id.d.ts +3 -0
- package/dist/shared/motion-source-node-id.js +12 -0
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
- package/dist/triggers/actions/manage-automation.d.ts +5 -5
- package/dist/vite/client.js +2 -0
- package/dist/vite/motion-source-provenance-plugin.d.ts +3 -0
- package/dist/vite/motion-source-provenance-plugin.js +339 -0
- package/package.json +14 -5
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
|
@@ -2,11 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { persistQueryClient, } from "@tanstack/query-persist-client-core";
|
|
3
3
|
import { IsRestoringProvider, useQueryClient, } from "@tanstack/react-query";
|
|
4
4
|
import { clear as clearStore, createStore, del as deleteEntry, get as getEntry, keys as listKeys, set as setEntry, } from "idb-keyval";
|
|
5
|
-
import { useEffect, useState } from "react";
|
|
5
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { actionErrorStatus } from "./action-failure-circuit.js";
|
|
7
7
|
import { clientBuildId } from "./build-compatibility.js";
|
|
8
|
-
import {
|
|
9
|
-
import { useSession } from "./use-session.js";
|
|
8
|
+
import { getAccessDeniedActionQueryBinding, isBrowserPersistableActionQueryForBinding, markRestoredBrowserPersistableActionQuery, registerActionQueryCacheRevoker, revokeBrowserPersistableActionQuery, setActionQueryCacheBindingToken, } from "./use-action.js";
|
|
9
|
+
import { isSessionFromFirstRead, useSession } from "./use-session.js";
|
|
10
10
|
// Bump when a cached result's shape or meaning changes. A record stored under
|
|
11
11
|
// another buster is discarded on restore instead of being hydrated.
|
|
12
12
|
const ACTION_QUERY_CACHE_BUSTER = "action-query-cache-v1";
|
|
@@ -17,7 +17,18 @@ const WRITE_DELAY_MS = 1_000;
|
|
|
17
17
|
// Restoring holds every query, so a store that never answers must not hold
|
|
18
18
|
// the app.
|
|
19
19
|
const RESTORE_TIMEOUT_MS = 2_000;
|
|
20
|
+
const INITIAL_CLEAR_TIMEOUT_MS = 2_000;
|
|
21
|
+
const BIND_WAIT_BUDGET_MS = 2_000;
|
|
20
22
|
const ACTION_QUERY_KEY = ["action"];
|
|
23
|
+
function isAccessDeniedActionQuery(query) {
|
|
24
|
+
if (query.queryKey[0] !== "action")
|
|
25
|
+
return false;
|
|
26
|
+
const status = actionErrorStatus(query.state.error);
|
|
27
|
+
return status === 401 || status === 403 || status === 404;
|
|
28
|
+
}
|
|
29
|
+
function clearAccessDeniedActionQueryData(query) {
|
|
30
|
+
query.setState({ data: undefined, dataUpdatedAt: 0 });
|
|
31
|
+
}
|
|
21
32
|
const indexedDbStore = createStore("agent-native-action-query-cache", "queries");
|
|
22
33
|
const indexedDbStorage = {
|
|
23
34
|
get: (key) => getEntry(key, indexedDbStore),
|
|
@@ -27,6 +38,207 @@ const indexedDbStorage = {
|
|
|
27
38
|
clear: () => clearStore(indexedDbStore),
|
|
28
39
|
};
|
|
29
40
|
let storage = indexedDbStorage;
|
|
41
|
+
let storageClearGeneration = 0;
|
|
42
|
+
let storageClearInFlight;
|
|
43
|
+
let storageClearRequired = false;
|
|
44
|
+
let bindAttemptGeneration = 0;
|
|
45
|
+
const inFlightStorageWrites = new Set();
|
|
46
|
+
const inFlightStorageDeletes = new Set();
|
|
47
|
+
const failedStorageDeleteScopes = new Set();
|
|
48
|
+
const CACHE_EPOCH_KEY = "agent-native.action-query-cache.epoch";
|
|
49
|
+
const CLEAR_INTENT_PREFIX = "agent-native.action-query-cache.clear-required:";
|
|
50
|
+
let cacheSafetyUnavailable = false;
|
|
51
|
+
function newCacheToken() {
|
|
52
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
53
|
+
}
|
|
54
|
+
function readCacheSafetyState(probeWrite = false) {
|
|
55
|
+
if (cacheSafetyUnavailable || typeof window === "undefined")
|
|
56
|
+
return undefined;
|
|
57
|
+
try {
|
|
58
|
+
const local = window.localStorage;
|
|
59
|
+
let epoch = local.getItem(CACHE_EPOCH_KEY);
|
|
60
|
+
if (!epoch) {
|
|
61
|
+
epoch = newCacheToken();
|
|
62
|
+
local.setItem(CACHE_EPOCH_KEY, epoch);
|
|
63
|
+
if (local.getItem(CACHE_EPOCH_KEY) !== epoch) {
|
|
64
|
+
cacheSafetyUnavailable = true;
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (probeWrite) {
|
|
69
|
+
const probeKey = `${CACHE_EPOCH_KEY}.probe:${newCacheToken()}`;
|
|
70
|
+
const probeValue = newCacheToken();
|
|
71
|
+
try {
|
|
72
|
+
local.setItem(probeKey, probeValue);
|
|
73
|
+
if (local.getItem(probeKey) !== probeValue) {
|
|
74
|
+
throw new Error("Unable to verify cache epoch storage access");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
local.removeItem(probeKey);
|
|
79
|
+
}
|
|
80
|
+
if (local.getItem(CACHE_EPOCH_KEY) !== epoch) {
|
|
81
|
+
cacheSafetyUnavailable = true;
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const pendingClearIntents = [];
|
|
86
|
+
for (let index = 0; index < local.length; index += 1) {
|
|
87
|
+
const key = local.key(index);
|
|
88
|
+
if (key?.startsWith(CLEAR_INTENT_PREFIX)) {
|
|
89
|
+
pendingClearIntents.push({ key, value: local.getItem(key) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { epoch, pendingClearIntents };
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
cacheSafetyUnavailable = true;
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function beginDurableClearIntent() {
|
|
100
|
+
const local = (() => {
|
|
101
|
+
try {
|
|
102
|
+
return typeof window === "undefined" ? undefined : window.localStorage;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
cacheSafetyUnavailable = true;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
if (!local || cacheSafetyUnavailable)
|
|
110
|
+
return undefined;
|
|
111
|
+
const value = newCacheToken();
|
|
112
|
+
const key = `${CLEAR_INTENT_PREFIX}${value}`;
|
|
113
|
+
let intent;
|
|
114
|
+
let epochAdvanced = false;
|
|
115
|
+
try {
|
|
116
|
+
local.setItem(key, value);
|
|
117
|
+
if (local.getItem(key) !== value)
|
|
118
|
+
throw new Error("Unable to persist cache clear intent");
|
|
119
|
+
intent = { key, value };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.warn("Unable to persist cache clear intent; trying epoch invalidation", error);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const epoch = newCacheToken();
|
|
126
|
+
local.setItem(CACHE_EPOCH_KEY, epoch);
|
|
127
|
+
if (local.getItem(CACHE_EPOCH_KEY) !== epoch)
|
|
128
|
+
throw new Error("Unable to persist cache epoch");
|
|
129
|
+
epochAdvanced = true;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.warn("Unable to persist cache epoch; relying on clear intent", error);
|
|
133
|
+
}
|
|
134
|
+
if (intent || epochAdvanced)
|
|
135
|
+
return intent;
|
|
136
|
+
cacheSafetyUnavailable = true;
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
function removeCompletedClearIntents(intents) {
|
|
140
|
+
try {
|
|
141
|
+
if (typeof window === "undefined")
|
|
142
|
+
return;
|
|
143
|
+
const local = window.localStorage;
|
|
144
|
+
for (const intent of intents) {
|
|
145
|
+
if (intent.key.startsWith(CLEAR_INTENT_PREFIX) &&
|
|
146
|
+
local.getItem(intent.key) === intent.value) {
|
|
147
|
+
local.removeItem(intent.key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.warn("Unable to remove completed cache clear intent; later binds will retry", error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function clearIntents() {
|
|
156
|
+
return readCacheSafetyState()?.pendingClearIntents ?? [];
|
|
157
|
+
}
|
|
158
|
+
function matchesCacheEpoch(epoch) {
|
|
159
|
+
const current = readCacheSafetyState();
|
|
160
|
+
return (current !== undefined &&
|
|
161
|
+
current.epoch === epoch &&
|
|
162
|
+
current.pendingClearIntents.length === 0);
|
|
163
|
+
}
|
|
164
|
+
function trackStorageWrite(write) {
|
|
165
|
+
inFlightStorageWrites.add(write);
|
|
166
|
+
void write.then(() => inFlightStorageWrites.delete(write), () => inFlightStorageWrites.delete(write));
|
|
167
|
+
}
|
|
168
|
+
function trackStorageDelete(deletion) {
|
|
169
|
+
inFlightStorageDeletes.add(deletion);
|
|
170
|
+
void deletion.then(() => inFlightStorageDeletes.delete(deletion), () => inFlightStorageDeletes.delete(deletion));
|
|
171
|
+
return deletion;
|
|
172
|
+
}
|
|
173
|
+
function beginStorageClear() {
|
|
174
|
+
if (storageClearInFlight)
|
|
175
|
+
return storageClearInFlight;
|
|
176
|
+
storageClearRequired = true;
|
|
177
|
+
beginDurableClearIntent();
|
|
178
|
+
const intentsToClear = clearIntents();
|
|
179
|
+
const generation = ++storageClearGeneration;
|
|
180
|
+
let clear;
|
|
181
|
+
clear = (async () => {
|
|
182
|
+
await waitForStorageOperations();
|
|
183
|
+
await storage.clear();
|
|
184
|
+
failedStorageDeleteScopes.clear();
|
|
185
|
+
removeCompletedClearIntents(intentsToClear);
|
|
186
|
+
storageClearRequired = clearIntents().length > 0;
|
|
187
|
+
})().finally(() => {
|
|
188
|
+
if (storageClearInFlight === clear &&
|
|
189
|
+
storageClearGeneration === generation) {
|
|
190
|
+
storageClearInFlight = undefined;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
storageClearInFlight = clear;
|
|
194
|
+
return clear;
|
|
195
|
+
}
|
|
196
|
+
async function waitForStorageClear() {
|
|
197
|
+
while (storageClearInFlight) {
|
|
198
|
+
const clear = storageClearInFlight;
|
|
199
|
+
await clear;
|
|
200
|
+
}
|
|
201
|
+
if (storageClearRequired) {
|
|
202
|
+
await beginStorageClear();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function waitForStorageClearWithinBound(timeoutMs = INITIAL_CLEAR_TIMEOUT_MS) {
|
|
206
|
+
if (timeoutMs <= 0) {
|
|
207
|
+
console.warn("Cached action results could not be cleared within the binding deadline");
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
let timer;
|
|
211
|
+
const timedOut = new Promise((resolve) => {
|
|
212
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
return await Promise.race([
|
|
216
|
+
waitForStorageClear().then(() => true),
|
|
217
|
+
timedOut,
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function releaseWithoutCache(client, attempt) {
|
|
225
|
+
closeBinding();
|
|
226
|
+
client.removeQueries({ queryKey: ACTION_QUERY_KEY });
|
|
227
|
+
registerActionQueryCacheRevoker(client, () => {
|
|
228
|
+
if (attempt !== bindAttemptGeneration || binding !== undefined) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
void clearActionQueryCache().catch((error) => console.warn("Unable to clear cached action results", error));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function waitForStorageOperations() {
|
|
235
|
+
while (inFlightStorageWrites.size > 0 || inFlightStorageDeletes.size > 0) {
|
|
236
|
+
await Promise.allSettled([
|
|
237
|
+
...inFlightStorageWrites,
|
|
238
|
+
...inFlightStorageDeletes,
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
30
242
|
/** @internal exported for tests */
|
|
31
243
|
export function setActionQueryCacheStorage(next) {
|
|
32
244
|
storage = next ?? indexedDbStorage;
|
|
@@ -60,46 +272,67 @@ function withinMaxAge(stored) {
|
|
|
60
272
|
};
|
|
61
273
|
}
|
|
62
274
|
// ponytail: one record holds every persisted result for the scope, with no size cap; split per query or cap bytes if restore shows up in traces.
|
|
63
|
-
function scopedPersister(scope) {
|
|
275
|
+
function scopedPersister(scope, cacheEpoch) {
|
|
276
|
+
const createdDuringClearGeneration = storageClearGeneration;
|
|
64
277
|
let open = true;
|
|
278
|
+
let persistBlocked = false;
|
|
65
279
|
let latest;
|
|
66
280
|
let timer;
|
|
67
281
|
let restored = new Map();
|
|
68
282
|
let restoredSources = new Map();
|
|
69
283
|
let restoreAccepted = false;
|
|
284
|
+
let restoreBlocked = false;
|
|
70
285
|
let restoreRevision = 0;
|
|
286
|
+
const writesInFlight = new Set();
|
|
71
287
|
const clearRestoredState = () => {
|
|
72
288
|
restoreAccepted = false;
|
|
73
289
|
restored = new Map();
|
|
74
290
|
restoredSources = new Map();
|
|
75
291
|
};
|
|
292
|
+
const canUseCache = () => {
|
|
293
|
+
const safe = matchesCacheEpoch(cacheEpoch);
|
|
294
|
+
if (!safe) {
|
|
295
|
+
persistBlocked = true;
|
|
296
|
+
restoreBlocked = true;
|
|
297
|
+
clearRestoredState();
|
|
298
|
+
latest = undefined;
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
timer = undefined;
|
|
301
|
+
}
|
|
302
|
+
return safe;
|
|
303
|
+
};
|
|
76
304
|
const flush = () => {
|
|
77
305
|
timer = undefined;
|
|
78
306
|
const next = latest;
|
|
79
307
|
latest = undefined;
|
|
80
|
-
if (!open || !next)
|
|
308
|
+
if (!open || persistBlocked || !next || !canUseCache())
|
|
81
309
|
return;
|
|
82
|
-
storage
|
|
310
|
+
const write = storage
|
|
83
311
|
.set(scope, next)
|
|
84
312
|
.catch((error) => console.warn("Unable to save cached action results", error));
|
|
313
|
+
writesInFlight.add(write);
|
|
314
|
+
trackStorageWrite(write);
|
|
315
|
+
void write.then(() => writesInFlight.delete(write), () => writesInFlight.delete(write));
|
|
85
316
|
};
|
|
86
317
|
return {
|
|
87
318
|
persistClient(client) {
|
|
88
|
-
if (!open)
|
|
319
|
+
if (!open || persistBlocked || !canUseCache())
|
|
89
320
|
return;
|
|
90
321
|
latest = client;
|
|
91
322
|
timer ??= setTimeout(flush, WRITE_DELAY_MS);
|
|
92
323
|
},
|
|
93
324
|
async restoreClient() {
|
|
94
325
|
clearRestoredState();
|
|
95
|
-
if (!open)
|
|
326
|
+
if (!open || restoreBlocked || !canUseCache())
|
|
96
327
|
return undefined;
|
|
97
328
|
const revision = restoreRevision;
|
|
98
329
|
const stored = await storage.get(scope);
|
|
99
330
|
// A revoked record can still be in an outstanding storage read. Do not
|
|
100
331
|
// return it to the persister after discard, even if the read was started
|
|
101
332
|
// before the matching access-denied response.
|
|
102
|
-
const record = open && revision === restoreRevision
|
|
333
|
+
const record = open && !restoreBlocked && revision === restoreRevision && canUseCache()
|
|
334
|
+
? withinMaxAge(stored)
|
|
335
|
+
: undefined;
|
|
103
336
|
restored = new Map(record?.clientState.queries.map((query) => [
|
|
104
337
|
query.queryHash,
|
|
105
338
|
query.state.dataUpdatedAt,
|
|
@@ -114,7 +347,10 @@ function scopedPersister(scope) {
|
|
|
114
347
|
},
|
|
115
348
|
]));
|
|
116
349
|
restoreAccepted =
|
|
117
|
-
open &&
|
|
350
|
+
open &&
|
|
351
|
+
!restoreBlocked &&
|
|
352
|
+
revision === restoreRevision &&
|
|
353
|
+
record !== undefined;
|
|
118
354
|
return record;
|
|
119
355
|
},
|
|
120
356
|
restoredStates() {
|
|
@@ -124,14 +360,24 @@ function scopedPersister(scope) {
|
|
|
124
360
|
return restoredSources;
|
|
125
361
|
},
|
|
126
362
|
restoreWasAccepted() {
|
|
127
|
-
return open && restoreAccepted;
|
|
363
|
+
return open && !restoreBlocked && restoreAccepted;
|
|
364
|
+
},
|
|
365
|
+
releaseRestorePayload() {
|
|
366
|
+
restoreAccepted = false;
|
|
367
|
+
restoredSources = new Map();
|
|
128
368
|
},
|
|
129
369
|
removeClient() {
|
|
370
|
+
restoreBlocked = true;
|
|
130
371
|
restoreRevision += 1;
|
|
131
372
|
clearRestoredState();
|
|
132
|
-
|
|
373
|
+
if (createdDuringClearGeneration !== storageClearGeneration) {
|
|
374
|
+
return Promise.resolve();
|
|
375
|
+
}
|
|
376
|
+
return trackStorageDelete(storage.del(scope));
|
|
133
377
|
},
|
|
134
378
|
discard() {
|
|
379
|
+
persistBlocked = true;
|
|
380
|
+
restoreBlocked = true;
|
|
135
381
|
restoreRevision += 1;
|
|
136
382
|
clearRestoredState();
|
|
137
383
|
// A debounced write still holds the client as it was before the revoke,
|
|
@@ -139,12 +385,31 @@ function scopedPersister(scope) {
|
|
|
139
385
|
latest = undefined;
|
|
140
386
|
clearTimeout(timer);
|
|
141
387
|
timer = undefined;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
388
|
+
if (createdDuringClearGeneration !== storageClearGeneration)
|
|
389
|
+
return;
|
|
390
|
+
const currentSafety = readCacheSafetyState();
|
|
391
|
+
if (currentSafety && currentSafety.epoch !== cacheEpoch)
|
|
392
|
+
return;
|
|
393
|
+
storageClearRequired = true;
|
|
394
|
+
beginDurableClearIntent();
|
|
395
|
+
const deletion = (async () => {
|
|
396
|
+
await Promise.all([...writesInFlight]);
|
|
397
|
+
await storage.del(scope);
|
|
398
|
+
})();
|
|
399
|
+
trackRevokedScopeDelete(scope, deletion);
|
|
400
|
+
},
|
|
401
|
+
blockRestoreAndPersistence() {
|
|
402
|
+
persistBlocked = true;
|
|
403
|
+
restoreBlocked = true;
|
|
404
|
+
restoreRevision += 1;
|
|
405
|
+
clearRestoredState();
|
|
406
|
+
latest = undefined;
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
timer = undefined;
|
|
145
409
|
},
|
|
146
410
|
close() {
|
|
147
411
|
open = false;
|
|
412
|
+
restoreBlocked = true;
|
|
148
413
|
restoreRevision += 1;
|
|
149
414
|
clearRestoredState();
|
|
150
415
|
latest = undefined;
|
|
@@ -155,24 +420,74 @@ function scopedPersister(scope) {
|
|
|
155
420
|
}
|
|
156
421
|
let binding;
|
|
157
422
|
let bindingGeneration = 0;
|
|
423
|
+
const bindingScopes = new WeakMap();
|
|
424
|
+
function trackRevokedScopeDelete(scope, deletion) {
|
|
425
|
+
const trackedDeletion = deletion.then(() => {
|
|
426
|
+
failedStorageDeleteScopes.delete(scope);
|
|
427
|
+
}, (error) => {
|
|
428
|
+
failedStorageDeleteScopes.add(scope);
|
|
429
|
+
storageClearRequired = true;
|
|
430
|
+
beginDurableClearIntent();
|
|
431
|
+
throw error;
|
|
432
|
+
});
|
|
433
|
+
trackStorageDelete(trackedDeletion);
|
|
434
|
+
void trackedDeletion.catch((error) => console.warn("Unable to delete cached action results", error));
|
|
435
|
+
return trackedDeletion;
|
|
436
|
+
}
|
|
158
437
|
/** Stops persisting and drops this document's action results. Storage is
|
|
159
438
|
* untouched. */
|
|
160
439
|
function closeBinding() {
|
|
161
440
|
bindingGeneration += 1;
|
|
162
|
-
|
|
163
|
-
return;
|
|
164
|
-
binding.persister.close();
|
|
165
|
-
binding.unsubscribe();
|
|
166
|
-
binding.client.removeQueries({ queryKey: ACTION_QUERY_KEY });
|
|
441
|
+
const closing = binding;
|
|
167
442
|
binding = undefined;
|
|
443
|
+
setActionQueryCacheBindingToken(undefined);
|
|
444
|
+
if (!closing)
|
|
445
|
+
return;
|
|
446
|
+
closing.persister.close();
|
|
447
|
+
closing.unsubscribe();
|
|
448
|
+
// Keep access-denied errors as evidence for the next bind. A cached success
|
|
449
|
+
// for the same key must not be restored after a 401/403/404 was observed.
|
|
450
|
+
for (const query of closing.client
|
|
451
|
+
.getQueryCache()
|
|
452
|
+
.findAll({ queryKey: ACTION_QUERY_KEY })) {
|
|
453
|
+
if (isAccessDeniedActionQuery(query)) {
|
|
454
|
+
clearAccessDeniedActionQueryData(query);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
closing.client.removeQueries({ queryKey: query.queryKey, exact: true });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
168
460
|
}
|
|
169
|
-
async function
|
|
461
|
+
async function waitForCleanup(cleanup, timeoutMessage = "Cached action results could not be prepared in time; loading without them.", timeoutMs = RESTORE_TIMEOUT_MS) {
|
|
462
|
+
if (timeoutMs <= 0) {
|
|
463
|
+
void cleanup.catch((error) => console.warn("Cache cleanup failed after its deadline", error));
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
let timer;
|
|
467
|
+
const timeout = new Promise((resolve) => {
|
|
468
|
+
timer = setTimeout(() => {
|
|
469
|
+
console.warn(timeoutMessage);
|
|
470
|
+
resolve(false);
|
|
471
|
+
}, timeoutMs);
|
|
472
|
+
});
|
|
473
|
+
try {
|
|
474
|
+
return await Promise.race([cleanup.then(() => true), timeout]);
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
clearTimeout(timer);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function waitForRestore(restored, timeoutMs = RESTORE_TIMEOUT_MS) {
|
|
481
|
+
if (timeoutMs <= 0) {
|
|
482
|
+
console.warn("Cached action results did not restore in time; loading without them.");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
170
485
|
let timer;
|
|
171
486
|
const timeout = new Promise((resolve) => {
|
|
172
487
|
timer = setTimeout(() => {
|
|
173
488
|
console.warn("Cached action results did not restore in time; loading without them.");
|
|
174
489
|
resolve();
|
|
175
|
-
},
|
|
490
|
+
}, timeoutMs);
|
|
176
491
|
});
|
|
177
492
|
try {
|
|
178
493
|
await Promise.race([
|
|
@@ -188,161 +503,304 @@ async function waitForRestore(restored) {
|
|
|
188
503
|
* Points the client's action results at one scope's record and restores it.
|
|
189
504
|
* Resolves once the record is in the client or the restore has given up.
|
|
190
505
|
*/
|
|
191
|
-
export async function bindActionQueryCache(client, scope) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// or org.
|
|
199
|
-
for (const key of await storage.keys()) {
|
|
200
|
-
if (key !== scope)
|
|
201
|
-
await storage.del(key);
|
|
202
|
-
}
|
|
203
|
-
if (generation !== bindingGeneration)
|
|
506
|
+
export async function bindActionQueryCache(client, scope, options = {}) {
|
|
507
|
+
const attempt = ++bindAttemptGeneration;
|
|
508
|
+
const bindDeadline = Date.now() + BIND_WAIT_BUDGET_MS;
|
|
509
|
+
const remainingBindBudget = () => Math.max(0, bindDeadline - Date.now());
|
|
510
|
+
let cacheSafety = readCacheSafetyState(true);
|
|
511
|
+
if (!cacheSafety) {
|
|
512
|
+
releaseWithoutCache(client, attempt);
|
|
204
513
|
return;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
? clientDeserializeData(data)
|
|
222
|
-
: data;
|
|
223
|
-
const expected = sourceEntries()[deserializeIndex++];
|
|
224
|
-
if (!expected || !Object.is(expected[1].data, data)) {
|
|
225
|
-
payloadMappingValid = false;
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
deserializedByHash.set(expected[0], {
|
|
229
|
-
dataUpdatedAt: expected[1].dataUpdatedAt,
|
|
230
|
-
data: result,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
return result;
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
const hydratedQueryHashes = new Set();
|
|
238
|
-
const unsubscribeHydration = client.getQueryCache().subscribe((event) => {
|
|
239
|
-
if (!persister.restoreWasAccepted())
|
|
240
|
-
return;
|
|
241
|
-
const hash = event.query.queryHash;
|
|
242
|
-
const source = persister.restoredSources().get(hash);
|
|
243
|
-
const deserialized = deserializedByHash.get(hash);
|
|
244
|
-
if (!source || !deserialized)
|
|
514
|
+
}
|
|
515
|
+
if (!storageClearInFlight) {
|
|
516
|
+
storageClearRequired = cacheSafety.pendingClearIntents.length > 0;
|
|
517
|
+
}
|
|
518
|
+
if (storageClearInFlight || storageClearRequired) {
|
|
519
|
+
let clearFinished;
|
|
520
|
+
try {
|
|
521
|
+
clearFinished = await waitForStorageClearWithinBound(remainingBindBudget());
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (attempt === bindAttemptGeneration) {
|
|
525
|
+
releaseWithoutCache(client, attempt);
|
|
526
|
+
}
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
if (attempt !== bindAttemptGeneration) {
|
|
245
530
|
return;
|
|
246
|
-
// An unrelated local setState can happen while restore is pending. Count
|
|
247
|
-
// it only when its payload is the object produced by SDK deserialization.
|
|
248
|
-
const isHydratedState = (state) => state.status === source.status &&
|
|
249
|
-
state.dataUpdatedAt === source.dataUpdatedAt &&
|
|
250
|
-
Object.is(state.data, deserialized.data);
|
|
251
|
-
if (event.type === "added" && isHydratedState(event.query.state)) {
|
|
252
|
-
hydratedQueryHashes.add(hash);
|
|
253
531
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
hydratedQueryHashes.add(hash);
|
|
532
|
+
if (!clearFinished || remainingBindBudget() <= 0) {
|
|
533
|
+
releaseWithoutCache(client, attempt);
|
|
534
|
+
return;
|
|
258
535
|
}
|
|
536
|
+
}
|
|
537
|
+
cacheSafety = readCacheSafetyState(true);
|
|
538
|
+
if (!cacheSafety || cacheSafety.pendingClearIntents.length > 0) {
|
|
539
|
+
releaseWithoutCache(client, attempt);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const trustPrebindResults = options.trustPrebindResults === true;
|
|
543
|
+
const previousBinding = binding;
|
|
544
|
+
const alreadyBoundClient = previousBinding?.client === client;
|
|
545
|
+
const deniedBeforeBind = client
|
|
546
|
+
.getQueryCache()
|
|
547
|
+
.getAll()
|
|
548
|
+
.filter(isAccessDeniedActionQuery);
|
|
549
|
+
const denialsKnownFromDifferentScope = deniedBeforeBind.filter((query) => {
|
|
550
|
+
const token = getAccessDeniedActionQueryBinding(query);
|
|
551
|
+
const denialScope = token ? bindingScopes.get(token) : undefined;
|
|
552
|
+
return denialScope !== undefined && denialScope !== scope;
|
|
259
553
|
});
|
|
260
|
-
const
|
|
261
|
-
|
|
554
|
+
const everyDenialKnownFromDifferentScope = deniedBeforeBind.length > 0 &&
|
|
555
|
+
denialsKnownFromDifferentScope.length === deniedBeforeBind.length;
|
|
556
|
+
const earlierDeletes = [...inFlightStorageDeletes];
|
|
557
|
+
closeBinding();
|
|
558
|
+
// An unverified result fetched before the first scope was known must not be
|
|
559
|
+
// consumed by the newly authenticated tree. Trusted first-read preloads stay
|
|
560
|
+
// in memory, but their fetch-start token remains absent, so they are never
|
|
561
|
+
// adopted into this scope's persisted record.
|
|
562
|
+
if (!alreadyBoundClient && !trustPrebindResults) {
|
|
563
|
+
client.removeQueries({
|
|
564
|
+
queryKey: ACTION_QUERY_KEY,
|
|
565
|
+
predicate: (query) => !isAccessDeniedActionQuery(query),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
const generation = bindingGeneration;
|
|
569
|
+
const persister = scopedPersister(scope, cacheSafety.epoch);
|
|
570
|
+
const token = {};
|
|
571
|
+
registerActionQueryCacheRevoker(client, () => persister.discard(), token);
|
|
572
|
+
bindingScopes.set(token, scope);
|
|
573
|
+
let unsubscribePersist = () => { };
|
|
574
|
+
let stopHydrationTracking = () => { };
|
|
575
|
+
let unsubscribeRevoked = () => { };
|
|
576
|
+
const currentBinding = {
|
|
577
|
+
client,
|
|
578
|
+
scope,
|
|
579
|
+
token,
|
|
262
580
|
persister,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
shouldDehydrateQuery: isBrowserPersistableActionQuery,
|
|
268
|
-
// A mutation replayed from disk would look like a write that succeeded.
|
|
269
|
-
shouldDehydrateMutation: () => false,
|
|
581
|
+
unsubscribe: () => {
|
|
582
|
+
unsubscribePersist();
|
|
583
|
+
unsubscribeRevoked();
|
|
584
|
+
stopHydrationTracking();
|
|
270
585
|
},
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
586
|
+
};
|
|
587
|
+
binding = currentBinding;
|
|
588
|
+
setActionQueryCacheBindingToken(token);
|
|
589
|
+
const isCurrentBinding = () => binding === currentBinding && generation === bindingGeneration;
|
|
590
|
+
unsubscribeRevoked = client.getQueryCache().subscribe((event) => {
|
|
275
591
|
if (event.type !== "updated" || event.action.type !== "error")
|
|
276
592
|
return;
|
|
277
593
|
if (event.query.queryKey[0] !== "action")
|
|
278
594
|
return;
|
|
279
595
|
const status = actionErrorStatus(event.action.error);
|
|
280
|
-
// The record may still hold this result from before the refetch. Only a
|
|
281
|
-
// successful query is dehydrated, so the rewrite after this error leaves
|
|
282
|
-
// it out.
|
|
283
596
|
if (status === 401 || status === 403 || status === 404) {
|
|
597
|
+
const deniedBinding = getAccessDeniedActionQueryBinding(event.query);
|
|
598
|
+
const deniedScope = deniedBinding
|
|
599
|
+
? bindingScopes.get(deniedBinding)
|
|
600
|
+
: undefined;
|
|
601
|
+
if (deniedScope !== undefined && deniedScope !== scope)
|
|
602
|
+
return;
|
|
284
603
|
revokeBrowserPersistableActionQuery(event.query);
|
|
285
604
|
persister.discard();
|
|
605
|
+
clearAccessDeniedActionQueryData(event.query);
|
|
286
606
|
}
|
|
287
607
|
});
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
608
|
+
if (deniedBeforeBind.length > 0) {
|
|
609
|
+
if (!everyDenialKnownFromDifferentScope)
|
|
610
|
+
persister.discard();
|
|
611
|
+
for (const query of deniedBeforeBind) {
|
|
612
|
+
revokeBrowserPersistableActionQuery(query);
|
|
613
|
+
if (everyDenialKnownFromDifferentScope) {
|
|
614
|
+
query.setState({
|
|
615
|
+
data: undefined,
|
|
616
|
+
dataUpdatedAt: 0,
|
|
617
|
+
error: null,
|
|
618
|
+
errorUpdatedAt: 0,
|
|
619
|
+
fetchFailureCount: 0,
|
|
620
|
+
fetchFailureReason: null,
|
|
621
|
+
status: "pending",
|
|
622
|
+
fetchStatus: "idle",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
clearAccessDeniedActionQueryData(query);
|
|
627
|
+
}
|
|
305
628
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
Object.is(query.state.data, deserialized?.data)) {
|
|
319
|
-
markRestoredBrowserPersistableActionQuery(query);
|
|
629
|
+
}
|
|
630
|
+
const cleanup = (async () => {
|
|
631
|
+
await Promise.allSettled(earlierDeletes);
|
|
632
|
+
if (!isCurrentBinding())
|
|
633
|
+
return;
|
|
634
|
+
let blockPersistenceForScope = false;
|
|
635
|
+
if (failedStorageDeleteScopes.has(scope)) {
|
|
636
|
+
try {
|
|
637
|
+
await trackRevokedScopeDelete(scope, storage.del(scope));
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
blockPersistenceForScope = true;
|
|
320
641
|
}
|
|
321
642
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
643
|
+
if (blockPersistenceForScope)
|
|
644
|
+
persister.blockRestoreAndPersistence();
|
|
645
|
+
if (!isCurrentBinding())
|
|
646
|
+
return;
|
|
647
|
+
await waitForStorageOperations();
|
|
648
|
+
if (!isCurrentBinding())
|
|
649
|
+
return;
|
|
650
|
+
const keys = await storage.keys();
|
|
651
|
+
if (!isCurrentBinding())
|
|
652
|
+
return;
|
|
653
|
+
for (const key of keys) {
|
|
654
|
+
if (key === scope)
|
|
655
|
+
continue;
|
|
656
|
+
if (!isCurrentBinding())
|
|
657
|
+
return;
|
|
658
|
+
await trackRevokedScopeDelete(key, storage.del(key));
|
|
659
|
+
}
|
|
660
|
+
})();
|
|
661
|
+
try {
|
|
662
|
+
if (!(await waitForCleanup(cleanup, undefined, remainingBindBudget())) ||
|
|
663
|
+
remainingBindBudget() <= 0) {
|
|
664
|
+
if (isCurrentBinding())
|
|
665
|
+
persister.blockRestoreAndPersistence();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (!isCurrentBinding())
|
|
669
|
+
return;
|
|
670
|
+
let payloadMappingValid = true;
|
|
671
|
+
let deserializeIndex = 0;
|
|
672
|
+
const deserializedByHash = new Map();
|
|
673
|
+
let cachedSourceEntries;
|
|
674
|
+
const sourceEntries = () => {
|
|
675
|
+
cachedSourceEntries ??= [...persister.restoredSources()].filter(([, source]) => source.data !== undefined);
|
|
676
|
+
return cachedSourceEntries;
|
|
677
|
+
};
|
|
678
|
+
const clientHydrateOptions = client.getDefaultOptions().hydrate;
|
|
679
|
+
const clientDeserializeData = clientHydrateOptions?.deserializeData;
|
|
680
|
+
const hydrateOptions = {
|
|
681
|
+
defaultOptions: {
|
|
682
|
+
...clientHydrateOptions,
|
|
683
|
+
deserializeData: (data) => {
|
|
684
|
+
const result = clientDeserializeData
|
|
685
|
+
? clientDeserializeData(data)
|
|
686
|
+
: data;
|
|
687
|
+
const expected = sourceEntries()[deserializeIndex++];
|
|
688
|
+
if (!expected || !Object.is(expected[1].data, data)) {
|
|
689
|
+
payloadMappingValid = false;
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
deserializedByHash.set(expected[0], {
|
|
693
|
+
dataUpdatedAt: expected[1].dataUpdatedAt,
|
|
694
|
+
data: result,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return result;
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
const hydratedQueryHashes = new Set();
|
|
702
|
+
stopHydrationTracking = client.getQueryCache().subscribe((event) => {
|
|
703
|
+
if (!persister.restoreWasAccepted())
|
|
704
|
+
return;
|
|
705
|
+
const hash = event.query.queryHash;
|
|
706
|
+
const source = persister.restoredSources().get(hash);
|
|
707
|
+
const deserialized = deserializedByHash.get(hash);
|
|
708
|
+
if (!source || !deserialized)
|
|
709
|
+
return;
|
|
710
|
+
// An unrelated local setState can happen while restore is pending. Count
|
|
711
|
+
// it only when its payload is the object produced by SDK deserialization.
|
|
712
|
+
const isHydratedState = (state) => state.status === source.status &&
|
|
713
|
+
state.dataUpdatedAt === source.dataUpdatedAt &&
|
|
714
|
+
Object.is(state.data, deserialized.data);
|
|
715
|
+
if (event.type === "added" && isHydratedState(event.query.state)) {
|
|
716
|
+
hydratedQueryHashes.add(hash);
|
|
717
|
+
}
|
|
718
|
+
else if (event.type === "updated" &&
|
|
719
|
+
event.action.type === "setState" &&
|
|
720
|
+
isHydratedState(event.action.state)) {
|
|
721
|
+
hydratedQueryHashes.add(hash);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
const [unsubscribe, restored] = persistQueryClient({
|
|
725
|
+
queryClient: client,
|
|
726
|
+
persister,
|
|
727
|
+
buster: `${ACTION_QUERY_CACHE_BUSTER}:${cacheSafety.epoch}`,
|
|
728
|
+
maxAge: ACTION_QUERY_CACHE_MAX_AGE_MS,
|
|
729
|
+
hydrateOptions,
|
|
730
|
+
dehydrateOptions: {
|
|
731
|
+
shouldDehydrateQuery: (query) => isBrowserPersistableActionQueryForBinding(query, token),
|
|
732
|
+
// A mutation replayed from disk would look like a write that succeeded.
|
|
733
|
+
shouldDehydrateMutation: () => false,
|
|
734
|
+
},
|
|
338
735
|
});
|
|
736
|
+
unsubscribePersist = unsubscribe;
|
|
737
|
+
const finishAcceptedRestore = () => {
|
|
738
|
+
const expectedDataCount = sourceEntries().length;
|
|
739
|
+
if (deserializeIndex !== expectedDataCount)
|
|
740
|
+
payloadMappingValid = false;
|
|
741
|
+
if (!payloadMappingValid ||
|
|
742
|
+
generation !== bindingGeneration ||
|
|
743
|
+
!persister.restoreWasAccepted()) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const restoredStates = persister.restoredStates();
|
|
747
|
+
const restoredSources = persister.restoredSources();
|
|
748
|
+
for (const query of client.getQueryCache().getAll()) {
|
|
749
|
+
const source = restoredSources.get(query.queryHash);
|
|
750
|
+
const deserialized = deserializedByHash.get(query.queryHash);
|
|
751
|
+
if (hydratedQueryHashes.has(query.queryHash) &&
|
|
752
|
+
source?.queryKey[0] === "action" &&
|
|
753
|
+
source.status === "success" &&
|
|
754
|
+
query.queryKey[0] === "action" &&
|
|
755
|
+
query.state.status === "success" &&
|
|
756
|
+
restoredStates.get(query.queryHash) === query.state.dataUpdatedAt &&
|
|
757
|
+
source.dataUpdatedAt === deserialized?.dataUpdatedAt &&
|
|
758
|
+
Object.is(query.state.data, deserialized?.data)) {
|
|
759
|
+
markRestoredBrowserPersistableActionQuery(query, token);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
// This runs after SDK hydration settles even if the UI's bounded wait has
|
|
764
|
+
// already released the app; a later scope generation cannot be authorized.
|
|
765
|
+
void restored
|
|
766
|
+
.then(() => {
|
|
767
|
+
finishAcceptedRestore();
|
|
768
|
+
}, () => { })
|
|
769
|
+
.finally(() => {
|
|
770
|
+
cachedSourceEntries = undefined;
|
|
771
|
+
deserializedByHash.clear();
|
|
772
|
+
hydratedQueryHashes.clear();
|
|
773
|
+
persister.releaseRestorePayload();
|
|
774
|
+
stopHydrationTracking();
|
|
775
|
+
stopHydrationTracking = () => { };
|
|
776
|
+
});
|
|
777
|
+
await waitForRestore(restored, remainingBindBudget());
|
|
778
|
+
// A scope change during the restore can hydrate the old scope after the new
|
|
779
|
+
// one has started; drop those results before the new scope saves them. A
|
|
780
|
+
// query the new scope has refetched since holds newer data and is kept.
|
|
781
|
+
if (generation !== bindingGeneration) {
|
|
782
|
+
const restoredStates = persister.restoredStates();
|
|
783
|
+
client.removeQueries({
|
|
784
|
+
predicate: (query) => query.queryKey[0] === "action" &&
|
|
785
|
+
restoredStates.get(query.queryHash) === query.state.dataUpdatedAt,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
if (binding === currentBinding)
|
|
791
|
+
closeBinding();
|
|
792
|
+
throw error;
|
|
339
793
|
}
|
|
340
794
|
}
|
|
341
795
|
/** Sign-out: stop, drop this document's results, and delete every stored
|
|
342
796
|
* record. */
|
|
343
797
|
export async function clearActionQueryCache() {
|
|
344
798
|
closeBinding();
|
|
345
|
-
|
|
799
|
+
const clear = beginStorageClear();
|
|
800
|
+
const clearFinished = await waitForCleanup(clear, "Cached action results could not be cleared in time; cleanup remains required.");
|
|
801
|
+
if (!clearFinished) {
|
|
802
|
+
throw new Error("Cached action result clearing timed out; cleanup remains required.");
|
|
803
|
+
}
|
|
346
804
|
}
|
|
347
805
|
/**
|
|
348
806
|
* Holds action queries until the session's cached results are in the client,
|
|
@@ -355,31 +813,40 @@ export function ActionQueryCacheGate({ children }) {
|
|
|
355
813
|
const scope = session
|
|
356
814
|
? actionQueryCacheScope(session, clientBuildId())
|
|
357
815
|
: null;
|
|
358
|
-
const
|
|
816
|
+
const terminalUnauthenticated = status === "unauthenticated";
|
|
817
|
+
const bindAttempt = useMemo(() => ({
|
|
818
|
+
trustPrebindResults: scope !== null && !terminalUnauthenticated && isSessionFromFirstRead(),
|
|
819
|
+
}), [queryClient, scope, terminalUnauthenticated]);
|
|
820
|
+
const [settledAttempt, setSettledAttempt] = useState(null);
|
|
359
821
|
useEffect(() => {
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
if (status === "unauthenticated") {
|
|
822
|
+
// A transient session reread does not replace a same-scope cache binding.
|
|
823
|
+
if (terminalUnauthenticated) {
|
|
363
824
|
clearActionQueryCache().catch((error) => console.warn("Unable to clear cached action results", error));
|
|
825
|
+
setSettledAttempt(bindAttempt);
|
|
364
826
|
return;
|
|
365
827
|
}
|
|
366
|
-
if (!scope)
|
|
828
|
+
if (!scope) {
|
|
829
|
+
setSettledAttempt(bindAttempt);
|
|
367
830
|
return;
|
|
831
|
+
}
|
|
368
832
|
let current = true;
|
|
369
|
-
setRestoredScope(null);
|
|
370
833
|
const finish = () => {
|
|
371
834
|
if (current)
|
|
372
|
-
|
|
835
|
+
setSettledAttempt(bindAttempt);
|
|
373
836
|
};
|
|
374
|
-
bindActionQueryCache(queryClient, scope
|
|
837
|
+
bindActionQueryCache(queryClient, scope, {
|
|
838
|
+
trustPrebindResults: bindAttempt.trustPrebindResults,
|
|
839
|
+
}).then(finish, (error) => {
|
|
375
840
|
console.warn("Unable to use cached action results", error);
|
|
376
841
|
finish();
|
|
377
842
|
});
|
|
378
843
|
return () => {
|
|
379
844
|
current = false;
|
|
845
|
+
bindAttemptGeneration += 1;
|
|
380
846
|
closeBinding();
|
|
381
847
|
};
|
|
382
|
-
}, [queryClient, scope,
|
|
383
|
-
const restoring = scope !== null &&
|
|
384
|
-
|
|
848
|
+
}, [bindAttempt, queryClient, scope, terminalUnauthenticated]);
|
|
849
|
+
const restoring = scope !== null && settledAttempt !== bindAttempt;
|
|
850
|
+
const holdUntrustedChildren = scope !== null && !bindAttempt.trustPrebindResults && restoring;
|
|
851
|
+
return (_jsx(IsRestoringProvider, { value: restoring, children: holdUntrustedChildren ? null : children }));
|
|
385
852
|
}
|