@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.
Files changed (51) hide show
  1. package/dist/agent/production-agent.js +2 -0
  2. package/dist/cli/design-connect.d.ts +6 -0
  3. package/dist/cli/design-connect.js +880 -9
  4. package/dist/client/action-query-cache.d.ts +3 -1
  5. package/dist/client/action-query-cache.js +628 -161
  6. package/dist/client/action-query-cache.session-test-fixture.d.ts +49 -0
  7. package/dist/client/action-query-cache.session-test-fixture.js +151 -0
  8. package/dist/client/action-query-cache.test-fixtures.d.ts +2 -0
  9. package/dist/client/action-query-cache.test-fixtures.js +56 -0
  10. package/dist/client/motion-text.css +3 -0
  11. package/dist/client/motion-text.d.ts +38 -0
  12. package/dist/client/motion-text.js +551 -0
  13. package/dist/client/org/hooks.d.ts +3 -1
  14. package/dist/client/org/hooks.js +8 -2
  15. package/dist/client/route-chunk-recovery.d.ts +2 -0
  16. package/dist/client/route-chunk-recovery.js +11 -0
  17. package/dist/client/use-action.d.ts +10 -2
  18. package/dist/client/use-action.js +81 -12
  19. package/dist/client/use-settings-shortcut.js +8 -3
  20. package/dist/collab/awareness.d.ts +2 -2
  21. package/dist/collab/routes.d.ts +1 -1
  22. package/dist/collab/struct-routes.d.ts +1 -1
  23. package/dist/extensions/fetch-tool.js +8 -2
  24. package/dist/extensions/routes.js +7 -3
  25. package/dist/extensions/web-content.js +17 -2
  26. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  27. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  28. package/dist/mcp/build-server.js +5 -0
  29. package/dist/observability/routes.d.ts +4 -4
  30. package/dist/progress/routes.d.ts +1 -1
  31. package/dist/resources/handlers.d.ts +1 -1
  32. package/dist/secrets/routes.d.ts +5 -5
  33. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  34. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  35. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  36. package/dist/server/realtime-token.d.ts +1 -1
  37. package/dist/server/transcribe-voice.d.ts +1 -1
  38. package/dist/server/visual-edit-bridge-capability.d.ts +26 -0
  39. package/dist/server/visual-edit-bridge-capability.js +233 -0
  40. package/dist/shared/motion-easing.d.ts +6 -0
  41. package/dist/shared/motion-easing.js +109 -0
  42. package/dist/shared/motion-source-node-id.d.ts +3 -0
  43. package/dist/shared/motion-source-node-id.js +12 -0
  44. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  45. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +35 -36
  46. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  47. package/dist/vite/client.js +2 -0
  48. package/dist/vite/motion-source-provenance-plugin.d.ts +3 -0
  49. package/dist/vite/motion-source-provenance-plugin.js +339 -0
  50. package/package.json +14 -5
  51. 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 { isBrowserPersistableActionQuery, markRestoredBrowserPersistableActionQuery, revokeBrowserPersistableActionQuery, } from "./use-action.js";
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 ? withinMaxAge(stored) : undefined;
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 && revision === restoreRevision && record !== undefined;
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
- return storage.del(scope);
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
- storage
143
- .del(scope)
144
- .catch((error) => console.warn("Unable to delete cached action results", error));
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
- if (!binding)
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 waitForRestore(restored) {
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
- }, RESTORE_TIMEOUT_MS);
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
- // Nothing is cleared here: closeBinding has already dropped any previous
193
- // scope's results, and results fetched before the session resolved (a hinted
194
- // preload) belong to this session and must survive the bind.
195
- closeBinding();
196
- const generation = bindingGeneration;
197
- // One record at a time: a record for any other scope belongs to another user
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
- const persister = scopedPersister(scope);
206
- let payloadMappingValid = true;
207
- let deserializeIndex = 0;
208
- const deserializedByHash = new Map();
209
- let cachedSourceEntries;
210
- const sourceEntries = () => {
211
- cachedSourceEntries ??= [...persister.restoredSources()].filter(([, source]) => source.data !== undefined);
212
- return cachedSourceEntries;
213
- };
214
- const clientHydrateOptions = client.getDefaultOptions().hydrate;
215
- const clientDeserializeData = clientHydrateOptions?.deserializeData;
216
- const hydrateOptions = {
217
- defaultOptions: {
218
- ...clientHydrateOptions,
219
- deserializeData: (data) => {
220
- const result = clientDeserializeData
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
- else if (event.type === "updated" &&
255
- event.action.type === "setState" &&
256
- isHydratedState(event.action.state)) {
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 [unsubscribe, restored] = persistQueryClient({
261
- queryClient: client,
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
- buster: ACTION_QUERY_CACHE_BUSTER,
264
- maxAge: ACTION_QUERY_CACHE_MAX_AGE_MS,
265
- hydrateOptions,
266
- dehydrateOptions: {
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
- // Subscribed before persistQueryClient's own listener (which subscribes only
273
- // after the restore), so the delete runs before the error's rewrite.
274
- const unsubscribeRevoked = client.getQueryCache().subscribe((event) => {
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
- binding = {
289
- client,
290
- persister,
291
- unsubscribe: () => {
292
- unsubscribe();
293
- unsubscribeRevoked();
294
- unsubscribeHydration();
295
- },
296
- };
297
- const finishAcceptedRestore = () => {
298
- const expectedDataCount = sourceEntries().length;
299
- if (deserializeIndex !== expectedDataCount)
300
- payloadMappingValid = false;
301
- if (!payloadMappingValid ||
302
- generation !== bindingGeneration ||
303
- !persister.restoreWasAccepted()) {
304
- return;
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
- const restoredStates = persister.restoredStates();
307
- const restoredSources = persister.restoredSources();
308
- for (const query of client.getQueryCache().getAll()) {
309
- const source = restoredSources.get(query.queryHash);
310
- const deserialized = deserializedByHash.get(query.queryHash);
311
- if (hydratedQueryHashes.has(query.queryHash) &&
312
- source?.queryKey[0] === "action" &&
313
- source.status === "success" &&
314
- query.queryKey[0] === "action" &&
315
- query.state.status === "success" &&
316
- restoredStates.get(query.queryHash) === query.state.dataUpdatedAt &&
317
- source.dataUpdatedAt === deserialized?.dataUpdatedAt &&
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
- // This runs after SDK hydration settles even if the UI's bounded wait has
324
- // already released the app; a later scope generation cannot be authorized.
325
- void restored.then(() => {
326
- finishAcceptedRestore();
327
- unsubscribeHydration();
328
- }, () => unsubscribeHydration());
329
- await waitForRestore(restored);
330
- // A scope change during the restore can hydrate the old scope after the new
331
- // one has started; drop those results before the new scope saves them. A
332
- // query the new scope has refetched since holds newer data and is kept.
333
- if (generation !== bindingGeneration) {
334
- const restoredStates = persister.restoredStates();
335
- client.removeQueries({
336
- predicate: (query) => query.queryKey[0] === "action" &&
337
- restoredStates.get(query.queryHash) === query.state.dataUpdatedAt,
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
- await storage.clear();
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 [restoredScope, setRestoredScope] = useState(null);
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
- // "loading", "unavailable" and "signing-out" are not an ended session:
361
- // signOut() clears the store itself, and a transient read must not wipe it.
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
- setRestoredScope(scope);
835
+ setSettledAttempt(bindAttempt);
373
836
  };
374
- bindActionQueryCache(queryClient, scope).then(finish, (error) => {
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, status]);
383
- const restoring = scope !== null && restoredScope !== scope;
384
- return (_jsx(IsRestoringProvider, { value: restoring, children: children }));
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
  }