on-zero 0.4.22 → 0.4.24

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 (93) hide show
  1. package/dist/cjs/combineZeroClients.cjs +101 -0
  2. package/dist/cjs/combineZeroClients.native.js +150 -0
  3. package/dist/cjs/combineZeroClients.native.js.map +1 -0
  4. package/dist/cjs/createUseQuery.cjs +92 -4
  5. package/dist/cjs/createUseQuery.native.js +130 -4
  6. package/dist/cjs/createUseQuery.native.js.map +1 -1
  7. package/dist/cjs/createZeroClient.cjs +89 -12
  8. package/dist/cjs/createZeroClient.native.js +134 -43
  9. package/dist/cjs/createZeroClient.native.js.map +1 -1
  10. package/dist/cjs/index.cjs +1 -0
  11. package/dist/cjs/index.native.js +1 -0
  12. package/dist/cjs/index.native.js.map +1 -1
  13. package/dist/cjs/instanceRegistry.cjs +65 -0
  14. package/dist/cjs/instanceRegistry.native.js +111 -0
  15. package/dist/cjs/instanceRegistry.native.js.map +1 -0
  16. package/dist/cjs/multiInstance.test.cjs +322 -0
  17. package/dist/cjs/multiInstance.test.native.js +387 -0
  18. package/dist/cjs/multiInstance.test.native.js.map +1 -0
  19. package/dist/cjs/multiInstanceNested.test.cjs +206 -0
  20. package/dist/cjs/multiInstanceNested.test.native.js +254 -0
  21. package/dist/cjs/multiInstanceNested.test.native.js.map +1 -0
  22. package/dist/cjs/run.cjs +5 -5
  23. package/dist/cjs/run.native.js +6 -5
  24. package/dist/cjs/run.native.js.map +1 -1
  25. package/dist/cjs/zeroRunner.cjs +4 -1
  26. package/dist/cjs/zeroRunner.native.js +4 -1
  27. package/dist/cjs/zeroRunner.native.js.map +1 -1
  28. package/dist/esm/combineZeroClients.mjs +76 -0
  29. package/dist/esm/combineZeroClients.mjs.map +1 -0
  30. package/dist/esm/combineZeroClients.native.js +122 -0
  31. package/dist/esm/combineZeroClients.native.js.map +1 -0
  32. package/dist/esm/createUseQuery.mjs +92 -5
  33. package/dist/esm/createUseQuery.mjs.map +1 -1
  34. package/dist/esm/createUseQuery.native.js +130 -5
  35. package/dist/esm/createUseQuery.native.js.map +1 -1
  36. package/dist/esm/createZeroClient.mjs +93 -16
  37. package/dist/esm/createZeroClient.mjs.map +1 -1
  38. package/dist/esm/createZeroClient.native.js +138 -47
  39. package/dist/esm/createZeroClient.native.js.map +1 -1
  40. package/dist/esm/index.js +1 -0
  41. package/dist/esm/index.js.map +1 -1
  42. package/dist/esm/index.mjs +1 -0
  43. package/dist/esm/index.mjs.map +1 -1
  44. package/dist/esm/index.native.js +1 -0
  45. package/dist/esm/index.native.js.map +1 -1
  46. package/dist/esm/instanceRegistry.mjs +38 -0
  47. package/dist/esm/instanceRegistry.mjs.map +1 -0
  48. package/dist/esm/instanceRegistry.native.js +81 -0
  49. package/dist/esm/instanceRegistry.native.js.map +1 -0
  50. package/dist/esm/multiInstance.test.mjs +323 -0
  51. package/dist/esm/multiInstance.test.mjs.map +1 -0
  52. package/dist/esm/multiInstance.test.native.js +385 -0
  53. package/dist/esm/multiInstance.test.native.js.map +1 -0
  54. package/dist/esm/multiInstanceNested.test.mjs +207 -0
  55. package/dist/esm/multiInstanceNested.test.mjs.map +1 -0
  56. package/dist/esm/multiInstanceNested.test.native.js +252 -0
  57. package/dist/esm/multiInstanceNested.test.native.js.map +1 -0
  58. package/dist/esm/run.mjs +5 -5
  59. package/dist/esm/run.mjs.map +1 -1
  60. package/dist/esm/run.native.js +6 -5
  61. package/dist/esm/run.native.js.map +1 -1
  62. package/dist/esm/zeroRunner.mjs +4 -1
  63. package/dist/esm/zeroRunner.mjs.map +1 -1
  64. package/dist/esm/zeroRunner.native.js +4 -1
  65. package/dist/esm/zeroRunner.native.js.map +1 -1
  66. package/package.json +5 -3
  67. package/readme.md +59 -0
  68. package/src/combineZeroClients.tsx +186 -0
  69. package/src/createUseQuery.tsx +175 -12
  70. package/src/createZeroClient.tsx +227 -54
  71. package/src/index.ts +1 -0
  72. package/src/instanceRegistry.ts +75 -0
  73. package/src/multiInstance.test.tsx +284 -0
  74. package/src/multiInstanceNested.test.tsx +205 -0
  75. package/src/run.ts +7 -6
  76. package/src/zeroRunner.ts +7 -1
  77. package/types/combineZeroClients.d.ts +38 -0
  78. package/types/combineZeroClients.d.ts.map +1 -0
  79. package/types/createUseQuery.d.ts +15 -0
  80. package/types/createUseQuery.d.ts.map +1 -1
  81. package/types/createZeroClient.d.ts +10 -4
  82. package/types/createZeroClient.d.ts.map +1 -1
  83. package/types/index.d.ts +1 -0
  84. package/types/index.d.ts.map +1 -1
  85. package/types/instanceRegistry.d.ts +15 -0
  86. package/types/instanceRegistry.d.ts.map +1 -0
  87. package/types/multiInstance.test.d.ts +2 -0
  88. package/types/multiInstance.test.d.ts.map +1 -0
  89. package/types/multiInstanceNested.test.d.ts +5 -0
  90. package/types/multiInstanceNested.test.d.ts.map +1 -0
  91. package/types/run.d.ts.map +1 -1
  92. package/types/zeroRunner.d.ts +3 -1
  93. package/types/zeroRunner.d.ts.map +1 -1
@@ -1,6 +1,6 @@
1
- import { defineQueries, defineQuery } from '@rocicorp/zero'
1
+ import { defineQueries, defineQuery, Zero as ZeroClient } from '@rocicorp/zero'
2
2
  import { useConnectionState, useZero, ZeroProvider } from '@rocicorp/zero/react'
3
- import { createEmitter } from '@take-out/helpers'
3
+ import { createEmitter, useEmitterValue } from '@take-out/helpers'
4
4
  import {
5
5
  createContext,
6
6
  memo,
@@ -8,11 +8,18 @@ import {
8
8
  useEffect,
9
9
  useMemo,
10
10
  useRef,
11
+ useState,
11
12
  type ReactNode,
12
13
  } from 'react'
13
14
 
14
15
  import { createPermissions } from './createPermissions'
15
- import { createUseQuery, type QueryControlMode } from './createUseQuery'
16
+ import {
17
+ createUseQuery,
18
+ createUseQueryDirect,
19
+ type QueryControlMode,
20
+ type UseQueryHook,
21
+ } from './createUseQuery'
22
+ import { registerClientInstance } from './instanceRegistry'
16
23
  import { createMutators } from './helpers/createMutators'
17
24
  import { getAuth } from './helpers/getAuth'
18
25
  import { getAllMutationsPermissions, getMutationsPermissions } from './modelRegistry'
@@ -21,7 +28,7 @@ import { resolveQuery, type PlainQueryFn } from './resolveQuery'
21
28
  import { setCustomQueries } from './run'
22
29
  import { getEnvironment, setAuthData, setEnvironment, setSchema } from './state'
23
30
  import { getRawWhere, setEvaluatingPermission } from './where'
24
- import { setRunner } from './zeroRunner'
31
+ import { setRunner, type ZeroRunner } from './zeroRunner'
25
32
  import { zql } from './zql'
26
33
 
27
34
  import type { AuthData, GenericModels, GetZeroMutators, ZeroEvent } from './types'
@@ -62,11 +69,16 @@ export function createZeroClient<
62
69
  models,
63
70
  groupedQueries,
64
71
  permissionStrategy = 'optimistic',
72
+ instanceName = 'default',
65
73
  }: {
66
74
  schema: Schema
67
75
  models: Models
68
76
  groupedQueries: GroupedQueries
69
77
  permissionStrategy?: PermissionStrategy
78
+ // names this client instance so multiple instances can coexist on one page.
79
+ // each query/mutator namespace is claimed by exactly one instance, and the
80
+ // ambient run() + the combineZeroClients facade dispatch by that claim.
81
+ instanceName?: string
70
82
  }) {
71
83
  type ZeroMutators = GetZeroMutators<Models>
72
84
  type ZeroInstance = Zero<Schema, ZeroMutators>
@@ -176,6 +188,15 @@ export function createZeroClient<
176
188
  // create the single shared CustomQuery registry
177
189
  const customQueries = defineQueries(wrappedNamespaces)
178
190
 
191
+ // claim this instance's query/mutator namespaces so the ambient run() and
192
+ // the combineZeroClients facade dispatch to the owning instance. the
193
+ // auto-generated 'permission' namespace stays unclaimed (per-instance).
194
+ const instance = registerClientInstance({
195
+ name: instanceName,
196
+ namespaces: Object.keys({ ...groupedQueries, ...models }),
197
+ customQueries,
198
+ })
199
+
179
200
  // register for global run() helper
180
201
  setCustomQueries(customQueries)
181
202
 
@@ -196,7 +217,20 @@ export function createZeroClient<
196
217
  },
197
218
  })
198
219
 
199
- const zeroEvents = createEmitter<ZeroEvent | null>('zero', null)
220
+ // emitter names are global keys (dev hmr cache) — scope them per instance
221
+ // so two instances never share cached values. the default name stays
222
+ // unchanged for single-instance back-compat.
223
+ const emitterScope = instanceName === 'default' ? '' : `:${instanceName}`
224
+
225
+ const zeroEvents = createEmitter<ZeroEvent | null>(`zero${emitterScope}`, null)
226
+
227
+ // bumped after commit whenever the mounted zero instance changes (first
228
+ // mount, identity change, client-state-not-found rotation) — the direct
229
+ // query path re-materializes its views against the new instance
230
+ const zeroInstanceVersion = createEmitter<number>(
231
+ `zero-instance-version${emitterScope}`,
232
+ 0,
233
+ )
200
234
 
201
235
  const AuthDataContext = createContext<AuthData>({} as AuthData)
202
236
 
@@ -205,50 +239,93 @@ export function createZeroClient<
205
239
  customQueries,
206
240
  })
207
241
 
242
+ // context-free counterpart: materializes directly on THIS instance's
243
+ // mounted zero instead of the nearest ZeroProvider context. used by
244
+ // combineZeroClients for instances whose provider is not innermost.
245
+ const useQueryDirect = createUseQueryDirect<Schema>({
246
+ DisabledContext,
247
+ customQueries,
248
+ getZero: () => latestZeroInstance,
249
+ zeroVersion: zeroInstanceVersion,
250
+ })
251
+
208
252
  // permission check uses a per-model synced query so server is authoritative
209
- // permissionStrategy controls client behavior before server responds
210
- function usePermission(
211
- table: TableName | (string & {}),
212
- objOrId: string | Partial<Row<any>> | undefined,
213
- enabled = typeof objOrId !== 'undefined',
214
- debug = false,
215
- ): boolean | null {
216
- const disableMode = useContext(DisabledContext)
217
- const lastRef = useRef<boolean | null>(null)
218
- const tableStr = table as string
219
- const checkFn = permissionCheckFns[tableStr]
220
-
221
- // include auth user ID in query args so zero-cache creates per-user
222
- // permission views (prevents dedup across different auth contexts)
223
- const auth = getAuth()
224
- const _uid = auth?.id || 'anon'
225
-
226
- const [data, status] = useQuery(
227
- checkFn as any,
228
- { objOrId: objOrId as any, _uid },
229
- { enabled: Boolean(!disableMode && enabled && objOrId && checkFn) },
230
- )
253
+ // permissionStrategy controls client behavior before server responds.
254
+ // built over a query hook so the facade can route permission checks down
255
+ // the same context vs direct path as the table's other queries.
256
+ const createUsePermission = (useQueryImpl: UseQueryHook<Schema>) =>
257
+ function usePermission(
258
+ table: TableName | (string & {}),
259
+ objOrId: string | Partial<Row<any>> | undefined,
260
+ enabled = typeof objOrId !== 'undefined',
261
+ debug = false,
262
+ ): boolean | null {
263
+ const disableMode = useContext(DisabledContext)
264
+ const lastRef = useRef<boolean | null>(null)
265
+ const tableStr = table as string
266
+ const checkFn = permissionCheckFns[tableStr]
267
+
268
+ // include auth user ID in query args so zero-cache creates per-user
269
+ // permission views (prevents dedup across different auth contexts)
270
+ const auth = getAuth()
271
+ const _uid = auth?.id || 'anon'
272
+
273
+ const [data, status] = useQueryImpl(
274
+ checkFn as any,
275
+ { objOrId: objOrId as any, _uid },
276
+ { enabled: Boolean(!disableMode && enabled && objOrId && checkFn) },
277
+ )
231
278
 
232
- if (debug) {
233
- console.info(`usePermission()`, { table, objOrId, data, status })
234
- }
279
+ if (debug) {
280
+ console.info(`usePermission()`, { table, objOrId, data, status })
281
+ }
235
282
 
236
- if (!objOrId) return false
283
+ if (!objOrId) return false
237
284
 
238
- // null while loading, then server's authoritative answer
239
- const result = status.type === 'unknown' ? null : Boolean(data)
285
+ // null while loading, then server's authoritative answer
286
+ const result = status.type === 'unknown' ? null : Boolean(data)
240
287
 
241
- if (!disableMode) {
242
- lastRef.current = result
243
- return result
244
- }
288
+ if (!disableMode) {
289
+ lastRef.current = result
290
+ return result
291
+ }
245
292
 
246
- if (disableMode === 'last-value') {
247
- return lastRef.current
293
+ if (disableMode === 'last-value') {
294
+ return lastRef.current
295
+ }
296
+
297
+ return null
248
298
  }
249
299
 
250
- return null
251
- }
300
+ const usePermission = createUsePermission(useQuery)
301
+ const usePermissionDirect = createUsePermission(useQueryDirect)
302
+
303
+ // the zero instance lives OUTSIDE the react lifecycle. react destroys and
304
+ // re-fires the effects of a committed tree on any suspense hide/reveal, and
305
+ // consumers also remount the provider (splash -> IDE). when ZeroProvider
306
+ // created zero inside its own effect, every such cycle closed the live
307
+ // instance mid-connect ("Failed to connect / Store is closed") and built a
308
+ // replacement, killing in-flight queries and preloads. instead the instance
309
+ // is created in an effect, cached here per identity key, handed to
310
+ // ZeroProvider as an external `zero` (which it never closes), and the
311
+ // previous instance is closed only when the key truly changes — a real
312
+ // identity change (user, storage, server, logged in/out), never a react
313
+ // lifecycle artifact. single-provider assumption: one mounted ProvideZero
314
+ // per client (true of every consumer); two simultaneously mounted providers
315
+ // with different identities would thrash this slot.
316
+ let cachedZero: { key: string; instance: ZeroInstance } | null = null
317
+
318
+ // bumping this recreates the instance with the same options — parity with
319
+ // ZeroProvider's built-in onClientStateNotFound rotation. the pending flag
320
+ // mirrors upstream's rotationPendingRef: a storm of client-state-not-found
321
+ // callbacks coalesces into ONE rotation, re-armed when the fresh instance
322
+ // is created — without it a new instance that immediately re-fires the
323
+ // callback rotation-loops forever.
324
+ const zeroInstanceGeneration = createEmitter<number>(
325
+ `zero-instance-generation${emitterScope}`,
326
+ 0,
327
+ )
328
+ let zeroRotationPending = false
252
329
 
253
330
  const ProvideZero = ({
254
331
  children,
@@ -279,6 +356,93 @@ export function createZeroClient<
279
356
  // eslint-disable-next-line react-hooks/exhaustive-deps
280
357
  }, [])
281
358
 
359
+ const generation = useEmitterValue(zeroInstanceGeneration)
360
+
361
+ const auth = 'auth' in props ? (props as { auth?: string | null }).auth : undefined
362
+ const hasAuth = typeof auth === 'string'
363
+
364
+ // identity = every primitive option except the auth token value (token
365
+ // changes refresh in place below; logged-in <-> logged-out still rotates
366
+ // via hasAuth, matching zero's documented provider semantics). function
367
+ // props (callbacks, logSink, batchViewUpdates) are bound at construction.
368
+ const instanceKey = JSON.stringify([
369
+ Object.entries({ kvStore: 'mem', ...props })
370
+ .filter(
371
+ ([key, value]) =>
372
+ key !== 'auth' && typeof value !== 'function' && value !== undefined,
373
+ )
374
+ .sort(([a], [b]) => (a < b ? -1 : 1)),
375
+ hasAuth,
376
+ generation,
377
+ ])
378
+
379
+ // create/rotate in an effect — commit-safe: a discarded concurrent render
380
+ // can never close an instance the committed tree still uses. a suspense
381
+ // hide/reveal re-runs this effect with an unchanged key — cache hit, no
382
+ // churn, no cleanup-close. children mounting one effect-tick after the
383
+ // provider matches ZeroProvider's internal-creation timing, which consumer
384
+ // boot orchestration observes.
385
+ const [instance, setInstance] = useState<ZeroInstance | undefined>()
386
+ useEffect(() => {
387
+ let cached = cachedZero
388
+ if (cached?.key !== instanceKey) {
389
+ cached?.instance.close()
390
+ const options = props as Omit<
391
+ ZeroOptions<Schema, ZeroMutators>,
392
+ 'schema' | 'mutators'
393
+ >
394
+ cached = {
395
+ key: instanceKey,
396
+ instance: new ZeroClient<Schema, ZeroMutators>({
397
+ kvStore: 'mem',
398
+ ...options,
399
+ schema,
400
+ // @ts-expect-error same erasure ZeroProvider needed
401
+ mutators,
402
+ onClientStateNotFound: () => {
403
+ // consumer callback wins; recreate-with-same-options is the
404
+ // fallback, mirroring ZeroProvider's internal rotation.
405
+ if (options.onClientStateNotFound) {
406
+ try {
407
+ options.onClientStateNotFound()
408
+ return
409
+ } catch {
410
+ // fall through to rotation
411
+ }
412
+ }
413
+ if (zeroRotationPending) return
414
+ zeroRotationPending = true
415
+ zeroInstanceGeneration.emit(zeroInstanceGeneration.value + 1)
416
+ },
417
+ }),
418
+ }
419
+ cachedZero = cached
420
+ zeroRotationPending = false
421
+ }
422
+ setInstance(cached.instance)
423
+ // identity is fully captured by instanceKey; function props are bound
424
+ // at construction by design.
425
+ // eslint-disable-next-line react-hooks/exhaustive-deps
426
+ }, [instanceKey])
427
+
428
+ // a changed token on the same identity refreshes auth in place — zero
429
+ // sends an auth update over the live connection instead of reconnecting
430
+ // (upstream ZeroProvider does exactly this). string <-> undefined flips
431
+ // rotate the instance via hasAuth in the identity key instead.
432
+ const prevAuthRef = useRef(auth)
433
+ useEffect(() => {
434
+ const prevAuth = prevAuthRef.current
435
+ prevAuthRef.current = auth
436
+ if (
437
+ instance &&
438
+ typeof prevAuth === 'string' &&
439
+ typeof auth === 'string' &&
440
+ prevAuth !== auth
441
+ ) {
442
+ instance.connection.connect({ auth })
443
+ }
444
+ }, [instance, auth])
445
+
282
446
  // for now we re-parent
283
447
  if (disable) {
284
448
  return children
@@ -286,17 +450,13 @@ export function createZeroClient<
286
450
 
287
451
  return (
288
452
  <AuthDataContext.Provider value={authData}>
289
- <ZeroProvider
290
- schema={schema}
291
- kvStore="mem"
292
- // @ts-expect-error
293
- mutators={mutators}
294
- {...props}
295
- >
296
- <SetZeroInstance />
297
- <ConnectionMonitor zeroEvents={zeroEvents} />
298
- {children}
299
- </ZeroProvider>
453
+ {instance ? (
454
+ <ZeroProvider zero={instance}>
455
+ <SetZeroInstance />
456
+ <ConnectionMonitor zeroEvents={zeroEvents} />
457
+ {children}
458
+ </ZeroProvider>
459
+ ) : null}
300
460
  </AuthDataContext.Provider>
301
461
  )
302
462
  }
@@ -312,10 +472,20 @@ export function createZeroClient<
312
472
  // we'll need to add that soon
313
473
  if (zeroInstance !== latestZeroInstance) {
314
474
  latestZeroInstance = zeroInstance
315
- // register runner for global run() helper
316
- setRunner((query, options) => zeroInstance.run(query as any, options))
475
+ const runner: ZeroRunner = (query, options) =>
476
+ zeroInstance.run(query as any, options)
477
+ // the instance-keyed runner is what run() dispatches owned namespaces
478
+ // to; the global runner stays as the ambient fallback (inline zql)
479
+ instance.runner = runner
480
+ setRunner(runner)
317
481
  }
318
482
 
483
+ // notify direct-path views after commit (emitting during render would
484
+ // setState other components mid-render)
485
+ useEffect(() => {
486
+ zeroInstanceVersion.emit(zeroInstanceVersion.value + 1)
487
+ }, [zeroInstance])
488
+
319
489
  return null
320
490
  }
321
491
 
@@ -398,11 +568,14 @@ export function createZeroClient<
398
568
  }
399
569
 
400
570
  return {
571
+ instanceName,
401
572
  zeroEvents,
402
573
  ProvideZero,
403
574
  ControlQueries,
404
575
  useQuery,
576
+ useQueryDirect,
405
577
  usePermission,
578
+ usePermissionDirect,
406
579
  zero,
407
580
  preload,
408
581
  getQuery,
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from './helpers/useMutation'
8
8
  export { ensureAuth, getAuth } from './helpers/getAuth'
9
9
  export { setAuthData, setEnvironment } from './state'
10
10
 
11
+ export * from './combineZeroClients'
11
12
  export * from './createZeroClient'
12
13
  export * from './createUseQuery'
13
14
  export * from './resolveQuery'
@@ -0,0 +1,75 @@
1
+ // registry mapping query/mutator namespaces to the zero client instance that
2
+ // owns them. enables multiple createZeroClient instances on one page (e.g. a
3
+ // control-plane instance + a per-project instance): the ambient run() and the
4
+ // combineZeroClients facade dispatch each query/mutation to the owning
5
+ // instance by its registered namespace instead of whichever mounted last.
6
+ //
7
+ // stored via globalValue so dual-loaded module copies (cjs/esm) share one map,
8
+ // matching the package's other cross-module registries.
9
+
10
+ import { globalValue } from '@take-out/helpers'
11
+
12
+ import { getQueryName } from './queryRegistry'
13
+
14
+ import type { ZeroRunner } from './zeroRunner'
15
+ import type { AnyQueryRegistry } from '@rocicorp/zero'
16
+
17
+ export type ZeroClientInstance = {
18
+ name: string
19
+ customQueries: AnyQueryRegistry
20
+ // set when the instance's provider mounts (client); stays null on the
21
+ // server, where the ambient transaction runner serves every instance
22
+ runner: ZeroRunner | null
23
+ }
24
+
25
+ const getInstancesByNamespace = () =>
26
+ globalValue<Map<string, ZeroClientInstance>>(
27
+ 'on-zero:instances-by-namespace',
28
+ () => new Map(),
29
+ )
30
+
31
+ export function registerClientInstance({
32
+ name,
33
+ namespaces,
34
+ customQueries,
35
+ }: {
36
+ name: string
37
+ namespaces: string[]
38
+ customQueries: AnyQueryRegistry
39
+ }): ZeroClientInstance {
40
+ const instancesByNamespace = getInstancesByNamespace()
41
+
42
+ // re-creating an instance under the same name (hmr) replaces its claims
43
+ for (const [namespace, owner] of instancesByNamespace) {
44
+ if (owner.name === name) {
45
+ instancesByNamespace.delete(namespace)
46
+ }
47
+ }
48
+
49
+ const instance: ZeroClientInstance = { name, customQueries, runner: null }
50
+
51
+ for (const namespace of namespaces) {
52
+ const existing = instancesByNamespace.get(namespace)
53
+ if (existing) {
54
+ throw new Error(
55
+ `[on-zero] namespace '${namespace}' is already claimed by zero client instance '${existing.name}' ` +
56
+ `(while creating instance '${name}'). Each query/mutator namespace must belong to exactly one createZeroClient instance.`,
57
+ )
58
+ }
59
+ instancesByNamespace.set(namespace, instance)
60
+ }
61
+
62
+ return instance
63
+ }
64
+
65
+ export function getInstanceForNamespace(
66
+ namespace: string,
67
+ ): ZeroClientInstance | undefined {
68
+ return getInstancesByNamespace().get(namespace)
69
+ }
70
+
71
+ export function getInstanceForQueryFn(fn: Function): ZeroClientInstance | undefined {
72
+ const queryName = getQueryName(fn)
73
+ if (!queryName) return undefined
74
+ return getInstanceForNamespace(queryName.split('.', 1)[0])
75
+ }