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.
Files changed (82) hide show
  1. package/dist/cjs/createZeroClient.cjs +91 -12
  2. package/dist/cjs/createZeroClient.connection.test.cjs +163 -0
  3. package/dist/cjs/createZeroClient.connection.test.native.js +242 -0
  4. package/dist/cjs/createZeroClient.connection.test.native.js.map +1 -0
  5. package/dist/cjs/createZeroClient.native.js +99 -12
  6. package/dist/cjs/createZeroClient.native.js.map +1 -1
  7. package/dist/cjs/createZeroClient.recovery.test.cjs +52 -0
  8. package/dist/cjs/createZeroClient.recovery.test.native.js +52 -0
  9. package/dist/cjs/createZeroClient.recovery.test.native.js.map +1 -1
  10. package/dist/cjs/helpers/recoverZeroClient.cjs +197 -14
  11. package/dist/cjs/helpers/recoverZeroClient.native.js +220 -24
  12. package/dist/cjs/helpers/recoverZeroClient.native.js.map +1 -1
  13. package/dist/cjs/helpers/recoverZeroClient.test.cjs +241 -0
  14. package/dist/cjs/helpers/recoverZeroClient.test.native.js +269 -0
  15. package/dist/cjs/helpers/recoverZeroClient.test.native.js.map +1 -1
  16. package/dist/cjs/index.cjs +2 -0
  17. package/dist/cjs/index.native.js +2 -0
  18. package/dist/cjs/index.native.js.map +1 -1
  19. package/dist/cjs/multi.cjs +13 -0
  20. package/dist/cjs/multi.native.js +35 -0
  21. package/dist/cjs/multi.native.js.map +1 -1
  22. package/dist/cjs/multiPartition.test.cjs +42 -0
  23. package/dist/cjs/multiPartition.test.native.js +51 -0
  24. package/dist/cjs/multiPartition.test.native.js.map +1 -0
  25. package/dist/esm/createZeroClient.connection.test.mjs +164 -0
  26. package/dist/esm/createZeroClient.connection.test.mjs.map +1 -0
  27. package/dist/esm/createZeroClient.connection.test.native.js +240 -0
  28. package/dist/esm/createZeroClient.connection.test.native.js.map +1 -0
  29. package/dist/esm/createZeroClient.mjs +92 -13
  30. package/dist/esm/createZeroClient.mjs.map +1 -1
  31. package/dist/esm/createZeroClient.native.js +100 -13
  32. package/dist/esm/createZeroClient.native.js.map +1 -1
  33. package/dist/esm/createZeroClient.recovery.test.mjs +52 -0
  34. package/dist/esm/createZeroClient.recovery.test.mjs.map +1 -1
  35. package/dist/esm/createZeroClient.recovery.test.native.js +52 -0
  36. package/dist/esm/createZeroClient.recovery.test.native.js.map +1 -1
  37. package/dist/esm/helpers/recoverZeroClient.mjs +196 -15
  38. package/dist/esm/helpers/recoverZeroClient.mjs.map +1 -1
  39. package/dist/esm/helpers/recoverZeroClient.native.js +219 -25
  40. package/dist/esm/helpers/recoverZeroClient.native.js.map +1 -1
  41. package/dist/esm/helpers/recoverZeroClient.test.mjs +242 -1
  42. package/dist/esm/helpers/recoverZeroClient.test.mjs.map +1 -1
  43. package/dist/esm/helpers/recoverZeroClient.test.native.js +270 -1
  44. package/dist/esm/helpers/recoverZeroClient.test.native.js.map +1 -1
  45. package/dist/esm/index.js +2 -2
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/index.mjs +2 -2
  48. package/dist/esm/index.mjs.map +1 -1
  49. package/dist/esm/index.native.js +2 -2
  50. package/dist/esm/index.native.js.map +1 -1
  51. package/dist/esm/multi.mjs +13 -1
  52. package/dist/esm/multi.mjs.map +1 -1
  53. package/dist/esm/multi.native.js +35 -1
  54. package/dist/esm/multi.native.js.map +1 -1
  55. package/dist/esm/multiPartition.test.mjs +43 -0
  56. package/dist/esm/multiPartition.test.mjs.map +1 -0
  57. package/dist/esm/multiPartition.test.native.js +49 -0
  58. package/dist/esm/multiPartition.test.native.js.map +1 -0
  59. package/lint/ssr-guards.js +164 -0
  60. package/package.json +2 -2
  61. package/readme.md +126 -0
  62. package/src/createZeroClient.connection.test.tsx +162 -0
  63. package/src/createZeroClient.recovery.test.tsx +68 -0
  64. package/src/createZeroClient.tsx +159 -13
  65. package/src/helpers/recoverZeroClient.test.ts +270 -1
  66. package/src/helpers/recoverZeroClient.ts +347 -50
  67. package/src/index.ts +6 -0
  68. package/src/multi.ts +33 -0
  69. package/src/multiPartition.test.ts +38 -0
  70. package/types/createZeroClient.connection.test.d.ts +5 -0
  71. package/types/createZeroClient.connection.test.d.ts.map +1 -0
  72. package/types/createZeroClient.d.ts +17 -0
  73. package/types/createZeroClient.d.ts.map +1 -1
  74. package/types/createZeroServer.d.ts +4 -4
  75. package/types/helpers/recoverZeroClient.d.ts +23 -1
  76. package/types/helpers/recoverZeroClient.d.ts.map +1 -1
  77. package/types/index.d.ts +1 -1
  78. package/types/index.d.ts.map +1 -1
  79. package/types/multi.d.ts +1 -0
  80. package/types/multi.d.ts.map +1 -1
  81. package/types/multiPartition.test.d.ts +2 -0
  82. 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
- function recoveryGuardOpen(reasonKey) {
25
+ var inMemoryGuard = /* @__PURE__ */new Map();
26
+ function defaultGuardStorage() {
11
27
  try {
12
- var key = `on-zero-recover-${reasonKey}`;
13
- var last = Number(window.sessionStorage.getItem(key) || 0);
14
- if (Date.now() - last < RECOVER_GUARD_MS) return false;
15
- window.sessionStorage.setItem(key, String(Date.now()));
16
- } catch (e) {}
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(Promise.resolve().then(deps.deleteLocalState).catch(function () {}));
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 _deps_reload;
40
- var doReload = (_deps_reload = deps.reload) !== null && _deps_reload !== void 0 ? _deps_reload : function () {
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
- Promise.resolve().then(function () {
45
- return Promise.allSettled(pendingDeletes);
46
- }).then(function () {
47
- var _deps_beforeReload;
48
- return (_deps_beforeReload = deps.beforeReload) === null || _deps_beforeReload === void 0 ? void 0 : _deps_beforeReload.call(deps);
49
- }).catch(function () {}).finally(doReload);
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
- if (level !== "error") return;
82
- var text = args.map(function (arg) {
83
- return typeof arg === "string" ? arg : arg && (typeof arg === "undefined" ? "undefined" : _type_of(arg)) === "object" && "message" in arg ? String(arg.message) : "";
84
- }).join(" ");
85
- if (text.includes(LOCAL_STORE_LOST)) {
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":["_type_of","obj","Symbol","constructor","RECOVER_GUARD_MS","LOCAL_STORE_LOST","reloadScheduled","pendingDeletes","recoveryGuardOpen","reasonKey","key","last","Number","window","sessionStorage","getItem","Date","now","setItem","String","e","recover","deps","message","dropLocalState","push","Promise","resolve","then","deleteLocalState","catch","console","error","zeroEvents","emit","type","reason","warn","_deps_reload","doReload","reload","_globalThis_location_reload","_globalThis_location","globalThis","location","call","allSettled","_deps_beforeReload","beforeReload","finally","makeZeroRecovery","onUpdateNeeded","onClientStateNotFound","logToConsole","level","context","_len","arguments","length","args","Array","_key","prefix","Object","entries","map","param","value","join","method","composeRecoveryLogSink","consumerLogSink","consumerFlush","flush"],"sources":["../../../src/helpers/recoverZeroClient.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
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