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.
- package/dist/cjs/createUseQuery.cjs +2 -1
- package/dist/cjs/createUseQuery.native.js +2 -1
- package/dist/cjs/createUseQuery.native.js.map +1 -1
- package/dist/cjs/createUseQueryDirect.cjs +2 -1
- package/dist/cjs/createUseQueryDirect.native.js +2 -1
- package/dist/cjs/createUseQueryDirect.native.js.map +1 -1
- package/dist/cjs/createZeroClient.cjs +75 -9
- package/dist/cjs/createZeroClient.native.js +151 -81
- package/dist/cjs/createZeroClient.native.js.map +1 -1
- package/dist/cjs/createZeroClient.recovery.test.cjs +351 -10
- package/dist/cjs/createZeroClient.recovery.test.native.js +412 -12
- package/dist/cjs/createZeroClient.recovery.test.native.js.map +1 -1
- package/dist/cjs/generate.cjs +16 -11
- package/dist/cjs/generate.native.js +16 -11
- package/dist/cjs/generate.native.js.map +1 -1
- package/dist/cjs/generate.test.cjs +22 -0
- package/dist/cjs/generate.test.native.js +22 -0
- package/dist/cjs/generate.test.native.js.map +1 -1
- package/dist/cjs/helpers/recoverZeroClient.cjs +542 -52
- package/dist/cjs/helpers/recoverZeroClient.native.js +721 -59
- package/dist/cjs/helpers/recoverZeroClient.native.js.map +1 -1
- package/dist/cjs/helpers/recoverZeroClient.test.cjs +318 -0
- package/dist/cjs/helpers/recoverZeroClient.test.native.js +361 -0
- package/dist/cjs/helpers/recoverZeroClient.test.native.js.map +1 -1
- package/dist/cjs/kvStore.cjs +30 -0
- package/dist/cjs/kvStore.native.js +34 -0
- package/dist/cjs/kvStore.native.js.map +1 -0
- package/dist/cjs/queryJson.cjs +58 -0
- package/dist/cjs/queryJson.native.js +86 -0
- package/dist/cjs/queryJson.native.js.map +1 -0
- package/dist/cjs/resolveQuery.cjs +2 -0
- package/dist/cjs/resolveQuery.native.js +2 -0
- package/dist/cjs/resolveQuery.native.js.map +1 -1
- package/dist/cjs/useQuery.empty.test.cjs +11 -1
- package/dist/cjs/useQuery.empty.test.native.js +23 -1
- package/dist/cjs/useQuery.empty.test.native.js.map +1 -1
- package/dist/cjs/useQuery.ssr.test.cjs +22 -1
- package/dist/cjs/useQuery.ssr.test.native.js +30 -1
- package/dist/cjs/useQuery.ssr.test.native.js.map +1 -1
- package/dist/esm/createUseQuery.mjs +2 -1
- package/dist/esm/createUseQuery.mjs.map +1 -1
- package/dist/esm/createUseQuery.native.js +2 -1
- package/dist/esm/createUseQuery.native.js.map +1 -1
- package/dist/esm/createUseQueryDirect.mjs +2 -1
- package/dist/esm/createUseQueryDirect.mjs.map +1 -1
- package/dist/esm/createUseQueryDirect.native.js +2 -1
- package/dist/esm/createUseQueryDirect.native.js.map +1 -1
- package/dist/esm/createZeroClient.mjs +76 -10
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +152 -82
- package/dist/esm/createZeroClient.native.js.map +1 -1
- package/dist/esm/createZeroClient.recovery.test.mjs +352 -11
- package/dist/esm/createZeroClient.recovery.test.mjs.map +1 -1
- package/dist/esm/createZeroClient.recovery.test.native.js +413 -13
- package/dist/esm/createZeroClient.recovery.test.native.js.map +1 -1
- package/dist/esm/generate.mjs +16 -11
- package/dist/esm/generate.mjs.map +1 -1
- package/dist/esm/generate.native.js +16 -11
- package/dist/esm/generate.native.js.map +1 -1
- package/dist/esm/generate.test.mjs +22 -0
- package/dist/esm/generate.test.mjs.map +1 -1
- package/dist/esm/generate.test.native.js +22 -0
- package/dist/esm/generate.test.native.js.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.mjs +542 -54
- package/dist/esm/helpers/recoverZeroClient.mjs.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.native.js +721 -61
- package/dist/esm/helpers/recoverZeroClient.native.js.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.test.mjs +319 -1
- package/dist/esm/helpers/recoverZeroClient.test.mjs.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.test.native.js +362 -1
- package/dist/esm/helpers/recoverZeroClient.test.native.js.map +1 -1
- package/dist/esm/kvStore.mjs +5 -0
- package/dist/esm/kvStore.mjs.map +1 -0
- package/dist/esm/kvStore.native.js +6 -0
- package/dist/esm/kvStore.native.js.map +1 -0
- package/dist/esm/queryJson.mjs +32 -0
- package/dist/esm/queryJson.mjs.map +1 -0
- package/dist/esm/queryJson.native.js +57 -0
- package/dist/esm/queryJson.native.js.map +1 -0
- package/dist/esm/resolveQuery.mjs +2 -0
- package/dist/esm/resolveQuery.mjs.map +1 -1
- package/dist/esm/resolveQuery.native.js +2 -0
- package/dist/esm/resolveQuery.native.js.map +1 -1
- package/dist/esm/useQuery.empty.test.mjs +12 -2
- package/dist/esm/useQuery.empty.test.mjs.map +1 -1
- package/dist/esm/useQuery.empty.test.native.js +24 -2
- package/dist/esm/useQuery.empty.test.native.js.map +1 -1
- package/dist/esm/useQuery.ssr.test.mjs +23 -2
- package/dist/esm/useQuery.ssr.test.mjs.map +1 -1
- package/dist/esm/useQuery.ssr.test.native.js +31 -2
- package/dist/esm/useQuery.ssr.test.native.js.map +1 -1
- package/package.json +11 -3
- package/src/createUseQuery.tsx +2 -1
- package/src/createUseQueryDirect.tsx +2 -1
- package/src/createZeroClient.recovery.test.tsx +370 -7
- package/src/createZeroClient.tsx +100 -10
- package/src/generate.test.ts +31 -0
- package/src/generate.ts +22 -10
- package/src/helpers/recoverZeroClient.test.ts +323 -0
- package/src/helpers/recoverZeroClient.ts +896 -78
- package/src/kvStore.native.ts +7 -0
- package/src/kvStore.ts +7 -0
- package/src/queryJson.ts +46 -0
- package/src/resolveQuery.ts +2 -0
- package/src/useQuery.empty.test.tsx +16 -2
- package/src/useQuery.ssr.test.ts +23 -2
- package/types/createUseQuery.d.ts.map +1 -1
- package/types/createUseQueryDirect.d.ts.map +1 -1
- package/types/createZeroClient.d.ts.map +1 -1
- package/types/createZeroClient.recovery.test.d.ts.map +1 -1
- package/types/generate.d.ts.map +1 -1
- package/types/helpers/recoverZeroClient.d.ts +6 -0
- package/types/helpers/recoverZeroClient.d.ts.map +1 -1
- package/types/kvStore.d.ts +2 -0
- package/types/kvStore.d.ts.map +1 -0
- package/types/kvStore.native.d.ts +3 -0
- package/types/kvStore.native.d.ts.map +1 -0
- package/types/queryJson.d.ts +3 -0
- package/types/queryJson.d.ts.map +1 -0
- 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
reloadLatchTimer
|
|
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
|
-
|
|
95
|
-
reloadInProgress
|
|
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
|
|
518
|
+
let globalDeletes = [];
|
|
519
|
+
let globalRemints = [];
|
|
520
|
+
let openTransitions = [];
|
|
521
|
+
const transitionDeletes = [];
|
|
522
|
+
const transitionRemints = [];
|
|
104
523
|
return Promise.resolve().then(() => {
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
112
|
-
|
|
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
|
-
|
|
127
|
-
|
|
597
|
+
refreshGroupCache(deps);
|
|
598
|
+
if (GROUP_TRANSITION_REASONS.has(reasonKey)) {
|
|
599
|
+
recoverViaGroupTransition(deps, reasonKey, message, dropLocalState);
|
|
600
|
+
return;
|
|
128
601
|
}
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
272
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
reloadLatchTimer
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
}
|