on-zero 0.16.19 → 0.16.20

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 (120) hide show
  1. package/dist/cjs/createUseQuery.cjs +2 -1
  2. package/dist/cjs/createUseQuery.native.js +2 -1
  3. package/dist/cjs/createUseQuery.native.js.map +1 -1
  4. package/dist/cjs/createUseQueryDirect.cjs +2 -1
  5. package/dist/cjs/createUseQueryDirect.native.js +2 -1
  6. package/dist/cjs/createUseQueryDirect.native.js.map +1 -1
  7. package/dist/cjs/createZeroClient.cjs +75 -9
  8. package/dist/cjs/createZeroClient.native.js +151 -81
  9. package/dist/cjs/createZeroClient.native.js.map +1 -1
  10. package/dist/cjs/createZeroClient.recovery.test.cjs +351 -10
  11. package/dist/cjs/createZeroClient.recovery.test.native.js +412 -12
  12. package/dist/cjs/createZeroClient.recovery.test.native.js.map +1 -1
  13. package/dist/cjs/generate.cjs +16 -11
  14. package/dist/cjs/generate.native.js +16 -11
  15. package/dist/cjs/generate.native.js.map +1 -1
  16. package/dist/cjs/generate.test.cjs +22 -0
  17. package/dist/cjs/generate.test.native.js +22 -0
  18. package/dist/cjs/generate.test.native.js.map +1 -1
  19. package/dist/cjs/helpers/recoverZeroClient.cjs +542 -52
  20. package/dist/cjs/helpers/recoverZeroClient.native.js +721 -59
  21. package/dist/cjs/helpers/recoverZeroClient.native.js.map +1 -1
  22. package/dist/cjs/helpers/recoverZeroClient.test.cjs +318 -0
  23. package/dist/cjs/helpers/recoverZeroClient.test.native.js +361 -0
  24. package/dist/cjs/helpers/recoverZeroClient.test.native.js.map +1 -1
  25. package/dist/cjs/kvStore.cjs +30 -0
  26. package/dist/cjs/kvStore.native.js +34 -0
  27. package/dist/cjs/kvStore.native.js.map +1 -0
  28. package/dist/cjs/queryJson.cjs +58 -0
  29. package/dist/cjs/queryJson.native.js +86 -0
  30. package/dist/cjs/queryJson.native.js.map +1 -0
  31. package/dist/cjs/resolveQuery.cjs +2 -0
  32. package/dist/cjs/resolveQuery.native.js +2 -0
  33. package/dist/cjs/resolveQuery.native.js.map +1 -1
  34. package/dist/cjs/useQuery.empty.test.cjs +11 -1
  35. package/dist/cjs/useQuery.empty.test.native.js +23 -1
  36. package/dist/cjs/useQuery.empty.test.native.js.map +1 -1
  37. package/dist/cjs/useQuery.ssr.test.cjs +22 -1
  38. package/dist/cjs/useQuery.ssr.test.native.js +30 -1
  39. package/dist/cjs/useQuery.ssr.test.native.js.map +1 -1
  40. package/dist/esm/createUseQuery.mjs +2 -1
  41. package/dist/esm/createUseQuery.mjs.map +1 -1
  42. package/dist/esm/createUseQuery.native.js +2 -1
  43. package/dist/esm/createUseQuery.native.js.map +1 -1
  44. package/dist/esm/createUseQueryDirect.mjs +2 -1
  45. package/dist/esm/createUseQueryDirect.mjs.map +1 -1
  46. package/dist/esm/createUseQueryDirect.native.js +2 -1
  47. package/dist/esm/createUseQueryDirect.native.js.map +1 -1
  48. package/dist/esm/createZeroClient.mjs +76 -10
  49. package/dist/esm/createZeroClient.mjs.map +1 -1
  50. package/dist/esm/createZeroClient.native.js +152 -82
  51. package/dist/esm/createZeroClient.native.js.map +1 -1
  52. package/dist/esm/createZeroClient.recovery.test.mjs +352 -11
  53. package/dist/esm/createZeroClient.recovery.test.mjs.map +1 -1
  54. package/dist/esm/createZeroClient.recovery.test.native.js +413 -13
  55. package/dist/esm/createZeroClient.recovery.test.native.js.map +1 -1
  56. package/dist/esm/generate.mjs +16 -11
  57. package/dist/esm/generate.mjs.map +1 -1
  58. package/dist/esm/generate.native.js +16 -11
  59. package/dist/esm/generate.native.js.map +1 -1
  60. package/dist/esm/generate.test.mjs +22 -0
  61. package/dist/esm/generate.test.mjs.map +1 -1
  62. package/dist/esm/generate.test.native.js +22 -0
  63. package/dist/esm/generate.test.native.js.map +1 -1
  64. package/dist/esm/helpers/recoverZeroClient.mjs +542 -54
  65. package/dist/esm/helpers/recoverZeroClient.mjs.map +1 -1
  66. package/dist/esm/helpers/recoverZeroClient.native.js +721 -61
  67. package/dist/esm/helpers/recoverZeroClient.native.js.map +1 -1
  68. package/dist/esm/helpers/recoverZeroClient.test.mjs +319 -1
  69. package/dist/esm/helpers/recoverZeroClient.test.mjs.map +1 -1
  70. package/dist/esm/helpers/recoverZeroClient.test.native.js +362 -1
  71. package/dist/esm/helpers/recoverZeroClient.test.native.js.map +1 -1
  72. package/dist/esm/kvStore.mjs +5 -0
  73. package/dist/esm/kvStore.mjs.map +1 -0
  74. package/dist/esm/kvStore.native.js +6 -0
  75. package/dist/esm/kvStore.native.js.map +1 -0
  76. package/dist/esm/queryJson.mjs +32 -0
  77. package/dist/esm/queryJson.mjs.map +1 -0
  78. package/dist/esm/queryJson.native.js +57 -0
  79. package/dist/esm/queryJson.native.js.map +1 -0
  80. package/dist/esm/resolveQuery.mjs +2 -0
  81. package/dist/esm/resolveQuery.mjs.map +1 -1
  82. package/dist/esm/resolveQuery.native.js +2 -0
  83. package/dist/esm/resolveQuery.native.js.map +1 -1
  84. package/dist/esm/useQuery.empty.test.mjs +12 -2
  85. package/dist/esm/useQuery.empty.test.mjs.map +1 -1
  86. package/dist/esm/useQuery.empty.test.native.js +24 -2
  87. package/dist/esm/useQuery.empty.test.native.js.map +1 -1
  88. package/dist/esm/useQuery.ssr.test.mjs +23 -2
  89. package/dist/esm/useQuery.ssr.test.mjs.map +1 -1
  90. package/dist/esm/useQuery.ssr.test.native.js +31 -2
  91. package/dist/esm/useQuery.ssr.test.native.js.map +1 -1
  92. package/package.json +11 -3
  93. package/src/createUseQuery.tsx +2 -1
  94. package/src/createUseQueryDirect.tsx +2 -1
  95. package/src/createZeroClient.recovery.test.tsx +370 -7
  96. package/src/createZeroClient.tsx +100 -10
  97. package/src/generate.test.ts +31 -0
  98. package/src/generate.ts +22 -10
  99. package/src/helpers/recoverZeroClient.test.ts +323 -0
  100. package/src/helpers/recoverZeroClient.ts +896 -78
  101. package/src/kvStore.native.ts +7 -0
  102. package/src/kvStore.ts +7 -0
  103. package/src/queryJson.ts +46 -0
  104. package/src/resolveQuery.ts +2 -0
  105. package/src/useQuery.empty.test.tsx +16 -2
  106. package/src/useQuery.ssr.test.ts +23 -2
  107. package/types/createUseQuery.d.ts.map +1 -1
  108. package/types/createUseQueryDirect.d.ts.map +1 -1
  109. package/types/createZeroClient.d.ts.map +1 -1
  110. package/types/createZeroClient.recovery.test.d.ts.map +1 -1
  111. package/types/generate.d.ts.map +1 -1
  112. package/types/helpers/recoverZeroClient.d.ts +6 -0
  113. package/types/helpers/recoverZeroClient.d.ts.map +1 -1
  114. package/types/kvStore.d.ts +2 -0
  115. package/types/kvStore.d.ts.map +1 -0
  116. package/types/kvStore.native.d.ts +3 -0
  117. package/types/kvStore.native.d.ts.map +1 -0
  118. package/types/queryJson.d.ts +3 -0
  119. package/types/queryJson.d.ts.map +1 -0
  120. package/types/resolveQuery.d.ts.map +1 -1
@@ -24,25 +24,51 @@ var recoverZeroClient_exports = {};
24
24
  __export(recoverZeroClient_exports, {
25
25
  classifyZeroRecoveryLog: () => classifyZeroRecoveryLog,
26
26
  composeRecoveryLogSink: () => composeRecoveryLogSink,
27
+ isGroupTransitionOpen: () => isGroupTransitionOpen,
27
28
  isRecoverableZeroStalePokeMessage: () => isRecoverableZeroStalePokeMessage,
28
29
  makeZeroRecovery: () => makeZeroRecovery,
30
+ primeGroupCache: () => primeGroupCache,
29
31
  resetRecoveryStateForTests: () => resetRecoveryStateForTests
30
32
  });
31
33
  module.exports = __toCommonJS(recoverZeroClient_exports);
34
+ var import_globalValue = require("./globalValue.cjs");
32
35
  const RECOVER_GUARD_MS = 6e4;
33
36
  const RELOAD_LATCH_TIMEOUT_MS = 15e3;
37
+ const GROUP_TRANSITION_REASONS = /* @__PURE__ */new Set(["NewClientGroup", "VersionNotSupported", "SchemaVersionNotSupported"]);
34
38
  const LOCAL_STORE_LOST = "Expected IndexedDB not found";
35
39
  const SQLITE_ERROR_NAME = "SqliteError";
36
40
  const SQLITE_STATEMENT_FINALIZED = "This statement has been finalized";
37
41
  const STORE_CLOSED = "Store is closed";
38
42
  const STORE_CLOSED_REPEAT_MIN_MS = 2e3;
39
43
  const STORE_CLOSED_REPEAT_MAX_MS = 6e4;
40
- let reloadScheduled = false;
41
- let reloadInProgress = false;
42
- let reloadLatchTimer;
43
- const pendingDeletes = [];
44
- const pendingInPlaceRecoveries = [];
45
- const inMemoryGuard = /* @__PURE__ */new Map();
44
+ function sharedRecoveryState() {
45
+ return (0, import_globalValue.globalValue)("on-zero:recovery-shared", () => ({
46
+ reloadScheduled: false,
47
+ reloadInProgress: false,
48
+ reloadLatchTimer: void 0,
49
+ pendingDeletes: [],
50
+ pendingInPlaceRecoveries: [],
51
+ inMemoryGuard: /* @__PURE__ */new Map(),
52
+ constructionSeq: 0,
53
+ constructionSeqByDeps: /* @__PURE__ */new WeakMap(),
54
+ groupByDeps: /* @__PURE__ */new WeakMap(),
55
+ transitions: /* @__PURE__ */new Map(),
56
+ terminalMemory: /* @__PURE__ */new Map(),
57
+ terminalEmitted: /* @__PURE__ */new Map(),
58
+ unknownPostSignals: /* @__PURE__ */new Map(),
59
+ lastStoreClosedAtMs: 0
60
+ }));
61
+ }
62
+ function documentScope() {
63
+ try {
64
+ return globalThis.location?.href ?? "";
65
+ } catch {
66
+ return "";
67
+ }
68
+ }
69
+ function guardStorageKey(reasonKey) {
70
+ return `on-zero-recover-${documentScope()}-${reasonKey}`;
71
+ }
46
72
  function defaultGuardStorage() {
47
73
  try {
48
74
  if (typeof window === "undefined" || !window.sessionStorage) return void 0;
@@ -52,13 +78,9 @@ function defaultGuardStorage() {
52
78
  }
53
79
  }
54
80
  function recoveryGuardOpen(reasonKey, guardStorage) {
55
- let documentScope = "";
56
- try {
57
- documentScope = globalThis.location?.href ?? "";
58
- } catch {}
59
- const key = `on-zero-recover-${documentScope}-${reasonKey}`;
81
+ const key = guardStorageKey(reasonKey);
60
82
  const now = Date.now();
61
- const memLast = inMemoryGuard.get(key) ?? 0;
83
+ const memLast = sharedRecoveryState().inMemoryGuard.get(key) ?? 0;
62
84
  if (memLast > 0 && now - memLast < RECOVER_GUARD_MS) return false;
63
85
  const storage = guardStorage ?? defaultGuardStorage();
64
86
  if (storage) {
@@ -69,30 +91,423 @@ function recoveryGuardOpen(reasonKey, guardStorage) {
69
91
  storage.setItem(key, String(now));
70
92
  } catch {}
71
93
  }
72
- inMemoryGuard.set(key, now);
94
+ sharedRecoveryState().inMemoryGuard.set(key, now);
73
95
  return true;
74
96
  }
75
- let lastStoreClosedAtMs = 0;
97
+ function readRecoveryMarker(reasonKey, guardStorage) {
98
+ const storage = guardStorage ?? defaultGuardStorage();
99
+ if (!storage) return null;
100
+ let raw = null;
101
+ try {
102
+ raw = storage.getItem(guardStorageKey(reasonKey));
103
+ } catch {
104
+ return null;
105
+ }
106
+ if (!raw) return null;
107
+ if (/^\d+$/.test(raw)) return {
108
+ t: Number(raw)
109
+ };
110
+ try {
111
+ const parsed = JSON.parse(raw);
112
+ if (!parsed || typeof parsed.t !== "number") return null;
113
+ return parsed;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ function writeRecoveryMarker(reasonKey, marker, guardStorage) {
119
+ const storage = guardStorage ?? defaultGuardStorage();
120
+ if (!storage) return;
121
+ try {
122
+ storage.setItem(guardStorageKey(reasonKey), JSON.stringify(marker));
123
+ } catch {}
124
+ }
125
+ function clearRecoveryMarker(reasonKey, guardStorage) {
126
+ const storage = guardStorage ?? defaultGuardStorage();
127
+ if (!storage) return;
128
+ try {
129
+ if (typeof storage.removeItem === "function") {
130
+ storage.removeItem(guardStorageKey(reasonKey));
131
+ } else {
132
+ storage.setItem(guardStorageKey(reasonKey), JSON.stringify({
133
+ t: 0
134
+ }));
135
+ }
136
+ } catch {}
137
+ }
76
138
  function armReloadLatchTimeout() {
139
+ const shared = sharedRecoveryState();
77
140
  if (typeof setTimeout !== "function") return;
78
- if (reloadLatchTimer) clearTimeout(reloadLatchTimer);
79
- reloadLatchTimer = setTimeout(() => {
80
- reloadLatchTimer = void 0;
81
- reloadScheduled = false;
141
+ if (shared.reloadLatchTimer) clearTimeout(shared.reloadLatchTimer);
142
+ shared.reloadLatchTimer = setTimeout(() => {
143
+ shared.reloadLatchTimer = void 0;
144
+ shared.reloadScheduled = false;
82
145
  }, RELOAD_LATCH_TIMEOUT_MS);
83
- if (reloadLatchTimer && typeof reloadLatchTimer === "object" && "unref" in reloadLatchTimer) {
84
- reloadLatchTimer.unref();
146
+ if (shared.reloadLatchTimer && typeof shared.reloadLatchTimer === "object" && "unref" in shared.reloadLatchTimer) {
147
+ shared.reloadLatchTimer.unref();
85
148
  }
86
149
  }
87
150
  function disarmReloadLatchTimeout() {
88
- if (reloadLatchTimer) {
89
- clearTimeout(reloadLatchTimer);
90
- reloadLatchTimer = void 0;
151
+ const shared = sharedRecoveryState();
152
+ if (shared.reloadLatchTimer) {
153
+ clearTimeout(shared.reloadLatchTimer);
154
+ shared.reloadLatchTimer = void 0;
155
+ }
156
+ }
157
+ function pushGlobalDelete(deps) {
158
+ const shared = sharedRecoveryState();
159
+ if (shared.pendingDeletes.some(entry => entry.owner === deps.deleteLocalState)) {
160
+ return;
161
+ }
162
+ shared.pendingDeletes.push({
163
+ owner: deps.deleteLocalState,
164
+ run: () => Promise.resolve().then(deps.deleteLocalState)
165
+ });
166
+ }
167
+ function removeGlobalDelete(owner) {
168
+ const pending = sharedRecoveryState().pendingDeletes;
169
+ const index = pending.findIndex(entry => entry.owner === owner);
170
+ if (index !== -1) pending.splice(index, 1);
171
+ }
172
+ function removeGlobalRemint(fn) {
173
+ if (!fn) return;
174
+ const pending = sharedRecoveryState().pendingInPlaceRecoveries;
175
+ const index = pending.indexOf(fn);
176
+ if (index !== -1) pending.splice(index, 1);
177
+ }
178
+ function seqOf(deps) {
179
+ const shared = sharedRecoveryState();
180
+ let seq = shared.constructionSeqByDeps.get(deps);
181
+ if (seq === void 0) {
182
+ seq = ++shared.constructionSeq;
183
+ shared.constructionSeqByDeps.set(deps, seq);
184
+ }
185
+ return seq;
186
+ }
187
+ function refreshGroupCache(deps) {
188
+ if (!deps.getGroupID) return;
189
+ let result;
190
+ try {
191
+ result = deps.getGroupID();
192
+ } catch {
193
+ return;
194
+ }
195
+ if (typeof result === "string") {
196
+ if (result) sharedRecoveryState().groupByDeps.set(deps, result);
197
+ return;
198
+ }
199
+ void Promise.resolve(result).then(id => {
200
+ if (typeof id === "string" && id) sharedRecoveryState().groupByDeps.set(deps, id);
201
+ }, () => {});
202
+ }
203
+ function cachedGroupID(deps) {
204
+ return sharedRecoveryState().groupByDeps.get(deps);
205
+ }
206
+ function primeGroupCache(deps) {
207
+ refreshGroupCache(deps);
208
+ }
209
+ function participantKey(deps) {
210
+ return deps.recoverInPlace ?? deps.deleteLocalState;
211
+ }
212
+ function transitionKey(deps) {
213
+ return `${documentScope()}\0${deps.clientIdentity ?? ""}`;
214
+ }
215
+ function findOpenTransitionParticipant(deps) {
216
+ const transition = sharedRecoveryState().transitions.get(transitionKey(deps));
217
+ if (!transition || transition.settled) return void 0;
218
+ if (seqOf(deps) > transition.startSeq) return void 0;
219
+ return transition.participants.get(participantKey(deps));
220
+ }
221
+ function isGroupTransitionOpen() {
222
+ return sharedRecoveryState().transitions.size > 0;
223
+ }
224
+ function terminalKey(key, reasonKey) {
225
+ return `${key}\0${reasonKey}`;
226
+ }
227
+ function stampTerminal(key, reasonKey, guardStorage, message, groups) {
228
+ sharedRecoveryState().terminalMemory.set(terminalKey(key, reasonKey), {
229
+ t: Date.now(),
230
+ groups
231
+ });
232
+ writeRecoveryMarker(reasonKey, {
233
+ t: Date.now(),
234
+ terminal: true,
235
+ groups,
236
+ msg: message
237
+ }, guardStorage);
238
+ }
239
+ function emitTerminalOnce(deps, key, reasonKey, message) {
240
+ const shared = sharedRecoveryState();
241
+ const now = Date.now();
242
+ const emitKey = terminalKey(key, reasonKey);
243
+ const lastEmitted = shared.terminalEmitted.get(emitKey) ?? 0;
244
+ if (lastEmitted > 0 && now - lastEmitted < RECOVER_GUARD_MS) return;
245
+ shared.terminalEmitted.set(emitKey, now);
246
+ try {
247
+ console.error(`[on-zero] ${message}`);
248
+ deps.zeroEvents.emit({
249
+ type: "fatal",
250
+ reasonKey,
251
+ reason: message
252
+ });
253
+ } catch {}
254
+ try {
255
+ globalThis.dispatchEvent?.(new CustomEvent("on-zero-fatal", {
256
+ detail: {
257
+ reasonKey,
258
+ reason: message
259
+ }
260
+ }));
261
+ } catch {}
262
+ }
263
+ function settleTransition(transition, outcome) {
264
+ if (transition.settled) return;
265
+ transition.settled = true;
266
+ transition.outcome = outcome;
267
+ sharedRecoveryState().transitions.delete(transition.key);
268
+ transition.notifySettled();
269
+ }
270
+ function checkTransitionComplete(transition) {
271
+ if (transition.settled) return;
272
+ for (const participant of transition.participants.values()) {
273
+ if (!participant.reminted && !participant.skipped) return;
91
274
  }
275
+ if (transition.pendingWaits > 0) return;
276
+ settleTransition(transition, "completed");
277
+ }
278
+ function failTransition(transition, input) {
279
+ if (transition.settled) return;
280
+ const groups = [...new Set([...transition.participants.values()].map(participant => participant.oldGroupID).filter(group => typeof group === "string"))];
281
+ settleTransition(transition, "terminal");
282
+ for (const reasonKey of /* @__PURE__ */new Set([...transition.reasons, input.reasonKey])) {
283
+ stampTerminal(transition.key, reasonKey, input.signalDeps.guardStorage, input.message, groups);
284
+ }
285
+ emitTerminalOnce(input.signalDeps, transition.key, input.reasonKey, input.message);
286
+ }
287
+ function makeWrappedRemint(transition, participant) {
288
+ return async () => {
289
+ if (participant.skipped || participant.reminted) return true;
290
+ const remint = participant.deps.recoverInPlace;
291
+ if (!remint) {
292
+ participant.skipped = true;
293
+ checkTransitionComplete(transition);
294
+ return false;
295
+ }
296
+ let ok;
297
+ try {
298
+ ok = await remint();
299
+ } catch (error) {
300
+ const [firstReason] = transition.reasons;
301
+ failTransition(transition, {
302
+ reasonKey: firstReason ?? "NewClientGroup",
303
+ message: `in-place recovery threw (${error instanceof Error ? error.message : String(error)})`,
304
+ signalDeps: participant.deps
305
+ });
306
+ return false;
307
+ }
308
+ if (!ok) {
309
+ participant.skipped = true;
310
+ checkTransitionComplete(transition);
311
+ return false;
312
+ }
313
+ participant.reminted = true;
314
+ const awaitReconnected = participant.deps.awaitReconnected;
315
+ if (awaitReconnected) {
316
+ transition.pendingWaits += 1;
317
+ try {
318
+ await Promise.race([awaitReconnected(), transition.settledPromise]);
319
+ } catch {} finally {
320
+ transition.pendingWaits -= 1;
321
+ }
322
+ }
323
+ checkTransitionComplete(transition);
324
+ return true;
325
+ };
326
+ }
327
+ async function runParticipantImmediate(transition, participant) {
328
+ const doDelete = participant.dropLocalState && !participant.deleteExecuted;
329
+ const doRemint = !participant.remintExecuted;
330
+ participant.deleteExecuted = true;
331
+ participant.remintExecuted = true;
332
+ if (doDelete) {
333
+ try {
334
+ await participant.deps.deleteLocalState();
335
+ } catch {}
336
+ }
337
+ if (doRemint) {
338
+ if (participant.wrappedRemint) await participant.wrappedRemint();else {
339
+ participant.skipped = true;
340
+ checkTransitionComplete(transition);
341
+ }
342
+ }
343
+ }
344
+ function joinTransition(transition, deps, reasonKey, dropLocalState) {
345
+ transition.reasons.add(reasonKey);
346
+ const key = participantKey(deps);
347
+ const existing = transition.participants.get(key);
348
+ if (existing) {
349
+ if (dropLocalState && !existing.dropLocalState) {
350
+ existing.dropLocalState = true;
351
+ if (existing.deleteExecuted) pushGlobalDelete(deps);
352
+ }
353
+ return;
354
+ }
355
+ removeGlobalDelete(deps.deleteLocalState);
356
+ removeGlobalRemint(deps.recoverInPlace);
357
+ const participant = {
358
+ deps,
359
+ dropLocalState,
360
+ deleteExecuted: false,
361
+ remintExecuted: false,
362
+ reminted: false,
363
+ skipped: false,
364
+ oldGroupID: cachedGroupID(deps),
365
+ wrappedRemint: null
366
+ };
367
+ participant.wrappedRemint = makeWrappedRemint(transition, participant);
368
+ transition.participants.set(key, participant);
369
+ if (transition.remintsExecuted) void runParticipantImmediate(transition, participant);
370
+ }
371
+ function startTransition(deps, reasonKey, message, dropLocalState) {
372
+ const shared = sharedRecoveryState();
373
+ const key = transitionKey(deps);
374
+ let notifySettled;
375
+ const settledPromise = new Promise(resolve => {
376
+ notifySettled = resolve;
377
+ });
378
+ const transition = {
379
+ key,
380
+ reasons: /* @__PURE__ */new Set([reasonKey]),
381
+ participants: /* @__PURE__ */new Map(),
382
+ startSeq: shared.constructionSeq,
383
+ startedAtMs: Date.now(),
384
+ remintsExecuted: false,
385
+ pendingWaits: 0,
386
+ settled: false,
387
+ outcome: null,
388
+ settledPromise,
389
+ notifySettled
390
+ };
391
+ shared.transitions.set(key, transition);
392
+ joinTransition(transition, deps, reasonKey, dropLocalState);
393
+ console.warn(`[on-zero] ${message} \u2014 recovering`);
394
+ deps.zeroEvents.emit({
395
+ type: "recovering",
396
+ reasonKey,
397
+ reason: message
398
+ });
399
+ if (shared.reloadScheduled && !shared.reloadInProgress) return;
400
+ if (shared.reloadInProgress) {
401
+ transition.remintsExecuted = true;
402
+ for (const participant of transition.participants.values()) {
403
+ void runParticipantImmediate(transition, participant);
404
+ }
405
+ return;
406
+ }
407
+ shared.reloadScheduled = true;
408
+ armReloadLatchTimeout();
409
+ const runReload = () => performReload(deps);
410
+ if (deps.scheduleReload) {
411
+ deps.scheduleReload({
412
+ reason: message,
413
+ reasonKey,
414
+ dropLocalState,
415
+ performReload: runReload
416
+ });
417
+ } else {
418
+ void runReload();
419
+ }
420
+ }
421
+ function checkTransitionGuard(deps, reasonKey) {
422
+ const shared = sharedRecoveryState();
423
+ const key = transitionKey(deps);
424
+ const now = Date.now();
425
+ const myGroup = cachedGroupID(deps);
426
+ const memory = shared.terminalMemory.get(terminalKey(key, reasonKey));
427
+ if (memory && memory.t > 0 && now - memory.t < RECOVER_GUARD_MS) {
428
+ if (myGroup !== void 0 && memory.groups.length > 0 && !memory.groups.includes(myGroup)) {
429
+ shared.terminalMemory.delete(terminalKey(key, reasonKey));
430
+ } else {
431
+ return "silent";
432
+ }
433
+ }
434
+ const marker = readRecoveryMarker(reasonKey, deps.guardStorage);
435
+ if (marker && marker.t > 0 && now - marker.t < RECOVER_GUARD_MS) {
436
+ const markerGroups = marker.groups && marker.groups.length > 0 ? marker.groups : void 0;
437
+ if (myGroup !== void 0 && markerGroups !== void 0 && !markerGroups.includes(myGroup)) {
438
+ clearRecoveryMarker(reasonKey, deps.guardStorage);
439
+ return "allow";
440
+ }
441
+ if (marker.terminal) {
442
+ emitTerminalOnce(deps, key, reasonKey, marker.msg ?? `update needed (${reasonKey}) \u2014 recovery already failed, not retrying`);
443
+ return "silent";
444
+ }
445
+ if (myGroup !== void 0 && markerGroups !== void 0 && markerGroups.includes(myGroup)) {
446
+ const message2 = `update needed (${reasonKey}) \u2014 client group ${myGroup} \u2192 ${myGroup} (unchanged after reload), not reloading`;
447
+ stampTerminal(key, reasonKey, deps.guardStorage, message2, markerGroups);
448
+ emitTerminalOnce(deps, key, reasonKey, message2);
449
+ return "silent";
450
+ }
451
+ const message = `update needed (${reasonKey}) \u2014 already recovered once, not reloading`;
452
+ stampTerminal(key, reasonKey, deps.guardStorage, message, markerGroups ?? []);
453
+ emitTerminalOnce(deps, key, reasonKey, message);
454
+ return "silent";
455
+ }
456
+ return "allow";
457
+ }
458
+ function handlePostTransitionSignal(transition, deps, reasonKey, message, dropLocalState) {
459
+ const shared = sharedRecoveryState();
460
+ const newGroup = cachedGroupID(deps);
461
+ const oldGroups = [...new Set([...transition.participants.values()].map(participant => participant.oldGroupID).filter(group => typeof group === "string"))];
462
+ if (newGroup !== void 0 && oldGroups.length > 0) {
463
+ if (!oldGroups.includes(newGroup)) {
464
+ settleTransition(transition, "superseded");
465
+ startTransition(deps, reasonKey, message, dropLocalState);
466
+ return;
467
+ }
468
+ failTransition(transition, {
469
+ reasonKey,
470
+ message: `${message} \u2014 client group ${newGroup} did not change after recovery (still ${oldGroups.join(", ")}), not retrying`,
471
+ signalDeps: deps
472
+ });
473
+ return;
474
+ }
475
+ const now = Date.now();
476
+ const budget = shared.unknownPostSignals.get(transition.key);
477
+ const freshBudget = budget && now - budget.windowStartMs < RECOVER_GUARD_MS ? budget : null;
478
+ if (freshBudget && freshBudget.count >= 1) {
479
+ failTransition(transition, {
480
+ reasonKey,
481
+ message: `${message} \u2014 recovery did not resolve the client group, not retrying`,
482
+ signalDeps: deps
483
+ });
484
+ return;
485
+ }
486
+ shared.unknownPostSignals.set(transition.key, {
487
+ count: (freshBudget?.count ?? 0) + 1,
488
+ windowStartMs: freshBudget?.windowStartMs ?? now
489
+ });
490
+ settleTransition(transition, "superseded");
491
+ startTransition(deps, reasonKey, message, dropLocalState);
492
+ }
493
+ function recoverViaGroupTransition(deps, reasonKey, message, dropLocalState) {
494
+ const key = transitionKey(deps);
495
+ const open = sharedRecoveryState().transitions.get(key);
496
+ if (open && !open.settled) {
497
+ if (seqOf(deps) > open.startSeq) {
498
+ handlePostTransitionSignal(open, deps, reasonKey, message, dropLocalState);
499
+ return;
500
+ }
501
+ joinTransition(open, deps, reasonKey, dropLocalState);
502
+ return;
503
+ }
504
+ if (checkTransitionGuard(deps, reasonKey) !== "allow") return;
505
+ startTransition(deps, reasonKey, message, dropLocalState);
92
506
  }
93
507
  function performReload(deps) {
94
- if (reloadInProgress) return Promise.resolve();
95
- reloadInProgress = true;
508
+ const shared = sharedRecoveryState();
509
+ if (shared.reloadInProgress) return Promise.resolve();
510
+ shared.reloadInProgress = true;
96
511
  disarmReloadLatchTimeout();
97
512
  const doReload = deps.reload ?? (() => {
98
513
  const reload = globalThis.location?.reload;
@@ -100,36 +515,104 @@ function performReload(deps) {
100
515
  reload.call(globalThis.location);
101
516
  return true;
102
517
  });
103
- let inPlaceRecoveries = [];
518
+ let globalDeletes = [];
519
+ let globalRemints = [];
520
+ let openTransitions = [];
521
+ const transitionDeletes = [];
522
+ const transitionRemints = [];
104
523
  return Promise.resolve().then(() => {
105
- inPlaceRecoveries = pendingInPlaceRecoveries.splice(0);
106
- return Promise.allSettled(pendingDeletes.splice(0).map(run => run()));
524
+ globalDeletes = shared.pendingDeletes.splice(0);
525
+ globalRemints = shared.pendingInPlaceRecoveries.splice(0);
526
+ openTransitions = [...shared.transitions.values()].filter(transition => !transition.settled);
527
+ for (const transition of openTransitions) {
528
+ transition.remintsExecuted = true;
529
+ for (const participant of transition.participants.values()) {
530
+ if (participant.dropLocalState && !participant.deleteExecuted) {
531
+ participant.deleteExecuted = true;
532
+ const runDelete = participant.deps.deleteLocalState;
533
+ transitionDeletes.push(() => Promise.resolve().then(runDelete));
534
+ }
535
+ if (!participant.remintExecuted) {
536
+ participant.remintExecuted = true;
537
+ if (participant.wrappedRemint) {
538
+ transitionRemints.push({
539
+ transition,
540
+ run: participant.wrappedRemint
541
+ });
542
+ } else {
543
+ participant.skipped = true;
544
+ checkTransitionComplete(transition);
545
+ }
546
+ }
547
+ }
548
+ }
549
+ return Promise.allSettled([...globalDeletes.map(entry => entry.run()), ...transitionDeletes.map(run => run())]);
107
550
  }).then(() => deps.beforeReload?.()).catch(() => {}).then(async () => {
108
- const reloadStarted = doReload();
109
- if (reloadStarted !== false) return;
551
+ let reloadStarted = false;
552
+ try {
553
+ reloadStarted = doReload();
554
+ } catch {
555
+ reloadStarted = false;
556
+ }
557
+ if (reloadStarted !== false) {
558
+ noteTransitionsReloading(openTransitions);
559
+ return;
560
+ }
110
561
  try {
111
- const results = await Promise.all(inPlaceRecoveries.map(recover2 => recover2()));
112
- if (inPlaceRecoveries.length > 0 && results.some(recovered => !recovered)) {
562
+ const liveRemints = transitionRemints.filter(({
563
+ transition
564
+ }) => !transition.settled);
565
+ const results = await Promise.all([...globalRemints.map(recover2 => recover2()), ...liveRemints.map(({
566
+ run
567
+ }) => run())]);
568
+ const globalResults = results.slice(0, globalRemints.length);
569
+ if (globalRemints.length > 0 && globalResults.some(recovered => !recovered)) {
113
570
  console.error("[on-zero] recovery could not reload or reconstruct the client");
114
571
  }
115
572
  } catch (error) {
116
573
  console.error("[on-zero] in-place recovery failed", error);
117
574
  } finally {
118
- reloadInProgress = false;
119
- reloadScheduled = false;
575
+ shared.reloadInProgress = false;
576
+ shared.reloadScheduled = false;
120
577
  }
121
578
  });
122
579
  }
580
+ function noteTransitionsReloading(transitions) {
581
+ for (const transition of transitions) {
582
+ if (transition.settled) continue;
583
+ const groups = [...new Set([...transition.participants.values()].map(participant => participant.oldGroupID).filter(group => typeof group === "string"))];
584
+ const [firstParticipant] = transition.participants.values();
585
+ for (const reasonKey of transition.reasons) {
586
+ writeRecoveryMarker(reasonKey, {
587
+ t: Date.now(),
588
+ groups
589
+ }, firstParticipant?.deps.guardStorage);
590
+ }
591
+ settleTransition(transition, "reloading");
592
+ }
593
+ }
123
594
  function recover(deps, reasonKey, message, dropLocalState) {
124
595
  if (typeof window === "undefined") return;
125
596
  deps.onRecovery?.(reasonKey);
126
- if (dropLocalState) {
127
- pendingDeletes.push(() => Promise.resolve().then(deps.deleteLocalState));
597
+ refreshGroupCache(deps);
598
+ if (GROUP_TRANSITION_REASONS.has(reasonKey)) {
599
+ recoverViaGroupTransition(deps, reasonKey, message, dropLocalState);
600
+ return;
128
601
  }
129
- if (deps.recoverInPlace && !pendingInPlaceRecoveries.includes(deps.recoverInPlace)) {
130
- pendingInPlaceRecoveries.push(deps.recoverInPlace);
602
+ const shared = sharedRecoveryState();
603
+ const participant = findOpenTransitionParticipant(deps);
604
+ if (!participant) {
605
+ if (dropLocalState) {
606
+ pushGlobalDelete(deps);
607
+ }
608
+ if (deps.recoverInPlace && !shared.pendingInPlaceRecoveries.includes(deps.recoverInPlace)) {
609
+ shared.pendingInPlaceRecoveries.push(deps.recoverInPlace);
610
+ }
611
+ } else if (dropLocalState && !participant.dropLocalState) {
612
+ participant.dropLocalState = true;
613
+ if (participant.deleteExecuted) pushGlobalDelete(deps);
131
614
  }
132
- if (reloadScheduled) return;
615
+ if (shared.reloadScheduled) return;
133
616
  if (!recoveryGuardOpen(reasonKey, deps.guardStorage)) {
134
617
  console.error(`[on-zero] ${message} \u2014 already recovered once, not reloading`);
135
618
  deps.zeroEvents.emit({
@@ -147,7 +630,7 @@ function recover(deps, reasonKey, message, dropLocalState) {
147
630
  } catch {}
148
631
  return;
149
632
  }
150
- reloadScheduled = true;
633
+ shared.reloadScheduled = true;
151
634
  armReloadLatchTimeout();
152
635
  console.warn(`[on-zero] ${message} \u2014 recovering`);
153
636
  deps.zeroEvents.emit({
@@ -168,6 +651,7 @@ function recover(deps, reasonKey, message, dropLocalState) {
168
651
  }
169
652
  }
170
653
  function makeZeroRecovery(deps) {
654
+ seqOf(deps);
171
655
  return {
172
656
  onUpdateNeeded(reason) {
173
657
  const dropLocalState = reason.type === "SchemaVersionNotSupported";
@@ -268,8 +752,9 @@ function classifyZeroRecoveryLog(level, args, nowMs = Date.now()) {
268
752
  };
269
753
  }
270
754
  if (text.includes(STORE_CLOSED) && !isBenignStoreClosedLog(text)) {
271
- const prevMs = lastStoreClosedAtMs;
272
- lastStoreClosedAtMs = nowMs;
755
+ const shared = sharedRecoveryState();
756
+ const prevMs = shared.lastStoreClosedAtMs;
757
+ shared.lastStoreClosedAtMs = nowMs;
273
758
  if (prevMs > 0 && nowMs - prevMs >= STORE_CLOSED_REPEAT_MIN_MS && nowMs - prevMs <= STORE_CLOSED_REPEAT_MAX_MS) {
274
759
  return {
275
760
  reasonKey: "store-closed-repeat",
@@ -305,14 +790,19 @@ function isRecoverableZeroStalePokeMessage(message) {
305
790
  return message.includes("Server returned unexpected base cookie during sync") || message.includes("Received cookie") && message.includes("is < than last snapshot cookie") && message.includes("ignoring client view");
306
791
  }
307
792
  function resetRecoveryStateForTests() {
308
- reloadScheduled = false;
309
- reloadInProgress = false;
310
- if (reloadLatchTimer) {
311
- clearTimeout(reloadLatchTimer);
312
- reloadLatchTimer = void 0;
313
- }
314
- pendingDeletes.length = 0;
315
- pendingInPlaceRecoveries.length = 0;
316
- lastStoreClosedAtMs = 0;
317
- inMemoryGuard.clear();
793
+ const shared = sharedRecoveryState();
794
+ shared.reloadScheduled = false;
795
+ shared.reloadInProgress = false;
796
+ if (shared.reloadLatchTimer) {
797
+ clearTimeout(shared.reloadLatchTimer);
798
+ shared.reloadLatchTimer = void 0;
799
+ }
800
+ shared.pendingDeletes.length = 0;
801
+ shared.pendingInPlaceRecoveries.length = 0;
802
+ shared.lastStoreClosedAtMs = 0;
803
+ shared.inMemoryGuard.clear();
804
+ shared.transitions.clear();
805
+ shared.terminalMemory.clear();
806
+ shared.terminalEmitted.clear();
807
+ shared.unknownPostSignals.clear();
318
808
  }