on-zero 0.6.2 → 0.6.4
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/createZeroClient.cjs +91 -12
- package/dist/cjs/createZeroClient.connection.test.cjs +163 -0
- package/dist/cjs/createZeroClient.connection.test.native.js +242 -0
- package/dist/cjs/createZeroClient.connection.test.native.js.map +1 -0
- package/dist/cjs/createZeroClient.native.js +99 -12
- package/dist/cjs/createZeroClient.native.js.map +1 -1
- package/dist/cjs/createZeroClient.recovery.test.cjs +52 -0
- package/dist/cjs/createZeroClient.recovery.test.native.js +52 -0
- package/dist/cjs/createZeroClient.recovery.test.native.js.map +1 -1
- package/dist/cjs/helpers/recoverZeroClient.cjs +197 -14
- package/dist/cjs/helpers/recoverZeroClient.native.js +220 -24
- package/dist/cjs/helpers/recoverZeroClient.native.js.map +1 -1
- package/dist/cjs/helpers/recoverZeroClient.test.cjs +241 -0
- package/dist/cjs/helpers/recoverZeroClient.test.native.js +269 -0
- package/dist/cjs/helpers/recoverZeroClient.test.native.js.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.native.js +2 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/multi.cjs +13 -0
- package/dist/cjs/multi.native.js +35 -0
- package/dist/cjs/multi.native.js.map +1 -1
- package/dist/cjs/multiPartition.test.cjs +42 -0
- package/dist/cjs/multiPartition.test.native.js +51 -0
- package/dist/cjs/multiPartition.test.native.js.map +1 -0
- package/dist/esm/createZeroClient.connection.test.mjs +164 -0
- package/dist/esm/createZeroClient.connection.test.mjs.map +1 -0
- package/dist/esm/createZeroClient.connection.test.native.js +240 -0
- package/dist/esm/createZeroClient.connection.test.native.js.map +1 -0
- package/dist/esm/createZeroClient.mjs +92 -13
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +100 -13
- package/dist/esm/createZeroClient.native.js.map +1 -1
- package/dist/esm/createZeroClient.recovery.test.mjs +52 -0
- package/dist/esm/createZeroClient.recovery.test.mjs.map +1 -1
- package/dist/esm/createZeroClient.recovery.test.native.js +52 -0
- package/dist/esm/createZeroClient.recovery.test.native.js.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.mjs +196 -15
- package/dist/esm/helpers/recoverZeroClient.mjs.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.native.js +219 -25
- package/dist/esm/helpers/recoverZeroClient.native.js.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.test.mjs +242 -1
- package/dist/esm/helpers/recoverZeroClient.test.mjs.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.test.native.js +270 -1
- package/dist/esm/helpers/recoverZeroClient.test.native.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/multi.mjs +13 -1
- package/dist/esm/multi.mjs.map +1 -1
- package/dist/esm/multi.native.js +35 -1
- package/dist/esm/multi.native.js.map +1 -1
- package/dist/esm/multiPartition.test.mjs +43 -0
- package/dist/esm/multiPartition.test.mjs.map +1 -0
- package/dist/esm/multiPartition.test.native.js +49 -0
- package/dist/esm/multiPartition.test.native.js.map +1 -0
- package/lint/ssr-guards.js +164 -0
- package/package.json +2 -2
- package/readme.md +126 -0
- package/src/createZeroClient.connection.test.tsx +162 -0
- package/src/createZeroClient.recovery.test.tsx +68 -0
- package/src/createZeroClient.tsx +159 -13
- package/src/helpers/recoverZeroClient.test.ts +270 -1
- package/src/helpers/recoverZeroClient.ts +347 -50
- package/src/index.ts +6 -0
- package/src/multi.ts +33 -0
- package/src/multiPartition.test.ts +38 -0
- package/types/createZeroClient.connection.test.d.ts +5 -0
- package/types/createZeroClient.connection.test.d.ts.map +1 -0
- package/types/createZeroClient.d.ts +17 -0
- package/types/createZeroClient.d.ts.map +1 -1
- package/types/createZeroServer.d.ts +4 -4
- package/types/helpers/recoverZeroClient.d.ts +23 -1
- package/types/helpers/recoverZeroClient.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +1 -1
- package/types/multi.d.ts +1 -0
- package/types/multi.d.ts.map +1 -1
- package/types/multiPartition.test.d.ts +2 -0
- package/types/multiPartition.test.d.ts.map +1 -0
|
@@ -1,28 +1,101 @@
|
|
|
1
|
+
function _instanceof(left, right) {
|
|
2
|
+
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
|
|
3
|
+
return !!right[Symbol.hasInstance](left);
|
|
4
|
+
} else {
|
|
5
|
+
return left instanceof right;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
1
8
|
function _type_of(obj) {
|
|
2
9
|
"@swc/helpers - typeof";
|
|
3
10
|
|
|
4
11
|
return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj;
|
|
5
12
|
}
|
|
6
13
|
var RECOVER_GUARD_MS = 6e4;
|
|
14
|
+
var RELOAD_LATCH_TIMEOUT_MS = 15e3;
|
|
7
15
|
var LOCAL_STORE_LOST = "Expected IndexedDB not found";
|
|
16
|
+
var SQLITE_ERROR_NAME = "SqliteError";
|
|
17
|
+
var SQLITE_STATEMENT_FINALIZED = "This statement has been finalized";
|
|
18
|
+
var STORE_CLOSED = "Store is closed";
|
|
19
|
+
var STORE_CLOSED_REPEAT_MIN_MS = 2e3;
|
|
20
|
+
var STORE_CLOSED_REPEAT_MAX_MS = 6e4;
|
|
8
21
|
var reloadScheduled = false;
|
|
22
|
+
var reloadInProgress = false;
|
|
23
|
+
var reloadLatchTimer;
|
|
9
24
|
var pendingDeletes = [];
|
|
10
|
-
|
|
25
|
+
var inMemoryGuard = /* @__PURE__ */new Map();
|
|
26
|
+
function defaultGuardStorage() {
|
|
11
27
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
28
|
+
if (typeof window === "undefined" || !window.sessionStorage) return void 0;
|
|
29
|
+
return window.sessionStorage;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function recoveryGuardOpen(reasonKey, guardStorage) {
|
|
35
|
+
var key = `on-zero-recover-${reasonKey}`;
|
|
36
|
+
var now = Date.now();
|
|
37
|
+
var _inMemoryGuard_get;
|
|
38
|
+
var memLast = (_inMemoryGuard_get = inMemoryGuard.get(key)) !== null && _inMemoryGuard_get !== void 0 ? _inMemoryGuard_get : 0;
|
|
39
|
+
if (memLast > 0 && now - memLast < RECOVER_GUARD_MS) return false;
|
|
40
|
+
var storage = guardStorage !== null && guardStorage !== void 0 ? guardStorage : defaultGuardStorage();
|
|
41
|
+
if (storage) {
|
|
42
|
+
try {
|
|
43
|
+
var rawLast = storage.getItem(key);
|
|
44
|
+
var last = rawLast ? Number(rawLast) : 0;
|
|
45
|
+
if (last > 0 && now - last < RECOVER_GUARD_MS) return false;
|
|
46
|
+
storage.setItem(key, String(now));
|
|
47
|
+
} catch (e) {}
|
|
48
|
+
}
|
|
49
|
+
inMemoryGuard.set(key, now);
|
|
17
50
|
return true;
|
|
18
51
|
}
|
|
52
|
+
var lastStoreClosedAtMs = 0;
|
|
53
|
+
function armReloadLatchTimeout() {
|
|
54
|
+
if (typeof setTimeout !== "function") return;
|
|
55
|
+
if (reloadLatchTimer) clearTimeout(reloadLatchTimer);
|
|
56
|
+
reloadLatchTimer = setTimeout(function () {
|
|
57
|
+
reloadLatchTimer = void 0;
|
|
58
|
+
reloadScheduled = false;
|
|
59
|
+
}, RELOAD_LATCH_TIMEOUT_MS);
|
|
60
|
+
if (reloadLatchTimer && (typeof reloadLatchTimer === "undefined" ? "undefined" : _type_of(reloadLatchTimer)) === "object" && "unref" in reloadLatchTimer) {
|
|
61
|
+
reloadLatchTimer.unref();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function disarmReloadLatchTimeout() {
|
|
65
|
+
if (reloadLatchTimer) {
|
|
66
|
+
clearTimeout(reloadLatchTimer);
|
|
67
|
+
reloadLatchTimer = void 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function performReload(deps) {
|
|
71
|
+
if (reloadInProgress) return Promise.resolve();
|
|
72
|
+
reloadInProgress = true;
|
|
73
|
+
disarmReloadLatchTimeout();
|
|
74
|
+
var _deps_reload;
|
|
75
|
+
var doReload = (_deps_reload = deps.reload) !== null && _deps_reload !== void 0 ? _deps_reload : function () {
|
|
76
|
+
var _globalThis_location_reload, _globalThis_location;
|
|
77
|
+
return (_globalThis_location = globalThis.location) === null || _globalThis_location === void 0 ? void 0 : (_globalThis_location_reload = _globalThis_location.reload) === null || _globalThis_location_reload === void 0 ? void 0 : _globalThis_location_reload.call(_globalThis_location);
|
|
78
|
+
};
|
|
79
|
+
return Promise.resolve().then(function () {
|
|
80
|
+
return Promise.allSettled(pendingDeletes.splice(0).map(function (run) {
|
|
81
|
+
return run();
|
|
82
|
+
}));
|
|
83
|
+
}).then(function () {
|
|
84
|
+
var _deps_beforeReload;
|
|
85
|
+
return (_deps_beforeReload = deps.beforeReload) === null || _deps_beforeReload === void 0 ? void 0 : _deps_beforeReload.call(deps);
|
|
86
|
+
}).catch(function () {}).then(function () {
|
|
87
|
+
doReload();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
19
90
|
function recover(deps, reasonKey, message, dropLocalState) {
|
|
20
91
|
if (typeof window === "undefined") return;
|
|
21
92
|
if (dropLocalState) {
|
|
22
|
-
pendingDeletes.push(
|
|
93
|
+
pendingDeletes.push(function () {
|
|
94
|
+
return Promise.resolve().then(deps.deleteLocalState);
|
|
95
|
+
});
|
|
23
96
|
}
|
|
24
97
|
if (reloadScheduled) return;
|
|
25
|
-
if (!recoveryGuardOpen(reasonKey)) {
|
|
98
|
+
if (!recoveryGuardOpen(reasonKey, deps.guardStorage)) {
|
|
26
99
|
console.error(`[on-zero] ${message} \u2014 already recovered once, not reloading`);
|
|
27
100
|
deps.zeroEvents.emit({
|
|
28
101
|
type: "fatal",
|
|
@@ -31,22 +104,25 @@ function recover(deps, reasonKey, message, dropLocalState) {
|
|
|
31
104
|
return;
|
|
32
105
|
}
|
|
33
106
|
reloadScheduled = true;
|
|
107
|
+
armReloadLatchTimeout();
|
|
34
108
|
console.warn(`[on-zero] ${message} \u2014 recovering`);
|
|
35
109
|
deps.zeroEvents.emit({
|
|
36
110
|
type: "recovering",
|
|
37
111
|
reason: message
|
|
38
112
|
});
|
|
39
|
-
var
|
|
40
|
-
|
|
41
|
-
var _globalThis_location_reload, _globalThis_location;
|
|
42
|
-
return (_globalThis_location = globalThis.location) === null || _globalThis_location === void 0 ? void 0 : (_globalThis_location_reload = _globalThis_location.reload) === null || _globalThis_location_reload === void 0 ? void 0 : _globalThis_location_reload.call(_globalThis_location);
|
|
113
|
+
var runReload = function () {
|
|
114
|
+
return performReload(deps);
|
|
43
115
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
if (deps.scheduleReload) {
|
|
117
|
+
deps.scheduleReload({
|
|
118
|
+
reason: message,
|
|
119
|
+
reasonKey,
|
|
120
|
+
dropLocalState,
|
|
121
|
+
performReload: runReload
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
void runReload();
|
|
125
|
+
}
|
|
50
126
|
}
|
|
51
127
|
function makeZeroRecovery(deps) {
|
|
52
128
|
return {
|
|
@@ -70,6 +146,114 @@ function logToConsole(level, context) {
|
|
|
70
146
|
var method = level === "debug" ? "debug" : level;
|
|
71
147
|
console[method](...(prefix ? [prefix] : []), ...args);
|
|
72
148
|
}
|
|
149
|
+
function logArgText(arg) {
|
|
150
|
+
if (typeof arg === "string") return arg;
|
|
151
|
+
if (_instanceof(arg, Error)) return `${arg.name} ${arg.message} ${arg.stack || ""}`;
|
|
152
|
+
if (arg && (typeof arg === "undefined" ? "undefined" : _type_of(arg)) === "object") {
|
|
153
|
+
var message = "message" in arg ? arg.message : void 0;
|
|
154
|
+
var name = "name" in arg ? arg.name : void 0;
|
|
155
|
+
if (typeof message === "string") return `${typeof name === "string" ? name : ""} ${message}`;
|
|
156
|
+
}
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
function isBenignStoreClosedLog(text) {
|
|
160
|
+
if (/Mutator\s+".*"\s+error on server/i.test(text)) return true;
|
|
161
|
+
if (/Mutator\s+".*"\s+app error on client/i.test(text)) return true;
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
function classifyMutationDesync(text) {
|
|
165
|
+
if (text.includes("sent mutation ID") && text.includes("but expected")) {
|
|
166
|
+
return {
|
|
167
|
+
reason: "mutation-desync",
|
|
168
|
+
reasonKey: "mutation-desync",
|
|
169
|
+
message: "mutation id desync"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (text.includes("oooMutation") || text.includes("Server reported an out-of-order mutation")) {
|
|
173
|
+
return {
|
|
174
|
+
reason: "mutation-desync",
|
|
175
|
+
reasonKey: "mutation-desync",
|
|
176
|
+
message: "out-of-order mutation"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (text.includes("already processed")) {
|
|
180
|
+
return {
|
|
181
|
+
reason: "mutation-desync",
|
|
182
|
+
reasonKey: "mutation-desync",
|
|
183
|
+
message: "mutation already processed"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (text.includes("InvalidConnectionRequestLastMutationID")) {
|
|
187
|
+
return {
|
|
188
|
+
reason: "mutation-desync",
|
|
189
|
+
reasonKey: "mutation-desync",
|
|
190
|
+
message: "invalid connection last mutation id"
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (text.includes("InvalidConnectionRequestBaseCookie")) {
|
|
194
|
+
return {
|
|
195
|
+
reason: "connection-cookie-invalid",
|
|
196
|
+
reasonKey: "connection-cookie-invalid",
|
|
197
|
+
message: "invalid connection base cookie"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (text.includes("ClientNotFound") || text.includes("Client not found")) {
|
|
201
|
+
return {
|
|
202
|
+
reason: "client-not-found",
|
|
203
|
+
reasonKey: "client-not-found",
|
|
204
|
+
message: "client not found"
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (text.includes("connection userID mismatch")) {
|
|
208
|
+
return {
|
|
209
|
+
reason: "connection-userid-mismatch",
|
|
210
|
+
reasonKey: "connection-userid-mismatch",
|
|
211
|
+
message: "connection user id mismatch"
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
function classifyZeroRecoveryLog(level, args) {
|
|
217
|
+
var nowMs = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : Date.now();
|
|
218
|
+
if (level !== "error") return void 0;
|
|
219
|
+
var text = args.map(logArgText).join(" ");
|
|
220
|
+
if (text.includes(LOCAL_STORE_LOST)) {
|
|
221
|
+
return {
|
|
222
|
+
reason: "indexeddb-not-found",
|
|
223
|
+
reasonKey: "local-store",
|
|
224
|
+
message: "local store lost",
|
|
225
|
+
dropLocalState: true
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (text.includes(SQLITE_ERROR_NAME) && text.includes(SQLITE_STATEMENT_FINALIZED)) {
|
|
229
|
+
return {
|
|
230
|
+
reason: "sqlite-statement-finalized",
|
|
231
|
+
reasonKey: "sqlite-statement-finalized",
|
|
232
|
+
message: "sqlite statement finalized",
|
|
233
|
+
dropLocalState: true
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
var desync = classifyMutationDesync(text);
|
|
237
|
+
if (desync) {
|
|
238
|
+
return {
|
|
239
|
+
...desync,
|
|
240
|
+
dropLocalState: true
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (text.includes(STORE_CLOSED) && !isBenignStoreClosedLog(text)) {
|
|
244
|
+
var prevMs = lastStoreClosedAtMs;
|
|
245
|
+
lastStoreClosedAtMs = nowMs;
|
|
246
|
+
if (prevMs > 0 && nowMs - prevMs >= STORE_CLOSED_REPEAT_MIN_MS && nowMs - prevMs <= STORE_CLOSED_REPEAT_MAX_MS) {
|
|
247
|
+
return {
|
|
248
|
+
reason: "store-closed-repeat",
|
|
249
|
+
reasonKey: "store-closed-repeat",
|
|
250
|
+
message: "local store closed repeatedly",
|
|
251
|
+
dropLocalState: true
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
73
257
|
function composeRecoveryLogSink(deps, consumerLogSink) {
|
|
74
258
|
var consumerFlush = consumerLogSink === null || consumerLogSink === void 0 ? void 0 : consumerLogSink.flush;
|
|
75
259
|
return {
|
|
@@ -78,13 +262,13 @@ function composeRecoveryLogSink(deps, consumerLogSink) {
|
|
|
78
262
|
args[_key - 2] = arguments[_key];
|
|
79
263
|
}
|
|
80
264
|
if (consumerLogSink) consumerLogSink.log(level, context, ...args);else logToConsole(level, context, ...args);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
recover(deps, "local-store", "local store lost", true);
|
|
265
|
+
var recovery = classifyZeroRecoveryLog(level, args);
|
|
266
|
+
if (!recovery) return;
|
|
267
|
+
if (deps.benignLogFilter) {
|
|
268
|
+
var text = args.map(logArgText).join(" ");
|
|
269
|
+
if (deps.benignLogFilter(text)) return;
|
|
87
270
|
}
|
|
271
|
+
recover(deps, recovery.reasonKey, recovery.message, recovery.dropLocalState);
|
|
88
272
|
},
|
|
89
273
|
// call through the consumer sink so a class-based sink keeps its `this`,
|
|
90
274
|
// rather than handing Zero a detached method reference.
|
|
@@ -93,9 +277,19 @@ function composeRecoveryLogSink(deps, consumerLogSink) {
|
|
|
93
277
|
} : void 0
|
|
94
278
|
};
|
|
95
279
|
}
|
|
280
|
+
function isRecoverableZeroStalePokeMessage(message) {
|
|
281
|
+
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");
|
|
282
|
+
}
|
|
96
283
|
function resetRecoveryStateForTests() {
|
|
97
284
|
reloadScheduled = false;
|
|
285
|
+
reloadInProgress = false;
|
|
286
|
+
if (reloadLatchTimer) {
|
|
287
|
+
clearTimeout(reloadLatchTimer);
|
|
288
|
+
reloadLatchTimer = void 0;
|
|
289
|
+
}
|
|
98
290
|
pendingDeletes.length = 0;
|
|
291
|
+
lastStoreClosedAtMs = 0;
|
|
292
|
+
inMemoryGuard.clear();
|
|
99
293
|
}
|
|
100
|
-
export { composeRecoveryLogSink, makeZeroRecovery, resetRecoveryStateForTests };
|
|
294
|
+
export { classifyZeroRecoveryLog, composeRecoveryLogSink, isRecoverableZeroStalePokeMessage, makeZeroRecovery, resetRecoveryStateForTests };
|
|
101
295
|
//# sourceMappingURL=recoverZeroClient.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["
|
|
1
|
+
{"version":3,"names":["_instanceof","left","right","Symbol","hasInstance","_type_of","obj","constructor","RECOVER_GUARD_MS","RELOAD_LATCH_TIMEOUT_MS","LOCAL_STORE_LOST","SQLITE_ERROR_NAME","SQLITE_STATEMENT_FINALIZED","STORE_CLOSED","STORE_CLOSED_REPEAT_MIN_MS","STORE_CLOSED_REPEAT_MAX_MS","reloadScheduled","reloadInProgress","reloadLatchTimer","pendingDeletes","inMemoryGuard","Map","defaultGuardStorage","window","sessionStorage","e","recoveryGuardOpen","reasonKey","guardStorage","key","now","Date","_inMemoryGuard_get","memLast","get","storage","rawLast","getItem","last","Number","setItem","String","set","lastStoreClosedAtMs","armReloadLatchTimeout","setTimeout","clearTimeout","unref","disarmReloadLatchTimeout","performReload","deps","Promise","resolve","_deps_reload","doReload","reload","_globalThis_location_reload","_globalThis_location","globalThis","location","call","then","allSettled","splice","map","run","_deps_beforeReload","beforeReload","catch","recover","message","dropLocalState","push","deleteLocalState","console","error","zeroEvents","emit","type","reason","warn","runReload","scheduleReload","makeZeroRecovery","onUpdateNeeded","onClientStateNotFound","logToConsole","level","context","_len","arguments","length","args","Array","_key","prefix","Object","entries","param","value","join","method","logArgText","arg","Error","name","stack","isBenignStoreClosedLog","text","test","classifyMutationDesync","includes","classifyZeroRecoveryLog","nowMs","desync","prevMs"],"sources":["../../../src/helpers/recoverZeroClient.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEmitter } from "@take-out/helpers";
|
|
2
2
|
import { UpdateNeededReasonType } from "@rocicorp/zero";
|
|
3
3
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
|
-
import { composeRecoveryLogSink, makeZeroRecovery, resetRecoveryStateForTests } from "./recoverZeroClient.mjs";
|
|
4
|
+
import { classifyZeroRecoveryLog, composeRecoveryLogSink, isRecoverableZeroStalePokeMessage, makeZeroRecovery, resetRecoveryStateForTests } from "./recoverZeroClient.mjs";
|
|
5
5
|
let emitterSeq = 0;
|
|
6
6
|
function setup() {
|
|
7
7
|
const events = [];
|
|
@@ -150,6 +150,45 @@ describe("zero recovery", () => {
|
|
|
150
150
|
expect(deleteLocalState).toHaveBeenCalledTimes(1);
|
|
151
151
|
expect(reload).toHaveBeenCalledTimes(1);
|
|
152
152
|
});
|
|
153
|
+
test("logSink recovers on native sqlite finalized-statement local-store loss", async () => {
|
|
154
|
+
const {
|
|
155
|
+
deps,
|
|
156
|
+
deleteLocalState,
|
|
157
|
+
reload
|
|
158
|
+
} = setup();
|
|
159
|
+
const sqliteError = new Error("This statement has been finalized");
|
|
160
|
+
sqliteError.name = "SqliteError";
|
|
161
|
+
const sink = composeRecoveryLogSink(deps);
|
|
162
|
+
sink.log("error", {
|
|
163
|
+
bgIntervalProcess: "Heartbeat"
|
|
164
|
+
}, "Error running.", sqliteError);
|
|
165
|
+
await flush();
|
|
166
|
+
expect(deleteLocalState).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
test("logSink recovers on repeated store-closed local-store loss", async () => {
|
|
170
|
+
const nowSpy = vi.spyOn(Date, "now");
|
|
171
|
+
const {
|
|
172
|
+
deps,
|
|
173
|
+
deleteLocalState,
|
|
174
|
+
reload
|
|
175
|
+
} = setup();
|
|
176
|
+
try {
|
|
177
|
+
const sink = composeRecoveryLogSink(deps);
|
|
178
|
+
nowSpy.mockReturnValue(1e4);
|
|
179
|
+
sink.log("error", void 0, "Failed to connect.", new Error("Store is closed"));
|
|
180
|
+
await flush();
|
|
181
|
+
expect(deleteLocalState).not.toHaveBeenCalled();
|
|
182
|
+
expect(reload).not.toHaveBeenCalled();
|
|
183
|
+
nowSpy.mockReturnValue(13e3);
|
|
184
|
+
sink.log("error", void 0, "Failed to connect.", new Error("Store is closed"));
|
|
185
|
+
await flush();
|
|
186
|
+
expect(deleteLocalState).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
188
|
+
} finally {
|
|
189
|
+
nowSpy.mockRestore();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
153
192
|
test("logSink with no consumer preserves console output and still watches", async () => {
|
|
154
193
|
const {
|
|
155
194
|
deps,
|
|
@@ -198,6 +237,27 @@ describe("zero recovery", () => {
|
|
|
198
237
|
expect(deleteLocalState).not.toHaveBeenCalled();
|
|
199
238
|
expect(reload).not.toHaveBeenCalled();
|
|
200
239
|
});
|
|
240
|
+
test("recovery log classification is narrow", () => {
|
|
241
|
+
const sqliteError = new Error("This statement has been finalized");
|
|
242
|
+
sqliteError.name = "SqliteError";
|
|
243
|
+
expect(classifyZeroRecoveryLog("error", ["Error running.", sqliteError])).toMatchObject({
|
|
244
|
+
reason: "sqlite-statement-finalized",
|
|
245
|
+
dropLocalState: true
|
|
246
|
+
});
|
|
247
|
+
expect(classifyZeroRecoveryLog("error", [{
|
|
248
|
+
name: "SqliteError",
|
|
249
|
+
message: "database is locked"
|
|
250
|
+
}])).toBeUndefined();
|
|
251
|
+
expect(classifyZeroRecoveryLog("warn", ["Expected IndexedDB not found"])).toBeUndefined();
|
|
252
|
+
expect(classifyZeroRecoveryLog("error", ["Store is closed"], 1e4)).toBeUndefined();
|
|
253
|
+
expect(classifyZeroRecoveryLog("error", ["Store is closed"], 11e3)).toBeUndefined();
|
|
254
|
+
expect(classifyZeroRecoveryLog("error", ["Store is closed"], 14e3)).toMatchObject({
|
|
255
|
+
reason: "store-closed-repeat",
|
|
256
|
+
dropLocalState: true
|
|
257
|
+
});
|
|
258
|
+
resetRecoveryStateForTests();
|
|
259
|
+
expect(classifyZeroRecoveryLog("error", ['Mutator "send" error on server', "Store is closed"], 2e4)).toBeUndefined();
|
|
260
|
+
});
|
|
201
261
|
test("default reload path is a safe no-op when location is absent", async () => {
|
|
202
262
|
const events = [];
|
|
203
263
|
const zeroEvents = createEmitter(`test-recover-${emitterSeq++}`, null);
|
|
@@ -229,5 +289,186 @@ describe("zero recovery", () => {
|
|
|
229
289
|
reason: "client state not found"
|
|
230
290
|
});
|
|
231
291
|
});
|
|
292
|
+
test("scheduleReload defers the reload AND the store delete until performReload runs", async () => {
|
|
293
|
+
const {
|
|
294
|
+
deps,
|
|
295
|
+
deleteLocalState,
|
|
296
|
+
reload
|
|
297
|
+
} = setup();
|
|
298
|
+
let captured;
|
|
299
|
+
deps.scheduleReload = ctx => {
|
|
300
|
+
captured = ctx;
|
|
301
|
+
};
|
|
302
|
+
makeZeroRecovery(deps).onClientStateNotFound();
|
|
303
|
+
await flush();
|
|
304
|
+
expect(reload).not.toHaveBeenCalled();
|
|
305
|
+
expect(deleteLocalState).not.toHaveBeenCalled();
|
|
306
|
+
expect(captured).toMatchObject({
|
|
307
|
+
reasonKey: "client-state-not-found",
|
|
308
|
+
dropLocalState: true
|
|
309
|
+
});
|
|
310
|
+
await captured.performReload();
|
|
311
|
+
await flush();
|
|
312
|
+
expect(deleteLocalState).toHaveBeenCalledTimes(1);
|
|
313
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
314
|
+
});
|
|
315
|
+
test("scheduleReload that reloads directly skips the delete and beforeReload", async () => {
|
|
316
|
+
const {
|
|
317
|
+
deps,
|
|
318
|
+
deleteLocalState,
|
|
319
|
+
reload
|
|
320
|
+
} = setup();
|
|
321
|
+
const nativeReload = vi.fn();
|
|
322
|
+
const beforeReload = vi.fn(async () => {});
|
|
323
|
+
deps.beforeReload = beforeReload;
|
|
324
|
+
deps.scheduleReload = () => {
|
|
325
|
+
nativeReload();
|
|
326
|
+
};
|
|
327
|
+
makeZeroRecovery(deps).onClientStateNotFound();
|
|
328
|
+
await flush();
|
|
329
|
+
expect(nativeReload).toHaveBeenCalledTimes(1);
|
|
330
|
+
expect(deleteLocalState).not.toHaveBeenCalled();
|
|
331
|
+
expect(beforeReload).not.toHaveBeenCalled();
|
|
332
|
+
expect(reload).not.toHaveBeenCalled();
|
|
333
|
+
});
|
|
334
|
+
test("scheduleReload performReload is idempotent (one reload even if called twice)", async () => {
|
|
335
|
+
const {
|
|
336
|
+
deps,
|
|
337
|
+
deleteLocalState,
|
|
338
|
+
reload
|
|
339
|
+
} = setup();
|
|
340
|
+
let captured;
|
|
341
|
+
deps.scheduleReload = ctx => {
|
|
342
|
+
captured = ctx;
|
|
343
|
+
};
|
|
344
|
+
makeZeroRecovery(deps).onClientStateNotFound();
|
|
345
|
+
await flush();
|
|
346
|
+
await captured.performReload();
|
|
347
|
+
await captured.performReload();
|
|
348
|
+
await flush();
|
|
349
|
+
expect(deleteLocalState).toHaveBeenCalledTimes(1);
|
|
350
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
351
|
+
});
|
|
352
|
+
test("scheduleReload deferred PAST the latch timeout still deletes and reloads exactly once", async () => {
|
|
353
|
+
vi.useFakeTimers();
|
|
354
|
+
try {
|
|
355
|
+
const {
|
|
356
|
+
deps,
|
|
357
|
+
deleteLocalState,
|
|
358
|
+
reload
|
|
359
|
+
} = setup();
|
|
360
|
+
let captured;
|
|
361
|
+
deps.scheduleReload = ctx => {
|
|
362
|
+
captured = ctx;
|
|
363
|
+
};
|
|
364
|
+
makeZeroRecovery(deps).onClientStateNotFound();
|
|
365
|
+
expect(captured).toBeDefined();
|
|
366
|
+
await vi.advanceTimersByTimeAsync(2e4);
|
|
367
|
+
expect(deleteLocalState).not.toHaveBeenCalled();
|
|
368
|
+
expect(reload).not.toHaveBeenCalled();
|
|
369
|
+
await captured.performReload();
|
|
370
|
+
expect(deleteLocalState).toHaveBeenCalledTimes(1);
|
|
371
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
372
|
+
} finally {
|
|
373
|
+
vi.useRealTimers();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
test("a slow beforeReload holds the latch through the timeout window (no double reload)", async () => {
|
|
377
|
+
vi.useFakeTimers();
|
|
378
|
+
try {
|
|
379
|
+
const {
|
|
380
|
+
deps,
|
|
381
|
+
reload
|
|
382
|
+
} = setup();
|
|
383
|
+
let releaseBeforeReload = () => {};
|
|
384
|
+
deps.beforeReload = () => new Promise(resolve => {
|
|
385
|
+
releaseBeforeReload = resolve;
|
|
386
|
+
});
|
|
387
|
+
const recovery = makeZeroRecovery(deps);
|
|
388
|
+
recovery.onClientStateNotFound();
|
|
389
|
+
await vi.advanceTimersByTimeAsync(2e4);
|
|
390
|
+
recovery.onClientStateNotFound();
|
|
391
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
392
|
+
expect(reload).not.toHaveBeenCalled();
|
|
393
|
+
releaseBeforeReload();
|
|
394
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
395
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
396
|
+
} finally {
|
|
397
|
+
vi.useRealTimers();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
test("injectable guardStorage gives cross-reload loop protection (Hermes has no sessionStorage)", async () => {
|
|
401
|
+
const store = /* @__PURE__ */new Map();
|
|
402
|
+
const {
|
|
403
|
+
deps,
|
|
404
|
+
reload,
|
|
405
|
+
events
|
|
406
|
+
} = setup();
|
|
407
|
+
deps.guardStorage = {
|
|
408
|
+
getItem: key => store.get(key) ?? null,
|
|
409
|
+
setItem: (key, value) => {
|
|
410
|
+
store.set(key, value);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const recovery = makeZeroRecovery(deps);
|
|
414
|
+
recovery.onClientStateNotFound();
|
|
415
|
+
await flush();
|
|
416
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
417
|
+
resetRecoveryStateForTests();
|
|
418
|
+
recovery.onClientStateNotFound();
|
|
419
|
+
await flush();
|
|
420
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
421
|
+
expect(events.some(event => event.type === "fatal")).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
test("a guardStorage that throws never crashes recovery (in-memory is the floor)", async () => {
|
|
424
|
+
const {
|
|
425
|
+
deps,
|
|
426
|
+
reload
|
|
427
|
+
} = setup();
|
|
428
|
+
deps.guardStorage = {
|
|
429
|
+
getItem: () => {
|
|
430
|
+
throw new Error("no storage on this platform");
|
|
431
|
+
},
|
|
432
|
+
setItem: () => {
|
|
433
|
+
throw new Error("no storage on this platform");
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
makeZeroRecovery(deps).onClientStateNotFound();
|
|
437
|
+
await flush();
|
|
438
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
439
|
+
});
|
|
440
|
+
test("classifier recognizes the mutation / connection desync class", () => {
|
|
441
|
+
const cases = [[["sent mutation ID 5 but expected 4"], "mutation-desync"], [["oooMutation detected"], "mutation-desync"], [["Server reported an out-of-order mutation"], "mutation-desync"], [["Ignoring mutation 3, already processed. Expected: 4"], "mutation-desync"], [["InvalidConnectionRequestLastMutationID"], "mutation-desync"], [["InvalidConnectionRequestBaseCookie"], "connection-cookie-invalid"], [["ClientNotFound: client gone"], "client-not-found"], [["connection userID mismatch"], "connection-userid-mismatch"]];
|
|
442
|
+
for (const [args, reason] of cases) {
|
|
443
|
+
expect(classifyZeroRecoveryLog("error", args)).toMatchObject({
|
|
444
|
+
reason,
|
|
445
|
+
dropLocalState: true
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
expect(classifyZeroRecoveryLog("error", ["consecutive server-ack timeouts"])).toBeUndefined();
|
|
449
|
+
expect(classifyZeroRecoveryLog("error", ["Connection attempt timed out after 10 seconds"])).toBeUndefined();
|
|
450
|
+
expect(classifyZeroRecoveryLog("warn", ["ClientNotFound"])).toBeUndefined();
|
|
451
|
+
});
|
|
452
|
+
test("benignLogFilter suppresses recovery for a matching classified log", async () => {
|
|
453
|
+
const {
|
|
454
|
+
deps,
|
|
455
|
+
deleteLocalState,
|
|
456
|
+
reload
|
|
457
|
+
} = setup();
|
|
458
|
+
deps.benignLogFilter = message => message.includes("ClientNotFound");
|
|
459
|
+
const sink = composeRecoveryLogSink(deps);
|
|
460
|
+
sink.log("error", void 0, "ClientNotFound: gone");
|
|
461
|
+
await flush();
|
|
462
|
+
expect(deleteLocalState).not.toHaveBeenCalled();
|
|
463
|
+
expect(reload).not.toHaveBeenCalled();
|
|
464
|
+
sink.log("error", void 0, "sent mutation ID 5 but expected 4");
|
|
465
|
+
await flush();
|
|
466
|
+
expect(reload).toHaveBeenCalledTimes(1);
|
|
467
|
+
});
|
|
468
|
+
test("isRecoverableZeroStalePokeMessage matches only the stale-cookie signatures", () => {
|
|
469
|
+
expect(isRecoverableZeroStalePokeMessage("Server returned unexpected base cookie during sync")).toBe(true);
|
|
470
|
+
expect(isRecoverableZeroStalePokeMessage("Received cookie 5 is < than last snapshot cookie 9, ignoring client view")).toBe(true);
|
|
471
|
+
expect(isRecoverableZeroStalePokeMessage("client state not found")).toBe(false);
|
|
472
|
+
});
|
|
232
473
|
});
|
|
233
474
|
//# sourceMappingURL=recoverZeroClient.test.mjs.map
|