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