on-zero 0.6.3 → 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 +140 -14
  11. package/dist/cjs/helpers/recoverZeroClient.native.js +155 -22
  12. package/dist/cjs/helpers/recoverZeroClient.native.js.map +1 -1
  13. package/dist/cjs/helpers/recoverZeroClient.test.cjs +181 -0
  14. package/dist/cjs/helpers/recoverZeroClient.test.native.js +209 -0
  15. package/dist/cjs/helpers/recoverZeroClient.test.native.js.map +1 -1
  16. package/dist/cjs/index.cjs +1 -0
  17. package/dist/cjs/index.native.js +1 -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 +140 -15
  38. package/dist/esm/helpers/recoverZeroClient.mjs.map +1 -1
  39. package/dist/esm/helpers/recoverZeroClient.native.js +155 -23
  40. package/dist/esm/helpers/recoverZeroClient.native.js.map +1 -1
  41. package/dist/esm/helpers/recoverZeroClient.test.mjs +182 -1
  42. package/dist/esm/helpers/recoverZeroClient.test.mjs.map +1 -1
  43. package/dist/esm/helpers/recoverZeroClient.test.native.js +210 -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 +204 -1
  66. package/src/helpers/recoverZeroClient.ts +263 -42
  67. package/src/index.ts +3 -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 +15 -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
@@ -7,6 +7,14 @@ import type { Context, LogLevel, LogSink } from '@rocicorp/logger'
7
7
  // surfaced as fatal instead of reload-storming on something the reload can't fix.
8
8
  const RECOVER_GUARD_MS = 60_000
9
9
 
10
+ // if the scheduled reload never actually takes the page down (a consumer's
11
+ // scheduleReload deferred it, a native reload no-oped, or reload() threw) the
12
+ // module latch would stay set and kill EVERY later recovery for the page's
13
+ // life. time it out so recovery can try again — a real reload tears down the
14
+ // context long before this fires, so it only matters when the reload didn't
15
+ // land.
16
+ const RELOAD_LATCH_TIMEOUT_MS = 15_000
17
+
10
18
  // the fatal local-store-loss signature Zero logs when the IndexedDB it expects
11
19
  // is gone (evicted, or deleted by another tab).
12
20
  const LOCAL_STORE_LOST = 'Expected IndexedDB not found'
@@ -20,6 +28,10 @@ export type ZeroRecoveryLogReason =
20
28
  | 'indexeddb-not-found'
21
29
  | 'sqlite-statement-finalized'
22
30
  | 'store-closed-repeat'
31
+ | 'mutation-desync'
32
+ | 'connection-cookie-invalid'
33
+ | 'client-not-found'
34
+ | 'connection-userid-mismatch'
23
35
 
24
36
  export type ZeroRecoveryLogClassification = {
25
37
  reason: ZeroRecoveryLogReason
@@ -28,6 +40,27 @@ export type ZeroRecoveryLogClassification = {
28
40
  dropLocalState: boolean
29
41
  }
30
42
 
43
+ // a minimal synchronous key/value store the cross-reload guard persists into.
44
+ // web defaults to sessionStorage; native (Hermes) has none, so a consumer can
45
+ // inject an MMKV/sqlite-backed store to get cross-reload loop protection — the
46
+ // in-memory guard already covers within-a-page-load.
47
+ export type RecoveryGuardStorage = {
48
+ getItem: (key: string) => string | null
49
+ setItem: (key: string, value: string) => void
50
+ }
51
+
52
+ // what a scheduleReload consumer receives so it can gate/annotate the reload
53
+ // (soot: IDE-active gate + countdown toast + background-mutation fence; mobile:
54
+ // expo-updates reload) while still driving the SAME deletes-then-reload work.
55
+ export type ScheduleReloadContext = {
56
+ reason: string
57
+ reasonKey: string
58
+ dropLocalState: boolean
59
+ // deletes every affected instance's local store, awaits beforeReload, then
60
+ // reloads. idempotent — safe to call once the consumer decides to proceed.
61
+ performReload: () => Promise<void>
62
+ }
63
+
31
64
  export type ZeroRecoveryDeps = {
32
65
  // delete THIS client's local persistent state (its own scoped IDB). pass the
33
66
  // instance's `.delete`. recovery is per-instance, so a combined client drops
@@ -38,31 +71,74 @@ export type ZeroRecoveryDeps = {
38
71
  // awaited before the reload — e.g. wait for the dev origin to come back so the
39
72
  // reload doesn't land on a restarting server. optional.
40
73
  beforeReload?: () => Promise<void>
74
+ // take over WHEN/HOW the recovery reload happens. default: immediate guarded
75
+ // reload. the consumer decides (defer until safe, show a countdown, reload
76
+ // natively) but still calls ctx.performReload to run the real work.
77
+ scheduleReload?: (ctx: ScheduleReloadContext) => void
78
+ // cross-reload guard backing store (defaults to sessionStorage on web). inject
79
+ // a native KV so Hermes gets real cross-reload loop protection.
80
+ guardStorage?: RecoveryGuardStorage
81
+ // called with the joined log text for a classified recovery signature; return
82
+ // true to treat it as benign and NOT recover (soot's cf cold-boot timeout).
83
+ benignLogFilter?: (message: string) => boolean
41
84
  // injectable for tests; defaults to a real page reload.
42
85
  reload?: () => void
43
86
  }
44
87
 
45
88
  // one reload per page-load. the local-state deletes that must precede it are
46
89
  // collected here so EVERY affected instance (a combined client has several) drops
47
- // its own store before we reload — not just whichever instance fired first.
90
+ // its own store before we reload — not just whichever instance fired first. they
91
+ // are thunks (not started promises) so a deferred reload can't leave the app
92
+ // running on an already-deleted store: the deletes run only when the reload
93
+ // actually proceeds.
48
94
  let reloadScheduled = false
49
- const pendingDeletes: Array<Promise<unknown>> = []
95
+ let reloadInProgress = false
96
+ let reloadLatchTimer: ReturnType<typeof setTimeout> | undefined
97
+ const pendingDeletes: Array<() => Promise<unknown>> = []
50
98
 
51
- // cross-page-load, per-reason guard: distinct keys mean a NewClientGroup recovery
52
- // never suppresses a later SchemaVersionNotSupported one. it survives the reload
53
- // (sessionStorage), so a reason that reloaded then immediately re-fires is caught
54
- // as a genuine fatal instead of reload-storming across loads.
55
- function recoveryGuardOpen(reasonKey: string): boolean {
99
+ // within-a-page-load per-reason guard: real loop protection everywhere,
100
+ // including Hermes (no storage needed). a reload wipes it, which is why the
101
+ // injectable storage below survives across reloads.
102
+ const inMemoryGuard = new Map<string, number>()
103
+
104
+ function defaultGuardStorage(): RecoveryGuardStorage | undefined {
56
105
  try {
57
- const key = `on-zero-recover-${reasonKey}`
58
- const rawLast = window.sessionStorage.getItem(key)
59
- const now = Date.now()
60
- const last = rawLast ? Number(rawLast) : 0
61
- if (last > 0 && now - last < RECOVER_GUARD_MS) return false
62
- window.sessionStorage.setItem(key, String(now))
106
+ if (typeof window === 'undefined' || !window.sessionStorage) return undefined
107
+ return window.sessionStorage
63
108
  } catch {
64
- // no sessionStorage (private mode) fall back to the in-memory latch.
109
+ // sessionStorage access throws in some sandboxes / Hermes no web store.
110
+ return undefined
65
111
  }
112
+ }
113
+
114
+ // cross-page-load, per-reason guard: distinct keys mean a NewClientGroup recovery
115
+ // never suppresses a later SchemaVersionNotSupported one. the in-memory tier
116
+ // catches re-fires within a page-load (works on Hermes); the storage tier
117
+ // survives the reload, so a reason that reloaded then immediately re-fires is
118
+ // caught as a genuine fatal instead of reload-storming across loads.
119
+ function recoveryGuardOpen(
120
+ reasonKey: string,
121
+ guardStorage: RecoveryGuardStorage | undefined,
122
+ ): boolean {
123
+ const key = `on-zero-recover-${reasonKey}`
124
+ const now = Date.now()
125
+
126
+ const memLast = inMemoryGuard.get(key) ?? 0
127
+ if (memLast > 0 && now - memLast < RECOVER_GUARD_MS) return false
128
+
129
+ const storage = guardStorage ?? defaultGuardStorage()
130
+ if (storage) {
131
+ try {
132
+ const rawLast = storage.getItem(key)
133
+ const last = rawLast ? Number(rawLast) : 0
134
+ if (last > 0 && now - last < RECOVER_GUARD_MS) return false
135
+ storage.setItem(key, String(now))
136
+ } catch {
137
+ // storage unavailable mid-flight — the in-memory tier is the floor.
138
+ }
139
+ }
140
+
141
+ inMemoryGuard.set(key, now)
66
142
  return true
67
143
  }
68
144
 
@@ -71,6 +147,64 @@ function recoveryGuardOpen(reasonKey: string): boolean {
71
147
  // closed local store and needs fresh local state.
72
148
  let lastStoreClosedAtMs = 0
73
149
 
150
+ function armReloadLatchTimeout() {
151
+ if (typeof setTimeout !== 'function') return
152
+ if (reloadLatchTimer) clearTimeout(reloadLatchTimer)
153
+ reloadLatchTimer = setTimeout(() => {
154
+ reloadLatchTimer = undefined
155
+ // re-open SCHEDULING only. a reload that never landed (still deferred behind
156
+ // a consumer's IDE gate / countdown, which can hold for minutes) must not
157
+ // kill future recovery — but the deferred performReload still owns the
158
+ // pending delete thunks and, if it is already mid-flight (a slow
159
+ // beforeReload can exceed this timeout), reloadInProgress must stay set so a
160
+ // second recovery can't double-drive the reload. so touch neither
161
+ // pendingDeletes nor reloadInProgress here.
162
+ reloadScheduled = false
163
+ }, RELOAD_LATCH_TIMEOUT_MS)
164
+ // don't hold the event loop open (node/native) waiting on this defense timer.
165
+ if (
166
+ reloadLatchTimer &&
167
+ typeof reloadLatchTimer === 'object' &&
168
+ 'unref' in reloadLatchTimer
169
+ ) {
170
+ reloadLatchTimer.unref()
171
+ }
172
+ }
173
+
174
+ function disarmReloadLatchTimeout() {
175
+ if (reloadLatchTimer) {
176
+ clearTimeout(reloadLatchTimer)
177
+ reloadLatchTimer = undefined
178
+ }
179
+ }
180
+
181
+ // deletes every collected instance's local store, awaits beforeReload, then
182
+ // reloads. deferred a microtask so sibling instances failing in the same tick
183
+ // enqueue their deletes first. idempotent: a consumer that calls performReload
184
+ // twice (or after the latch timeout) only runs one reload.
185
+ function performReload(deps: ZeroRecoveryDeps): Promise<void> {
186
+ if (reloadInProgress) return Promise.resolve()
187
+ reloadInProgress = true
188
+ // the consumer has committed to reloading, so the un-latch timeout has no job
189
+ // now — disarm it before the (possibly slow) delete/beforeReload chain so it
190
+ // can't fire mid-flight and re-open scheduling under an in-progress reload.
191
+ disarmReloadLatchTimeout()
192
+ // resolve the reload exactly as @rocicorp/zero does internally
193
+ // (`getBrowserGlobal('location')?.reload()`, zero.js): read location off the
194
+ // global and optional-chain through it. a non-DOM host with a `window` shim
195
+ // but no real `location` (the sootsim tenant render-worker hides `location`
196
+ // for isolation) then drops its stale IDB and no-ops the reload — letting the
197
+ // host remount — instead of throwing "reading 'reload'" on undefined.
198
+ const doReload = deps.reload ?? (() => globalThis.location?.reload?.())
199
+ return Promise.resolve()
200
+ .then(() => Promise.allSettled(pendingDeletes.splice(0).map((run) => run())))
201
+ .then(() => deps.beforeReload?.())
202
+ .catch(() => {})
203
+ .then(() => {
204
+ doReload()
205
+ })
206
+ }
207
+
74
208
  function recover(
75
209
  deps: ZeroRecoveryDeps,
76
210
  reasonKey: string,
@@ -82,36 +216,31 @@ function recover(
82
216
  // already queued — otherwise a sibling's store survives the reload and
83
217
  // fatal-loops on the next boot. the single scheduled reload awaits these.
84
218
  if (dropLocalState) {
85
- pendingDeletes.push(
86
- Promise.resolve()
87
- .then(deps.deleteLocalState)
88
- .catch(() => {}),
89
- )
219
+ pendingDeletes.push(() => Promise.resolve().then(deps.deleteLocalState))
90
220
  }
91
221
  // only ONE reload per page-load; a later trigger just contributes its delete.
92
222
  if (reloadScheduled) return
93
- if (!recoveryGuardOpen(reasonKey)) {
223
+ if (!recoveryGuardOpen(reasonKey, deps.guardStorage)) {
94
224
  console.error(`[on-zero] ${message} — already recovered once, not reloading`)
95
225
  deps.zeroEvents.emit({ type: 'fatal', reason: message })
96
226
  return
97
227
  }
98
228
  reloadScheduled = true
229
+ armReloadLatchTimeout()
99
230
  console.warn(`[on-zero] ${message} — recovering`)
100
231
  deps.zeroEvents.emit({ type: 'recovering', reason: message })
101
- // resolve the reload exactly as @rocicorp/zero does internally
102
- // (`getBrowserGlobal('location')?.reload()`, zero.js): read location off the
103
- // global and optional-chain through it. a non-DOM host with a `window` shim
104
- // but no real `location` (the sootsim tenant render-worker hides `location`
105
- // for isolation) then drops its stale IDB and no-ops the reload — letting the
106
- // host remount — instead of throwing "reading 'reload'" on undefined.
107
- const doReload = deps.reload ?? (() => globalThis.location?.reload?.())
108
- // defer one microtask so sibling instances failing in the same tick enqueue
109
- // their deletes first, then await them all before reloading.
110
- Promise.resolve()
111
- .then(() => Promise.allSettled(pendingDeletes))
112
- .then(() => deps.beforeReload?.())
113
- .catch(() => {})
114
- .finally(doReload)
232
+
233
+ const runReload = () => performReload(deps)
234
+ if (deps.scheduleReload) {
235
+ deps.scheduleReload({
236
+ reason: message,
237
+ reasonKey,
238
+ dropLocalState,
239
+ performReload: runReload,
240
+ })
241
+ } else {
242
+ void runReload()
243
+ }
115
244
  }
116
245
 
117
246
  // passing our own onUpdateNeeded/onClientStateNotFound DISABLES Zero's built-in
@@ -179,6 +308,71 @@ function isBenignStoreClosedLog(text: string): boolean {
179
308
  return false
180
309
  }
181
310
 
311
+ // the mutation/connection desync class: the local client group is out of sync
312
+ // with the server's last-mutation-id / cookie / client record, so the store is
313
+ // unusable and must be dropped + recovered. these surface only through the error
314
+ // log, never the structured onClientStateNotFound callback. genuinely app-infra
315
+ // signatures (a consumer's synthesized ack-timeout marker, a cold-boot connect
316
+ // timeout) are NOT here — a consumer keeps those with benignLogFilter / its own
317
+ // handling.
318
+ function classifyMutationDesync(
319
+ text: string,
320
+ ): { reason: ZeroRecoveryLogReason; reasonKey: string; message: string } | undefined {
321
+ if (text.includes('sent mutation ID') && text.includes('but expected')) {
322
+ return {
323
+ reason: 'mutation-desync',
324
+ reasonKey: 'mutation-desync',
325
+ message: 'mutation id desync',
326
+ }
327
+ }
328
+ if (
329
+ text.includes('oooMutation') ||
330
+ text.includes('Server reported an out-of-order mutation')
331
+ ) {
332
+ return {
333
+ reason: 'mutation-desync',
334
+ reasonKey: 'mutation-desync',
335
+ message: 'out-of-order mutation',
336
+ }
337
+ }
338
+ if (text.includes('already processed')) {
339
+ return {
340
+ reason: 'mutation-desync',
341
+ reasonKey: 'mutation-desync',
342
+ message: 'mutation already processed',
343
+ }
344
+ }
345
+ if (text.includes('InvalidConnectionRequestLastMutationID')) {
346
+ return {
347
+ reason: 'mutation-desync',
348
+ reasonKey: 'mutation-desync',
349
+ message: 'invalid connection last mutation id',
350
+ }
351
+ }
352
+ if (text.includes('InvalidConnectionRequestBaseCookie')) {
353
+ return {
354
+ reason: 'connection-cookie-invalid',
355
+ reasonKey: 'connection-cookie-invalid',
356
+ message: 'invalid connection base cookie',
357
+ }
358
+ }
359
+ if (text.includes('ClientNotFound') || text.includes('Client not found')) {
360
+ return {
361
+ reason: 'client-not-found',
362
+ reasonKey: 'client-not-found',
363
+ message: 'client not found',
364
+ }
365
+ }
366
+ if (text.includes('connection userID mismatch')) {
367
+ return {
368
+ reason: 'connection-userid-mismatch',
369
+ reasonKey: 'connection-userid-mismatch',
370
+ message: 'connection user id mismatch',
371
+ }
372
+ }
373
+ return undefined
374
+ }
375
+
182
376
  export function classifyZeroRecoveryLog(
183
377
  level: LogLevel | string,
184
378
  args: readonly unknown[],
@@ -202,6 +396,10 @@ export function classifyZeroRecoveryLog(
202
396
  dropLocalState: true,
203
397
  }
204
398
  }
399
+ const desync = classifyMutationDesync(text)
400
+ if (desync) {
401
+ return { ...desync, dropLocalState: true }
402
+ }
205
403
  if (text.includes(STORE_CLOSED) && !isBenignStoreClosedLog(text)) {
206
404
  const prevMs = lastStoreClosedAtMs
207
405
  lastStoreClosedAtMs = nowMs
@@ -221,10 +419,10 @@ export function classifyZeroRecoveryLog(
221
419
  return undefined
222
420
  }
223
421
 
224
- // watch error-level logs for the local-store-lost signature (the one fatal Zero
225
- // surfaces only through the log, not the structured callbacks) and trigger the
226
- // same recovery. forwards to the consumer sink, or to the console when there is
227
- // none. wire this ONLY when the consumer didn't pass their own logSink, so a
422
+ // watch error-level logs for the local-store-lost / desync signatures (the ones
423
+ // Zero surfaces only through the log, not the structured callbacks) and trigger
424
+ // the same recovery. forwards to the consumer sink, or to the console when there
425
+ // is none. wire this ONLY when the consumer didn't pass their own logSink, so a
228
426
  // consumer that owns log-based recovery isn't double-fired.
229
427
  export function composeRecoveryLogSink(
230
428
  deps: ZeroRecoveryDeps,
@@ -236,9 +434,12 @@ export function composeRecoveryLogSink(
236
434
  if (consumerLogSink) consumerLogSink.log(level, context, ...args)
237
435
  else logToConsole(level, context, ...args)
238
436
  const recovery = classifyZeroRecoveryLog(level, args)
239
- if (recovery) {
240
- recover(deps, recovery.reasonKey, recovery.message, recovery.dropLocalState)
437
+ if (!recovery) return
438
+ if (deps.benignLogFilter) {
439
+ const text = args.map(logArgText).join(' ')
440
+ if (deps.benignLogFilter(text)) return
241
441
  }
442
+ recover(deps, recovery.reasonKey, recovery.message, recovery.dropLocalState)
242
443
  },
243
444
  // call through the consumer sink so a class-based sink keeps its `this`,
244
445
  // rather than handing Zero a detached method reference.
@@ -246,10 +447,30 @@ export function composeRecoveryLogSink(
246
447
  }
247
448
  }
248
449
 
249
- // test-only: the reload latch + pending deletes are in-memory (a real page reload
250
- // clears them); tests simulate that reset between successive "page loads".
450
+ // generic Zero stale-poke / stale-cookie signatures: the client's view is behind
451
+ // the server's snapshot cookie, which a plain reconnect resolves — this is not a
452
+ // fatal store-loss, so ConnectionMonitor reconnects instead of recovering.
453
+ export function isRecoverableZeroStalePokeMessage(message: string): boolean {
454
+ return (
455
+ message.includes('Server returned unexpected base cookie during sync') ||
456
+ (message.includes('Received cookie') &&
457
+ message.includes('is < than last snapshot cookie') &&
458
+ message.includes('ignoring client view'))
459
+ )
460
+ }
461
+
462
+ // test-only: the reload latch + pending deletes + in-memory guard are in-memory
463
+ // (a real page reload clears them); tests simulate that reset between successive
464
+ // "page loads". the injectable/sessionStorage guard is NOT cleared here — like a
465
+ // real reload, it survives, which is what catches an immediate re-fire.
251
466
  export function resetRecoveryStateForTests() {
252
467
  reloadScheduled = false
468
+ reloadInProgress = false
469
+ if (reloadLatchTimer) {
470
+ clearTimeout(reloadLatchTimer)
471
+ reloadLatchTimer = undefined
472
+ }
253
473
  pendingDeletes.length = 0
254
474
  lastStoreClosedAtMs = 0
475
+ inMemoryGuard.clear()
255
476
  }
package/src/index.ts CHANGED
@@ -45,7 +45,10 @@ export {
45
45
  classifyZeroRecoveryLog,
46
46
  makeZeroRecovery,
47
47
  composeRecoveryLogSink,
48
+ isRecoverableZeroStalePokeMessage,
48
49
  type ZeroRecoveryLogClassification,
49
50
  type ZeroRecoveryLogReason,
50
51
  type ZeroRecoveryDeps,
52
+ type ScheduleReloadContext,
53
+ type RecoveryGuardStorage,
51
54
  } from './helpers/recoverZeroClient'
package/src/multi.ts CHANGED
@@ -22,3 +22,36 @@ export function createZeroClientWithDirectQueries<
22
22
  createDirectUseQuery,
23
23
  })
24
24
  }
25
+
26
+ // fail loud when a generated namespace drifts out of a multi-instance partition.
27
+ // multi-instance consumers (control + project, control + server, …) hand-split
28
+ // the generated query/model namespaces across instances; a namespace that ends
29
+ // up in NEITHER partition silently un-registers its queries (its useQuery throws
30
+ // "query not registered" at runtime, error-boundarying a whole screen — the soot
31
+ // `planGrant` regression). run this at module-eval over each partitioned group so
32
+ // the drift is a build/boot throw instead. also catches a namespace listed in
33
+ // MORE than one partition (an ambiguous owner), which the hand-rolled loops did
34
+ // not. `partitions` maps a label (e.g. 'control'/'project') to that instance's
35
+ // namespace table; every key in `entries` must appear in exactly one.
36
+ export function assertZeroInstancePartition(
37
+ kind: string,
38
+ entries: Record<string, unknown>,
39
+ partitions: Record<string, Record<string, unknown>>,
40
+ ): void {
41
+ const labels = Object.keys(partitions)
42
+ for (const name of Object.keys(entries)) {
43
+ const owners = labels.filter((label) => name in partitions[label])
44
+ if (owners.length === 0) {
45
+ throw new Error(
46
+ `[on-zero] generated ${kind} "${name}" is missing from the instance partition ` +
47
+ `(${labels.join(' / ')}) — add it to exactly one partition table.`,
48
+ )
49
+ }
50
+ if (owners.length > 1) {
51
+ throw new Error(
52
+ `[on-zero] generated ${kind} "${name}" is claimed by more than one instance partition ` +
53
+ `(${owners.join(', ')}) — it must belong to exactly one.`,
54
+ )
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { assertZeroInstancePartition } from './multi'
4
+
5
+ describe('assertZeroInstancePartition', () => {
6
+ const control = { user: 1, workspace: 1 }
7
+ const project = { message: 1, thread: 1 }
8
+
9
+ test('passes when every namespace belongs to exactly one partition', () => {
10
+ expect(() =>
11
+ assertZeroInstancePartition(
12
+ 'query namespace',
13
+ { user: {}, workspace: {}, message: {}, thread: {} },
14
+ { control, project },
15
+ ),
16
+ ).not.toThrow()
17
+ })
18
+
19
+ test('throws on a namespace missing from every partition (the planGrant drift)', () => {
20
+ expect(() =>
21
+ assertZeroInstancePartition(
22
+ 'query namespace',
23
+ { user: {}, planGrant: {} },
24
+ { control, project },
25
+ ),
26
+ ).toThrow(/planGrant.*missing from the instance partition/)
27
+ })
28
+
29
+ test('throws on a namespace claimed by more than one partition', () => {
30
+ expect(() =>
31
+ assertZeroInstancePartition(
32
+ 'query namespace',
33
+ { user: {} },
34
+ { control, project: { user: 1 } },
35
+ ),
36
+ ).toThrow(/user.*more than one instance partition/)
37
+ })
38
+ })
@@ -0,0 +1,5 @@
1
+ declare global {
2
+ var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
3
+ }
4
+ export {};
5
+ //# sourceMappingURL=createZeroClient.connection.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createZeroClient.connection.test.d.ts","sourceRoot":"","sources":["../src/createZeroClient.connection.test.tsx"],"names":[],"mappings":"AAuDA,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,wBAAwB,EAAE,OAAO,GAAG,SAAS,CAAA;CAClD"}
@@ -2,6 +2,7 @@ import { Zero as ZeroClient } from '@rocicorp/zero';
2
2
  import { type Emitter } from '@take-out/helpers';
3
3
  import { type Context, type ReactNode } from 'react';
4
4
  import { type QueryControlMode, type UseQueryHook } from './createUseQuery';
5
+ import { type RecoveryGuardStorage, type ScheduleReloadContext } from './helpers/recoverZeroClient';
5
6
  import { resolveQuery, type PlainQueryFn } from './resolveQuery';
6
7
  import type { AuthData, GenericModels, GetZeroMutators, ZeroEvent } from './types';
7
8
  import type { AnyQueryRegistry, Query, Row, ZeroOptions, Schema as ZeroSchema } from '@rocicorp/zero';
@@ -33,6 +34,11 @@ export declare function createZeroClient<Schema extends ZeroSchema, Models exten
33
34
  transport?: "http-pull";
34
35
  pullIntervalMs?: number;
35
36
  beforeReload?: (() => Promise<void>) | undefined;
37
+ scheduleReload?: ((ctx: ScheduleReloadContext) => void) | undefined;
38
+ guardStorage?: RecoveryGuardStorage;
39
+ benignLogFilter?: ((message: string) => boolean) | undefined;
40
+ refreshAuth?: (() => Promise<string | undefined>) | undefined;
41
+ connectionDataset?: boolean;
36
42
  }) => import("react/jsx-runtime").JSX.Element;
37
43
  ControlQueries: ({ children, action, whenDisabled, }: {
38
44
  children: ReactNode;
@@ -58,6 +64,9 @@ export declare function createZeroClient<Schema extends ZeroSchema, Models exten
58
64
  <TArg, TTable extends keyof Schema["tables"] & string, TReturn>(fn: PlainQueryFn<TArg, Query<TTable, Schema, TReturn>>, params: TArg): ReturnType<typeof resolveQuery<Schema>>;
59
65
  <TTable extends keyof Schema["tables"] & string, TReturn>(fn: PlainQueryFn<void, Query<TTable, Schema, TReturn>>): ReturnType<typeof resolveQuery<Schema>>;
60
66
  };
67
+ remint: (opts?: {
68
+ dropLocalState?: boolean;
69
+ }) => Promise<boolean>;
61
70
  };
62
71
  export declare function createZeroClientInternal<Schema extends ZeroSchema, Models extends GenericModels>({ schema, models, groupedQueries, permissionStrategy, instanceName, createDirectUseQuery, }: CreateZeroClientOptions<Schema, Models> & {
63
72
  createDirectUseQuery?: DirectQueryAdapter<Schema>;
@@ -71,6 +80,11 @@ export declare function createZeroClientInternal<Schema extends ZeroSchema, Mode
71
80
  transport?: "http-pull";
72
81
  pullIntervalMs?: number;
73
82
  beforeReload?: () => Promise<void>;
83
+ scheduleReload?: (ctx: ScheduleReloadContext) => void;
84
+ guardStorage?: RecoveryGuardStorage;
85
+ benignLogFilter?: (message: string) => boolean;
86
+ refreshAuth?: () => Promise<string | undefined>;
87
+ connectionDataset?: boolean;
74
88
  }) => import("react/jsx-runtime").JSX.Element;
75
89
  ControlQueries: ({ children, action, whenDisabled, }: {
76
90
  children: ReactNode;
@@ -96,6 +110,9 @@ export declare function createZeroClientInternal<Schema extends ZeroSchema, Mode
96
110
  <TArg, TTable extends keyof Schema["tables"] & string, TReturn>(fn: PlainQueryFn<TArg, Query<TTable, Schema, TReturn>>, params: TArg): ReturnType<typeof resolveQuery<Schema>>;
97
111
  <TTable extends keyof Schema["tables"] & string, TReturn>(fn: PlainQueryFn<void, Query<TTable, Schema, TReturn>>): ReturnType<typeof resolveQuery<Schema>>;
98
112
  };
113
+ remint: (opts?: {
114
+ dropLocalState?: boolean;
115
+ }) => Promise<boolean>;
99
116
  };
100
117
  export {};
101
118
  //# sourceMappingURL=createZeroClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createZeroClient.d.ts","sourceRoot":"","sources":["../src/createZeroClient.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"createZeroClient.d.ts","sourceRoot":"","sources":["../src/createZeroClient.tsx"],"names":[],"mappings":"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"}
@@ -90,7 +90,7 @@ export declare function createZeroServer<Schema extends ZeroSchema, Models exten
90
90
  error: "app";
91
91
  } | {
92
92
  details?: import("drizzle-zero").ReadonlyJSONValue | undefined;
93
- error: "alreadyProcessed" | "oooMutation";
93
+ error: "oooMutation" | "alreadyProcessed";
94
94
  };
95
95
  }[];
96
96
  } | {
@@ -102,7 +102,7 @@ export declare function createZeroServer<Schema extends ZeroSchema, Models exten
102
102
  clientID: string;
103
103
  }[];
104
104
  origin: "server";
105
- reason: "parse" | "database" | "oooMutation" | "internal" | "unsupportedPushVersion";
105
+ reason: "parse" | "oooMutation" | "database" | "internal" | "unsupportedPushVersion";
106
106
  } | {
107
107
  details?: import("drizzle-zero").ReadonlyJSONValue | undefined;
108
108
  bodyPreview?: string | undefined;
@@ -139,7 +139,7 @@ export declare function createZeroServer<Schema extends ZeroSchema, Models exten
139
139
  error: "app";
140
140
  } | {
141
141
  details?: import("drizzle-zero").ReadonlyJSONValue | undefined;
142
- error: "alreadyProcessed" | "oooMutation";
142
+ error: "oooMutation" | "alreadyProcessed";
143
143
  };
144
144
  }[];
145
145
  } | {
@@ -178,7 +178,7 @@ export declare function createZeroServer<Schema extends ZeroSchema, Models exten
178
178
  clientID: string;
179
179
  }[];
180
180
  origin: "server";
181
- reason: "parse" | "database" | "oooMutation" | "internal" | "unsupportedPushVersion";
181
+ reason: "parse" | "oooMutation" | "database" | "internal" | "unsupportedPushVersion";
182
182
  } | {
183
183
  details?: import("drizzle-zero").ReadonlyJSONValue | undefined;
184
184
  bodyPreview?: string | undefined;
@@ -2,17 +2,30 @@ import type { Emitter } from '@take-out/helpers';
2
2
  import type { ZeroEvent } from '../types';
3
3
  import type { UpdateNeededReason } from '@rocicorp/zero';
4
4
  import type { LogLevel, LogSink } from '@rocicorp/logger';
5
- export type ZeroRecoveryLogReason = 'indexeddb-not-found' | 'sqlite-statement-finalized' | 'store-closed-repeat';
5
+ export type ZeroRecoveryLogReason = 'indexeddb-not-found' | 'sqlite-statement-finalized' | 'store-closed-repeat' | 'mutation-desync' | 'connection-cookie-invalid' | 'client-not-found' | 'connection-userid-mismatch';
6
6
  export type ZeroRecoveryLogClassification = {
7
7
  reason: ZeroRecoveryLogReason;
8
8
  reasonKey: string;
9
9
  message: string;
10
10
  dropLocalState: boolean;
11
11
  };
12
+ export type RecoveryGuardStorage = {
13
+ getItem: (key: string) => string | null;
14
+ setItem: (key: string, value: string) => void;
15
+ };
16
+ export type ScheduleReloadContext = {
17
+ reason: string;
18
+ reasonKey: string;
19
+ dropLocalState: boolean;
20
+ performReload: () => Promise<void>;
21
+ };
12
22
  export type ZeroRecoveryDeps = {
13
23
  deleteLocalState: () => Promise<unknown>;
14
24
  zeroEvents: Emitter<ZeroEvent | null>;
15
25
  beforeReload?: () => Promise<void>;
26
+ scheduleReload?: (ctx: ScheduleReloadContext) => void;
27
+ guardStorage?: RecoveryGuardStorage;
28
+ benignLogFilter?: (message: string) => boolean;
16
29
  reload?: () => void;
17
30
  };
18
31
  export declare function makeZeroRecovery(deps: ZeroRecoveryDeps): {
@@ -21,5 +34,6 @@ export declare function makeZeroRecovery(deps: ZeroRecoveryDeps): {
21
34
  };
22
35
  export declare function classifyZeroRecoveryLog(level: LogLevel | string, args: readonly unknown[], nowMs?: number): ZeroRecoveryLogClassification | undefined;
23
36
  export declare function composeRecoveryLogSink(deps: ZeroRecoveryDeps, consumerLogSink?: LogSink): LogSink;
37
+ export declare function isRecoverableZeroStalePokeMessage(message: string): boolean;
24
38
  export declare function resetRecoveryStateForTests(): void;
25
39
  //# sourceMappingURL=recoverZeroClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recoverZeroClient.d.ts","sourceRoot":"","sources":["../../src/helpers/recoverZeroClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,KAAK,EAAW,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAelE,MAAM,MAAM,qBAAqB,GAC7B,qBAAqB,GACrB,4BAA4B,GAC5B,qBAAqB,CAAA;AAEzB,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,EAAE,qBAAqB,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,OAAO,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAI7B,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAExC,UAAU,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IAGrC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAElC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AA6ED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB;2BAE5B,kBAAkB;;EAsB5C;AAsCD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,QAAQ,GAAG,MAAM,EACxB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,KAAK,SAAa,GACjB,6BAA6B,GAAG,SAAS,CAoC3C;AAOD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,gBAAgB,EACtB,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAeT;AAID,wBAAgB,0BAA0B,SAIzC"}
1
+ {"version":3,"file":"recoverZeroClient.d.ts","sourceRoot":"","sources":["../../src/helpers/recoverZeroClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,KAAK,EAAW,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAuBlE,MAAM,MAAM,qBAAqB,GAC7B,qBAAqB,GACrB,4BAA4B,GAC5B,qBAAqB,GACrB,iBAAiB,GACjB,2BAA2B,GAC3B,kBAAkB,GAClB,4BAA4B,CAAA;AAEhC,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,EAAE,qBAAqB,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,OAAO,CAAA;CACxB,CAAA;AAMD,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IACvC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAC9C,CAAA;AAKD,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IAGvB,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAI7B,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAExC,UAAU,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IAGrC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAIlC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAGrD,YAAY,CAAC,EAAE,oBAAoB,CAAA;IAGnC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;IAE9C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAmKD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB;2BAE5B,kBAAkB;;EAsB5C;AAuGD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,QAAQ,GAAG,MAAM,EACxB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,KAAK,SAAa,GACjB,6BAA6B,GAAG,SAAS,CAwC3C;AAOD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,gBAAgB,EACtB,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAkBT;AAKD,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO1E;AAMD,wBAAgB,0BAA0B,SAUzC"}