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
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
reloadLatchTimer
|
|
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
|
-
|
|
64
|
-
reloadInProgress
|
|
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
|
|
485
|
+
let globalDeletes = [];
|
|
486
|
+
let globalRemints = [];
|
|
487
|
+
let openTransitions = [];
|
|
488
|
+
const transitionDeletes = [];
|
|
489
|
+
const transitionRemints = [];
|
|
73
490
|
return Promise.resolve().then(() => {
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
if (reloadStarted !== false) return;
|
|
518
|
+
let reloadStarted = false;
|
|
79
519
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
96
|
-
|
|
564
|
+
refreshGroupCache(deps);
|
|
565
|
+
if (GROUP_TRANSITION_REASONS.has(reasonKey)) {
|
|
566
|
+
recoverViaGroupTransition(deps, reasonKey, message, dropLocalState);
|
|
567
|
+
return;
|
|
97
568
|
}
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
241
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
reloadLatchTimer
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|