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.
- package/dist/cjs/combineZeroClients.cjs +101 -0
- package/dist/cjs/combineZeroClients.native.js +150 -0
- package/dist/cjs/combineZeroClients.native.js.map +1 -0
- package/dist/cjs/createUseQuery.cjs +92 -4
- package/dist/cjs/createUseQuery.native.js +130 -4
- package/dist/cjs/createUseQuery.native.js.map +1 -1
- package/dist/cjs/createZeroClient.cjs +89 -12
- package/dist/cjs/createZeroClient.native.js +134 -43
- package/dist/cjs/createZeroClient.native.js.map +1 -1
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/instanceRegistry.cjs +65 -0
- package/dist/cjs/instanceRegistry.native.js +111 -0
- package/dist/cjs/instanceRegistry.native.js.map +1 -0
- package/dist/cjs/multiInstance.test.cjs +322 -0
- package/dist/cjs/multiInstance.test.native.js +387 -0
- package/dist/cjs/multiInstance.test.native.js.map +1 -0
- package/dist/cjs/multiInstanceNested.test.cjs +206 -0
- package/dist/cjs/multiInstanceNested.test.native.js +254 -0
- package/dist/cjs/multiInstanceNested.test.native.js.map +1 -0
- package/dist/cjs/run.cjs +5 -5
- package/dist/cjs/run.native.js +6 -5
- package/dist/cjs/run.native.js.map +1 -1
- package/dist/cjs/zeroRunner.cjs +4 -1
- package/dist/cjs/zeroRunner.native.js +4 -1
- package/dist/cjs/zeroRunner.native.js.map +1 -1
- package/dist/esm/combineZeroClients.mjs +76 -0
- package/dist/esm/combineZeroClients.mjs.map +1 -0
- package/dist/esm/combineZeroClients.native.js +122 -0
- package/dist/esm/combineZeroClients.native.js.map +1 -0
- package/dist/esm/createUseQuery.mjs +92 -5
- package/dist/esm/createUseQuery.mjs.map +1 -1
- package/dist/esm/createUseQuery.native.js +130 -5
- package/dist/esm/createUseQuery.native.js.map +1 -1
- package/dist/esm/createZeroClient.mjs +93 -16
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +138 -47
- package/dist/esm/createZeroClient.native.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +1 -0
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/instanceRegistry.mjs +38 -0
- package/dist/esm/instanceRegistry.mjs.map +1 -0
- package/dist/esm/instanceRegistry.native.js +81 -0
- package/dist/esm/instanceRegistry.native.js.map +1 -0
- package/dist/esm/multiInstance.test.mjs +323 -0
- package/dist/esm/multiInstance.test.mjs.map +1 -0
- package/dist/esm/multiInstance.test.native.js +385 -0
- package/dist/esm/multiInstance.test.native.js.map +1 -0
- package/dist/esm/multiInstanceNested.test.mjs +207 -0
- package/dist/esm/multiInstanceNested.test.mjs.map +1 -0
- package/dist/esm/multiInstanceNested.test.native.js +252 -0
- package/dist/esm/multiInstanceNested.test.native.js.map +1 -0
- package/dist/esm/run.mjs +5 -5
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/run.native.js +6 -5
- package/dist/esm/run.native.js.map +1 -1
- package/dist/esm/zeroRunner.mjs +4 -1
- package/dist/esm/zeroRunner.mjs.map +1 -1
- package/dist/esm/zeroRunner.native.js +4 -1
- package/dist/esm/zeroRunner.native.js.map +1 -1
- package/package.json +5 -3
- package/readme.md +59 -0
- package/src/combineZeroClients.tsx +186 -0
- package/src/createUseQuery.tsx +175 -12
- package/src/createZeroClient.tsx +227 -54
- package/src/index.ts +1 -0
- package/src/instanceRegistry.ts +75 -0
- package/src/multiInstance.test.tsx +284 -0
- package/src/multiInstanceNested.test.tsx +205 -0
- package/src/run.ts +7 -6
- package/src/zeroRunner.ts +7 -1
- package/types/combineZeroClients.d.ts +38 -0
- package/types/combineZeroClients.d.ts.map +1 -0
- package/types/createUseQuery.d.ts +15 -0
- package/types/createUseQuery.d.ts.map +1 -1
- package/types/createZeroClient.d.ts +10 -4
- package/types/createZeroClient.d.ts.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/instanceRegistry.d.ts +15 -0
- package/types/instanceRegistry.d.ts.map +1 -0
- package/types/multiInstance.test.d.ts +2 -0
- package/types/multiInstance.test.d.ts.map +1 -0
- package/types/multiInstanceNested.test.d.ts +5 -0
- package/types/multiInstanceNested.test.d.ts.map +1 -0
- package/types/run.d.ts.map +1 -1
- package/types/zeroRunner.d.ts +3 -1
- package/types/zeroRunner.d.ts.map +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createSchema, string, table } from '@rocicorp/zero'
|
|
2
|
+
import { createEmitter } from '@take-out/helpers'
|
|
3
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { combineZeroClients } from './combineZeroClients'
|
|
6
|
+
import { createZeroClient } from './createZeroClient'
|
|
7
|
+
import { getInstanceForNamespace, registerClientInstance } from './instanceRegistry'
|
|
8
|
+
import { registerQuery } from './queryRegistry'
|
|
9
|
+
import { run } from './run'
|
|
10
|
+
import { setRunner, type ZeroRunner } from './zeroRunner'
|
|
11
|
+
|
|
12
|
+
import type { ZeroEvent } from './types'
|
|
13
|
+
import type { AnyQueryRegistry, Query } from '@rocicorp/zero'
|
|
14
|
+
import type { ReactNode } from 'react'
|
|
15
|
+
|
|
16
|
+
const userTable = table('user').columns({ id: string(), name: string() }).primaryKey('id')
|
|
17
|
+
const taskTable = table('task')
|
|
18
|
+
.columns({ id: string(), title: string() })
|
|
19
|
+
.primaryKey('id')
|
|
20
|
+
const schema = createSchema({ tables: [userTable, taskTable] })
|
|
21
|
+
|
|
22
|
+
// query fns are never invoked in these tests (runners are mocked, and
|
|
23
|
+
// defineQueries resolves to lazy QueryRequests), so the body is a stub
|
|
24
|
+
const makeQueryFn = () => (args: { id: string }) =>
|
|
25
|
+
args as unknown as Query<'user', typeof schema>
|
|
26
|
+
|
|
27
|
+
// each test uses unique instance names + namespaces — the instance registry
|
|
28
|
+
// is module-global and shared across tests in this file
|
|
29
|
+
function makeClient(instanceName: string, namespace: string) {
|
|
30
|
+
const byId = makeQueryFn()
|
|
31
|
+
return {
|
|
32
|
+
byId,
|
|
33
|
+
client: createZeroClient({
|
|
34
|
+
schema,
|
|
35
|
+
models: {},
|
|
36
|
+
groupedQueries: { [namespace]: { byId } },
|
|
37
|
+
instanceName,
|
|
38
|
+
}),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// a structural stand-in for a createZeroClient return value, registered in
|
|
43
|
+
// the instance registry so the facade can resolve namespace ownership.
|
|
44
|
+
// real clients can't be used for mutate/useQuery dispatch tests because
|
|
45
|
+
// their zero proxy requires a mounted ZeroProvider.
|
|
46
|
+
function fakeClient(name: string, namespaces: string[], zeroStub: unknown) {
|
|
47
|
+
registerClientInstance({
|
|
48
|
+
name,
|
|
49
|
+
namespaces,
|
|
50
|
+
// boundary stub: dispatch tests never resolve queries through it
|
|
51
|
+
customQueries: {} as AnyQueryRegistry,
|
|
52
|
+
})
|
|
53
|
+
return {
|
|
54
|
+
instanceName: name,
|
|
55
|
+
useQuery: vi.fn(() => `${name}-useQuery`),
|
|
56
|
+
useQueryDirect: vi.fn(() => `${name}-useQueryDirect`),
|
|
57
|
+
usePermission: vi.fn(() => `${name}-usePermission`),
|
|
58
|
+
usePermissionDirect: vi.fn(() => `${name}-usePermissionDirect`),
|
|
59
|
+
zero: zeroStub,
|
|
60
|
+
preload: vi.fn(() => `${name}-preload`),
|
|
61
|
+
getQuery: vi.fn(() => `${name}-getQuery`),
|
|
62
|
+
zeroEvents: createEmitter<ZeroEvent | null>(`zero:test-${name}`, null),
|
|
63
|
+
ControlQueries: ({ children }: { children: ReactNode }) => children,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('multi-instance namespace dispatch', () => {
|
|
68
|
+
test('run() dispatches named queries to the owning instance runner', async () => {
|
|
69
|
+
const control = makeClient('run-control', 'runUser')
|
|
70
|
+
const project = makeClient('run-project', 'runTask')
|
|
71
|
+
|
|
72
|
+
// simulate each instance's provider mount (SetZeroInstance)
|
|
73
|
+
const controlRunner = vi.fn(async (..._args: unknown[]) => ({ from: 'control' }))
|
|
74
|
+
const projectRunner = vi.fn(async (..._args: unknown[]) => ({ from: 'project' }))
|
|
75
|
+
getInstanceForNamespace('runUser')!.runner = controlRunner as ZeroRunner
|
|
76
|
+
getInstanceForNamespace('runTask')!.runner = projectRunner as ZeroRunner
|
|
77
|
+
|
|
78
|
+
// the last mount also claims the ambient runner — owned namespaces must
|
|
79
|
+
// not use it (this is the "second mount steals run()" bug)
|
|
80
|
+
const ambientRunner = vi.fn(async (..._args: unknown[]) => ({ from: 'ambient' }))
|
|
81
|
+
setRunner(ambientRunner as ZeroRunner)
|
|
82
|
+
|
|
83
|
+
await expect(run(control.byId, { id: '1' })).resolves.toEqual({ from: 'control' })
|
|
84
|
+
await expect(run(project.byId, { id: '2' })).resolves.toEqual({ from: 'project' })
|
|
85
|
+
expect(ambientRunner).not.toHaveBeenCalled()
|
|
86
|
+
|
|
87
|
+
// the request resolved through the owning instance's own query registry
|
|
88
|
+
const request = controlRunner.mock.calls[0]![0] as { query: { queryName: string } }
|
|
89
|
+
expect(request.query.queryName).toBe('runUser.byId')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('a claimed namespace with an unmounted instance uses the ambient runner (server path)', async () => {
|
|
93
|
+
const { byId } = makeClient('srv-instance', 'srvThing')
|
|
94
|
+
const ambient = vi.fn(async (..._args: unknown[]) => ({ from: 'ambient' }))
|
|
95
|
+
setRunner(ambient as ZeroRunner)
|
|
96
|
+
|
|
97
|
+
await expect(run(byId, { id: '1' })).resolves.toEqual({ from: 'ambient' })
|
|
98
|
+
expect(ambient).toHaveBeenCalledTimes(1)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('duplicate namespace claim throws at create time', () => {
|
|
102
|
+
makeClient('dup-a', 'dupNs')
|
|
103
|
+
expect(() => makeClient('dup-b', 'dupNs')).toThrow(/already claimed/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('re-creating an instance under the same name re-claims without throwing (hmr)', () => {
|
|
107
|
+
makeClient('hmr', 'hmrNs')
|
|
108
|
+
expect(() => makeClient('hmr', 'hmrNs')).not.toThrow()
|
|
109
|
+
expect(getInstanceForNamespace('hmrNs')?.name).toBe('hmr')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('model namespaces are claimed too', () => {
|
|
113
|
+
const models = { mdlThing: { mutate: { insert: async () => {} } } }
|
|
114
|
+
createZeroClient({
|
|
115
|
+
schema,
|
|
116
|
+
models,
|
|
117
|
+
groupedQueries: {},
|
|
118
|
+
instanceName: 'mdl-a',
|
|
119
|
+
})
|
|
120
|
+
expect(getInstanceForNamespace('mdlThing')?.name).toBe('mdl-a')
|
|
121
|
+
expect(() =>
|
|
122
|
+
createZeroClient({
|
|
123
|
+
schema,
|
|
124
|
+
models,
|
|
125
|
+
groupedQueries: {},
|
|
126
|
+
instanceName: 'mdl-b',
|
|
127
|
+
}),
|
|
128
|
+
).toThrow(/already claimed/)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('multi-instance isolation', () => {
|
|
133
|
+
test('each instance has an isolated zeroEvents emitter', () => {
|
|
134
|
+
const { client: a } = makeClient('emit-a', 'emitA')
|
|
135
|
+
const { client: b } = makeClient('emit-b', 'emitB')
|
|
136
|
+
|
|
137
|
+
expect(a.zeroEvents).not.toBe(b.zeroEvents)
|
|
138
|
+
expect(a.zeroEvents.options?.name).toBe('zero:emit-a')
|
|
139
|
+
expect(b.zeroEvents.options?.name).toBe('zero:emit-b')
|
|
140
|
+
|
|
141
|
+
const seenByB: Array<ZeroEvent | null> = []
|
|
142
|
+
b.zeroEvents.listen((event) => seenByB.push(event))
|
|
143
|
+
a.zeroEvents.emit({ type: 'error', message: 'a-only' })
|
|
144
|
+
|
|
145
|
+
expect(seenByB).toEqual([])
|
|
146
|
+
expect(a.zeroEvents.value).toEqual({ type: 'error', message: 'a-only' })
|
|
147
|
+
expect(b.zeroEvents.value).toBe(null)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('the default instance keeps the legacy emitter name', () => {
|
|
151
|
+
const { client } = makeClient('default', 'defaultNs')
|
|
152
|
+
expect(client.zeroEvents.options?.name).toBe('zero')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('each instance has its own unmounted zero proxy', () => {
|
|
156
|
+
const { client: a } = makeClient('proxy-a', 'proxyA')
|
|
157
|
+
const { client: b } = makeClient('proxy-b', 'proxyB')
|
|
158
|
+
// identity check without handing the proxy to expect() — vitest's
|
|
159
|
+
// thenable detection would access .then and trip the unmounted throw
|
|
160
|
+
expect(a.zero === (b.zero as unknown)).toBe(false)
|
|
161
|
+
// neither provider is mounted; each proxy throws its own error rather
|
|
162
|
+
// than resolving against some shared/global instance
|
|
163
|
+
expect(() => a.zero.clientID).toThrow(/not initialized/)
|
|
164
|
+
expect(() => b.zero.clientID).toThrow(/not initialized/)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('combineZeroClients facade', () => {
|
|
169
|
+
test('zero.mutate dispatches by model namespace, rest forwards to primary', () => {
|
|
170
|
+
const insertSpy = vi.fn()
|
|
171
|
+
const updateSpy = vi.fn()
|
|
172
|
+
const control = fakeClient('fz-control', ['fzUser'], {
|
|
173
|
+
mutate: { fzUser: { insert: insertSpy } },
|
|
174
|
+
userID: 'primary-user',
|
|
175
|
+
})
|
|
176
|
+
const project = fakeClient('fz-project', ['fzTask'], {
|
|
177
|
+
mutate: { fzTask: { update: updateSpy } },
|
|
178
|
+
userID: 'project-user',
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const combined = combineZeroClients(control, project)
|
|
182
|
+
const zero = combined.zero as Record<string, any>
|
|
183
|
+
|
|
184
|
+
zero.mutate.fzUser.insert({ id: '1' })
|
|
185
|
+
zero.mutate.fzTask.update({ id: '2' })
|
|
186
|
+
|
|
187
|
+
expect(insertSpy).toHaveBeenCalledWith({ id: '1' })
|
|
188
|
+
expect(updateSpy).toHaveBeenCalledWith({ id: '2' })
|
|
189
|
+
expect(zero.userID).toBe('primary-user')
|
|
190
|
+
// unclaimed namespaces fall back to the primary instance
|
|
191
|
+
expect(zero.mutate.unknownNs).toBe(undefined)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('useQuery/preload/getQuery/usePermission dispatch by namespace', () => {
|
|
195
|
+
const control = fakeClient('fq-control', ['fqUser'], {})
|
|
196
|
+
const project = fakeClient('fq-project', ['fqTask'], {})
|
|
197
|
+
const userQuery = makeQueryFn()
|
|
198
|
+
const taskQuery = makeQueryFn()
|
|
199
|
+
registerQuery(userQuery, 'fqUser.byId')
|
|
200
|
+
registerQuery(taskQuery, 'fqTask.byId')
|
|
201
|
+
|
|
202
|
+
// project is last → inner: its hooks ride the upstream context path;
|
|
203
|
+
// control is outer → its hooks go through the context-free direct path
|
|
204
|
+
const combined = combineZeroClients(control, project)
|
|
205
|
+
const useQuery = combined.useQuery as (...args: any[]) => any
|
|
206
|
+
const preload = combined.preload as (...args: any[]) => any
|
|
207
|
+
const getQuery = combined.getQuery as (...args: any[]) => any
|
|
208
|
+
const usePermission = combined.usePermission as (...args: any[]) => any
|
|
209
|
+
|
|
210
|
+
useQuery(userQuery, { id: '1' })
|
|
211
|
+
expect(control.useQueryDirect).toHaveBeenCalledWith(userQuery, { id: '1' })
|
|
212
|
+
expect(control.useQuery).not.toHaveBeenCalled()
|
|
213
|
+
expect(project.useQuery).not.toHaveBeenCalled()
|
|
214
|
+
|
|
215
|
+
useQuery(taskQuery, { id: '2' })
|
|
216
|
+
expect(project.useQuery).toHaveBeenCalledWith(taskQuery, { id: '2' })
|
|
217
|
+
expect(project.useQueryDirect).not.toHaveBeenCalled()
|
|
218
|
+
|
|
219
|
+
preload(taskQuery, { id: '3' })
|
|
220
|
+
expect(project.preload).toHaveBeenCalledWith(taskQuery, { id: '3' })
|
|
221
|
+
expect(control.preload).not.toHaveBeenCalled()
|
|
222
|
+
|
|
223
|
+
getQuery(userQuery, { id: '4' })
|
|
224
|
+
expect(control.getQuery).toHaveBeenCalledWith(userQuery, { id: '4' })
|
|
225
|
+
expect(project.getQuery).not.toHaveBeenCalled()
|
|
226
|
+
|
|
227
|
+
// unregistered query fns fall back to the primary instance (outer → direct)
|
|
228
|
+
const anonymous = makeQueryFn()
|
|
229
|
+
useQuery(anonymous, { id: '5' })
|
|
230
|
+
expect(control.useQueryDirect).toHaveBeenCalledWith(anonymous, { id: '5' })
|
|
231
|
+
|
|
232
|
+
// usePermission dispatches by table-named model namespace and follows the
|
|
233
|
+
// same context vs direct split: inner table → context, outer table → direct
|
|
234
|
+
usePermission('fqTask', 'row-1')
|
|
235
|
+
expect(project.usePermission).toHaveBeenCalledWith('fqTask', 'row-1')
|
|
236
|
+
expect(project.usePermissionDirect).not.toHaveBeenCalled()
|
|
237
|
+
|
|
238
|
+
usePermission('fqUser', 'row-2')
|
|
239
|
+
expect(control.usePermissionDirect).toHaveBeenCalledWith('fqUser', 'row-2')
|
|
240
|
+
expect(control.usePermission).not.toHaveBeenCalled()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('the inner option overrides which client uses the context path', () => {
|
|
244
|
+
const a = fakeClient('inner-a', ['innerA'], {})
|
|
245
|
+
const b = fakeClient('inner-b', ['innerB'], {})
|
|
246
|
+
const aQuery = makeQueryFn()
|
|
247
|
+
const bQuery = makeQueryFn()
|
|
248
|
+
registerQuery(aQuery, 'innerA.byId')
|
|
249
|
+
registerQuery(bQuery, 'innerB.byId')
|
|
250
|
+
|
|
251
|
+
// a is FIRST (primary) but declared inner → context path; b becomes direct
|
|
252
|
+
const combined = combineZeroClients(a, b, { inner: 'inner-a' })
|
|
253
|
+
const useQuery = combined.useQuery as (...args: any[]) => any
|
|
254
|
+
|
|
255
|
+
useQuery(aQuery, { id: '1' })
|
|
256
|
+
expect(a.useQuery).toHaveBeenCalledWith(aQuery, { id: '1' })
|
|
257
|
+
expect(a.useQueryDirect).not.toHaveBeenCalled()
|
|
258
|
+
|
|
259
|
+
useQuery(bQuery, { id: '2' })
|
|
260
|
+
expect(b.useQueryDirect).toHaveBeenCalledWith(bQuery, { id: '2' })
|
|
261
|
+
expect(b.useQuery).not.toHaveBeenCalled()
|
|
262
|
+
|
|
263
|
+
expect(() => combineZeroClients(a, b, { inner: 'nope' })).toThrow(
|
|
264
|
+
/not one of the passed clients/,
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('combined zeroEvents relays every instance', () => {
|
|
269
|
+
const { client: a } = makeClient('relay-a', 'relayA')
|
|
270
|
+
const { client: b } = makeClient('relay-b', 'relayB')
|
|
271
|
+
|
|
272
|
+
const combined = combineZeroClients(a, b)
|
|
273
|
+
const seen: Array<ZeroEvent | null> = []
|
|
274
|
+
combined.zeroEvents.listen((event) => seen.push(event))
|
|
275
|
+
|
|
276
|
+
a.zeroEvents.emit({ type: 'error', message: 'from-a' })
|
|
277
|
+
b.zeroEvents.emit({ type: 'error', message: 'from-b' })
|
|
278
|
+
|
|
279
|
+
expect(seen.map((event) => event?.message)).toEqual(['from-a', 'from-b'])
|
|
280
|
+
// the source emitters stay independent of each other
|
|
281
|
+
expect(a.zeroEvents.value?.message).toBe('from-a')
|
|
282
|
+
expect(b.zeroEvents.value?.message).toBe('from-b')
|
|
283
|
+
})
|
|
284
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// real nested-provider tests for combineZeroClients: zero-react's useQuery
|
|
4
|
+
// resolves its zero instance from the NEAREST ZeroProvider context, so under
|
|
5
|
+
// nested providers an outer instance's queries must NOT ride the context
|
|
6
|
+
// path — they must materialize directly on the owning instance.
|
|
7
|
+
|
|
8
|
+
import { createSchema, string, table } from '@rocicorp/zero'
|
|
9
|
+
import { act } from 'react'
|
|
10
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
11
|
+
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
12
|
+
|
|
13
|
+
import { combineZeroClients } from './combineZeroClients'
|
|
14
|
+
import { createZeroClient } from './createZeroClient'
|
|
15
|
+
import { zql } from './zql'
|
|
16
|
+
|
|
17
|
+
import type { MutatorContext } from './types'
|
|
18
|
+
import type { ReactNode } from 'react'
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
// eslint-disable-next-line no-var
|
|
22
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined
|
|
23
|
+
}
|
|
24
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
|
25
|
+
|
|
26
|
+
const userTable = table('user').columns({ id: string(), name: string() }).primaryKey('id')
|
|
27
|
+
const taskTable = table('task')
|
|
28
|
+
.columns({ id: string(), title: string() })
|
|
29
|
+
.primaryKey('id')
|
|
30
|
+
const schema = createSchema({ tables: [userTable, taskTable] })
|
|
31
|
+
|
|
32
|
+
type UserRow = { id: string; name: string }
|
|
33
|
+
|
|
34
|
+
// control = OUTER/primary instance — owns the 'user' model namespace and the
|
|
35
|
+
// 'ctlUser' query namespace
|
|
36
|
+
const controlModels = {
|
|
37
|
+
user: {
|
|
38
|
+
mutate: {
|
|
39
|
+
seed: async (ctx: MutatorContext, row?: UserRow) => {
|
|
40
|
+
await (ctx.tx.mutate as any).user.upsert(row)
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
const ctlUserById = (args: { id: string }) => (zql as any).user.where('id', args.id).one()
|
|
46
|
+
|
|
47
|
+
const control = createZeroClient({
|
|
48
|
+
schema,
|
|
49
|
+
models: controlModels,
|
|
50
|
+
groupedQueries: { ctlUser: { byId: ctlUserById } },
|
|
51
|
+
instanceName: 'nested-control',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// project = INNER instance — its own store. its seed mutator writes the same
|
|
55
|
+
// user TABLE (tables are physical, only namespaces are claims) so the two
|
|
56
|
+
// stores can hold conflicting rows for the same id.
|
|
57
|
+
const projectModels = {
|
|
58
|
+
prjSeed: {
|
|
59
|
+
mutate: {
|
|
60
|
+
seedUser: async (ctx: MutatorContext, row?: UserRow) => {
|
|
61
|
+
await (ctx.tx.mutate as any).user.upsert(row)
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
const prjTaskById = (args: { id: string }) => (zql as any).task.where('id', args.id).one()
|
|
67
|
+
|
|
68
|
+
const project = createZeroClient({
|
|
69
|
+
schema,
|
|
70
|
+
models: projectModels,
|
|
71
|
+
groupedQueries: { prjTask: { byId: prjTaskById } },
|
|
72
|
+
instanceName: 'nested-project',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// default contract: last argument = inner provider
|
|
76
|
+
const combined = combineZeroClients(control, project)
|
|
77
|
+
|
|
78
|
+
let root: Root
|
|
79
|
+
let container: HTMLElement
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
resetProbe()
|
|
83
|
+
container = document.createElement('div')
|
|
84
|
+
document.body.appendChild(container)
|
|
85
|
+
root = createRoot(container)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
await act(async () => root.unmount())
|
|
90
|
+
container.remove()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const render = (ui: ReactNode) => act(async () => root.render(ui))
|
|
94
|
+
|
|
95
|
+
async function waitFor(condition: () => boolean, what: string) {
|
|
96
|
+
for (let i = 0; i < 200; i++) {
|
|
97
|
+
if (condition()) return
|
|
98
|
+
await act(async () => {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`timed out waiting for ${what}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const probe: { data: UserRow | undefined; renders: number } = {
|
|
106
|
+
data: undefined,
|
|
107
|
+
renders: 0,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// reset via helper so the assignments don't narrow probe.data to undefined
|
|
111
|
+
// in test scope
|
|
112
|
+
function resetProbe() {
|
|
113
|
+
probe.data = undefined
|
|
114
|
+
probe.renders = 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ControlUserProbe({ id }: { id: string }) {
|
|
118
|
+
const [data] = (combined.useQuery as any)(ctlUserById, { id })
|
|
119
|
+
probe.data = data ?? undefined
|
|
120
|
+
probe.renders++
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const seedControl = async (row: UserRow) => {
|
|
125
|
+
await act(async () => {
|
|
126
|
+
await (control.zero.mutate as any).user.seed(row).client
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const seedProject = async (row: UserRow) => {
|
|
131
|
+
await act(async () => {
|
|
132
|
+
await (project.zero.mutate as any).prjSeed.seedUser(row).client
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
test('outer-instance query reads the OUTER store under nested providers', async () => {
|
|
137
|
+
await render(
|
|
138
|
+
<control.ProvideZero server={null} userID="t1-ctl">
|
|
139
|
+
<project.ProvideZero server={null} userID="t1-prj">
|
|
140
|
+
<ControlUserProbe id="u1" />
|
|
141
|
+
</project.ProvideZero>
|
|
142
|
+
</control.ProvideZero>,
|
|
143
|
+
)
|
|
144
|
+
await waitFor(() => probe.renders > 0, 'probe mount')
|
|
145
|
+
|
|
146
|
+
// same row id, conflicting values per store — whichever store the view
|
|
147
|
+
// materialized against decides the rendered name
|
|
148
|
+
await seedControl({ id: 'u1', name: 'from-control' })
|
|
149
|
+
await seedProject({ id: 'u1', name: 'from-project' })
|
|
150
|
+
|
|
151
|
+
await waitFor(() => probe.data !== undefined, 'control row visible')
|
|
152
|
+
expect(probe.data?.name).toBe('from-control')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('inner provider unmount/remount does not break outer-instance subscriptions', async () => {
|
|
156
|
+
const App = ({ showInner }: { showInner: boolean }) => (
|
|
157
|
+
<control.ProvideZero server={null} userID="t2-ctl">
|
|
158
|
+
{showInner ? (
|
|
159
|
+
<project.ProvideZero server={null} userID="t2-prj">
|
|
160
|
+
<ControlUserProbe id="u2" />
|
|
161
|
+
</project.ProvideZero>
|
|
162
|
+
) : (
|
|
163
|
+
<ControlUserProbe id="u2" />
|
|
164
|
+
)}
|
|
165
|
+
</control.ProvideZero>
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
await render(<App showInner />)
|
|
169
|
+
await waitFor(() => probe.renders > 0, 'probe mount')
|
|
170
|
+
|
|
171
|
+
await seedControl({ id: 'u2', name: 'v1' })
|
|
172
|
+
await waitFor(() => probe.data?.name === 'v1', 'v1 visible')
|
|
173
|
+
|
|
174
|
+
// unmount the inner provider (probe remounts outside it)
|
|
175
|
+
await render(<App showInner={false} />)
|
|
176
|
+
await seedControl({ id: 'u2', name: 'v2' })
|
|
177
|
+
await waitFor(() => probe.data?.name === 'v2', 'v2 visible after inner unmount')
|
|
178
|
+
|
|
179
|
+
// remount the inner provider
|
|
180
|
+
await render(<App showInner />)
|
|
181
|
+
await seedControl({ id: 'u2', name: 'v3' })
|
|
182
|
+
await waitFor(() => probe.data?.name === 'v3', 'v3 visible after inner remount')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('direct-path views re-materialize when the owning instance rotates', async () => {
|
|
186
|
+
const App = ({ userID }: { userID: string }) => (
|
|
187
|
+
<control.ProvideZero server={null} userID={userID}>
|
|
188
|
+
<ControlUserProbe id="u3" />
|
|
189
|
+
</control.ProvideZero>
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
await render(<App userID="rot-1" />)
|
|
193
|
+
await waitFor(() => probe.renders > 0, 'probe mount')
|
|
194
|
+
|
|
195
|
+
await seedControl({ id: 'u3', name: 'before-rotation' })
|
|
196
|
+
await waitFor(() => probe.data?.name === 'before-rotation', 'pre-rotation row')
|
|
197
|
+
|
|
198
|
+
// identity change rotates the mounted zero instance (new client group,
|
|
199
|
+
// fresh store) — the same mechanism a zeroInstanceGeneration bump uses
|
|
200
|
+
await render(<App userID="rot-2" />)
|
|
201
|
+
await waitFor(() => probe.data === undefined, 'view reset to the new empty store')
|
|
202
|
+
|
|
203
|
+
await seedControl({ id: 'u3', name: 'after-rotation' })
|
|
204
|
+
await waitFor(() => probe.data?.name === 'after-rotation', 'post-rotation row')
|
|
205
|
+
})
|
package/src/run.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getInstanceForQueryFn } from './instanceRegistry'
|
|
1
2
|
import { resolveQuery, type PlainQueryFn } from './resolveQuery'
|
|
2
3
|
import { getRunner } from './zeroRunner'
|
|
3
4
|
|
|
@@ -62,7 +63,6 @@ export function run(
|
|
|
62
63
|
const hasParams = modeArg !== undefined || (paramsOrMode && paramsOrMode !== 'complete')
|
|
63
64
|
const params = hasParams ? paramsOrMode : undefined
|
|
64
65
|
const mode = hasParams ? modeArg : paramsOrMode
|
|
65
|
-
const runner = getRunner()
|
|
66
66
|
const options =
|
|
67
67
|
mode === 'complete'
|
|
68
68
|
? ({
|
|
@@ -72,13 +72,14 @@ export function run(
|
|
|
72
72
|
|
|
73
73
|
if (queryOrFn && queryOrFn['ast']) {
|
|
74
74
|
// inline zql - on client it only resolves against cache, on server fully
|
|
75
|
-
return
|
|
75
|
+
return getRunner()(queryOrFn, options)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// with multiple client instances mounted, a named query executes against
|
|
79
|
+
// the instance that claimed its namespace, not whichever mounted last
|
|
80
|
+
const instance = getInstanceForQueryFn(queryOrFn)
|
|
81
|
+
const customQueries = instance?.customQueries ?? getCustomQueries()
|
|
79
82
|
const queryRequest = resolveQuery({ customQueries, fn: queryOrFn, params })
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return out
|
|
84
|
+
return getRunner(instance)(queryRequest as any, options)
|
|
84
85
|
}
|
package/src/zeroRunner.ts
CHANGED
|
@@ -20,11 +20,17 @@ export function setRunner(r: ZeroRunner) {
|
|
|
20
20
|
runner = r
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function getRunner(): ZeroRunner {
|
|
23
|
+
export function getRunner(instance?: { runner: ZeroRunner | null }): ZeroRunner {
|
|
24
24
|
if (isInZeroMutation()) {
|
|
25
25
|
return (q, o) => mutatorContext().tx.run(q, o)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// a mounted instance's own runner wins; otherwise the ambient runner
|
|
29
|
+
// (single-instance client, or the server transaction runner)
|
|
30
|
+
if (instance?.runner) {
|
|
31
|
+
return instance.runner
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
if (!runner) {
|
|
29
35
|
throw new Error(
|
|
30
36
|
'Zero runner not initialized. Ensure ProvideZero is mounted (client) or createZeroServer is called (server).',
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Emitter } from '@take-out/helpers';
|
|
2
|
+
import { run } from './run';
|
|
3
|
+
import type { ZeroEvent } from './types';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
type ControlQueriesProps = {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
action?: 'enable' | 'disable';
|
|
8
|
+
whenDisabled?: 'empty' | 'last-value';
|
|
9
|
+
};
|
|
10
|
+
type CombinableZeroClient = {
|
|
11
|
+
instanceName: string;
|
|
12
|
+
useQuery: (...args: any[]) => any;
|
|
13
|
+
useQueryDirect: (...args: any[]) => any;
|
|
14
|
+
usePermission: (...args: any[]) => any;
|
|
15
|
+
usePermissionDirect: (...args: any[]) => any;
|
|
16
|
+
zero: any;
|
|
17
|
+
preload: (...args: any[]) => any;
|
|
18
|
+
getQuery: (...args: any[]) => any;
|
|
19
|
+
zeroEvents: Emitter<ZeroEvent | null>;
|
|
20
|
+
ControlQueries: (props: ControlQueriesProps) => ReactNode;
|
|
21
|
+
};
|
|
22
|
+
export type CombineZeroClientsOptions = {
|
|
23
|
+
inner?: string;
|
|
24
|
+
};
|
|
25
|
+
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
26
|
+
export type CombinedZeroClients<Clients extends readonly CombinableZeroClient[]> = {
|
|
27
|
+
useQuery: UnionToIntersection<Clients[number]['useQuery']>;
|
|
28
|
+
usePermission: UnionToIntersection<Clients[number]['usePermission']>;
|
|
29
|
+
zero: UnionToIntersection<Clients[number]['zero']>;
|
|
30
|
+
preload: UnionToIntersection<Clients[number]['preload']>;
|
|
31
|
+
getQuery: UnionToIntersection<Clients[number]['getQuery']>;
|
|
32
|
+
run: typeof run;
|
|
33
|
+
zeroEvents: Emitter<ZeroEvent | null>;
|
|
34
|
+
ControlQueries: (props: ControlQueriesProps) => ReactNode;
|
|
35
|
+
};
|
|
36
|
+
export declare function combineZeroClients<const Clients extends readonly [CombinableZeroClient, ...CombinableZeroClient[]]>(...clientsAndOptions: [...Clients] | [...Clients, CombineZeroClientsOptions]): CombinedZeroClients<Clients>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=combineZeroClients.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combineZeroClients.d.ts","sourceRoot":"","sources":["../src/combineZeroClients.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAG/D,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAE3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAsBtC,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,SAAS,CAAA;IACnB,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IAC7B,YAAY,CAAC,EAAE,OAAO,GAAG,YAAY,CAAA;CACtC,CAAA;AAID,KAAK,oBAAoB,GAAG;IAC1B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IACjC,cAAc,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IACvC,aAAa,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IACtC,mBAAmB,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IAC5C,IAAI,EAAE,GAAG,CAAA;IACT,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IAChC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IACjC,UAAU,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IACrC,cAAc,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,SAAS,CAAA;CAC1D,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IAGtC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,GAAG,KAAK,CAAC,SAAS,CAC7E,CAAC,EAAE,MAAM,CAAC,KACP,IAAI,GACL,CAAC,GACD,KAAK,CAAA;AAET,MAAM,MAAM,mBAAmB,CAAC,OAAO,SAAS,SAAS,oBAAoB,EAAE,IAAI;IACjF,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;IAC1D,aAAa,EAAE,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAA;IACpE,IAAI,EAAE,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;IACxD,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;IAC1D,GAAG,EAAE,OAAO,GAAG,CAAA;IACf,UAAU,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IACrC,cAAc,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,SAAS,CAAA;CAC1D,CAAA;AAED,wBAAgB,kBAAkB,CAChC,KAAK,CAAC,OAAO,SAAS,SAAS,CAAC,oBAAoB,EAAE,GAAG,oBAAoB,EAAE,CAAC,EAEhF,GAAG,iBAAiB,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,EAAE,yBAAyB,CAAC,GAC3E,mBAAmB,CAAC,OAAO,CAAC,CA6G9B"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useQuery as zeroUseQuery } from '@rocicorp/zero/react';
|
|
2
|
+
import { type Emitter } from '@take-out/helpers';
|
|
2
3
|
import { type Context } from 'react';
|
|
3
4
|
import { type PlainQueryFn } from './resolveQuery';
|
|
4
5
|
import type { AnyQueryRegistry, HumanReadable, Query, Schema as ZeroSchema } from '@rocicorp/zero';
|
|
@@ -18,4 +19,18 @@ export declare function createUseQuery<Schema extends ZeroSchema>({ DisabledCont
|
|
|
18
19
|
DisabledContext: Context<QueryControlMode>;
|
|
19
20
|
customQueries: AnyQueryRegistry;
|
|
20
21
|
}): UseQueryHook<Schema>;
|
|
22
|
+
type MaterializableZero = {
|
|
23
|
+
materialize(query: any, options?: {
|
|
24
|
+
ttl?: any;
|
|
25
|
+
}): {
|
|
26
|
+
addListener(cb: (data: any, resultType: string) => void): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export declare function createUseQueryDirect<Schema extends ZeroSchema>({ DisabledContext, customQueries, getZero, zeroVersion, }: {
|
|
31
|
+
DisabledContext: Context<QueryControlMode>;
|
|
32
|
+
customQueries: AnyQueryRegistry;
|
|
33
|
+
getZero: () => MaterializableZero | null;
|
|
34
|
+
zeroVersion: Emitter<number>;
|
|
35
|
+
}): UseQueryHook<Schema>;
|
|
21
36
|
//# sourceMappingURL=createUseQuery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,
|
|
1
|
+
{"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAML,KAAK,OAAO,EACb,MAAM,OAAO,CAAA;AAGd,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAEhE,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,KAAK,EACL,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAGvB,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,YAAY,CAAA;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;CAC9C,CAAA;AAED,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,MAAM,MAAM,WAAW,CAAC,OAAO,IAAI,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAA;AAExF,YAAY,EAAE,YAAY,EAAE,CAAA;AAE5B,MAAM,MAAM,YAAY,CAAC,MAAM,SAAS,UAAU,IAAI;IAEpD,CAAC,IAAI,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EAC5D,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,MAAM,EAAE,IAAI,EACZ,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAC;IAGxB,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EACtD,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAA;CACxB,CAAA;AAmBD,wBAAgB,cAAc,CAAC,MAAM,SAAS,UAAU,EAAE,EACxD,eAAe,EACf,aAAa,GACd,EAAE;IACD,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;CAChC,GAAG,YAAY,CAAC,MAAM,CAAC,CAkCvB;AAoCD,KAAK,kBAAkB,GAAG;IACxB,WAAW,CACT,KAAK,EAAE,GAAG,EACV,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,GACtB;QACD,WAAW,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAA;QAC9D,OAAO,IAAI,IAAI,CAAA;KAChB,CAAA;CACF,CAAA;AAsCD,wBAAgB,oBAAoB,CAAC,MAAM,SAAS,UAAU,EAAE,EAC9D,eAAe,EACf,aAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE;IACD,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;IAE/B,OAAO,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAA;IAExC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7B,GAAG,YAAY,CAAC,MAAM,CAAC,CAwDvB"}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
import { Zero as ZeroClient } from '@rocicorp/zero';
|
|
1
2
|
import { type ReactNode } from 'react';
|
|
3
|
+
import { type UseQueryHook } from './createUseQuery';
|
|
2
4
|
import { resolveQuery, type PlainQueryFn } from './resolveQuery';
|
|
3
5
|
import type { AuthData, GenericModels, GetZeroMutators, ZeroEvent } from './types';
|
|
4
|
-
import type { Query, Row,
|
|
6
|
+
import type { Query, Row, ZeroOptions, Schema as ZeroSchema } from '@rocicorp/zero';
|
|
5
7
|
type PreloadOptions = {
|
|
6
8
|
ttl?: 'always' | 'never' | number | undefined;
|
|
7
9
|
};
|
|
8
10
|
export type GroupedQueries = Record<string, Record<string, (...args: any[]) => any>>;
|
|
9
11
|
export type PermissionStrategy = 'optimistic' | 'optimistic-deny' | 'optimistic-allow';
|
|
10
|
-
export declare function createZeroClient<Schema extends ZeroSchema, Models extends GenericModels>({ schema, models, groupedQueries, permissionStrategy, }: {
|
|
12
|
+
export declare function createZeroClient<Schema extends ZeroSchema, Models extends GenericModels>({ schema, models, groupedQueries, permissionStrategy, instanceName, }: {
|
|
11
13
|
schema: Schema;
|
|
12
14
|
models: Models;
|
|
13
15
|
groupedQueries: GroupedQueries;
|
|
14
16
|
permissionStrategy?: PermissionStrategy;
|
|
17
|
+
instanceName?: string;
|
|
15
18
|
}): {
|
|
19
|
+
instanceName: string;
|
|
16
20
|
zeroEvents: import("@take-out/helpers").Emitter<ZeroEvent | null>;
|
|
17
21
|
ProvideZero: ({ children, authData: authDataIn, disable, ...props }: Omit<ZeroOptions<Schema, GetZeroMutators<Models>>, "schema" | "mutators"> & {
|
|
18
22
|
children: ReactNode;
|
|
@@ -24,9 +28,11 @@ export declare function createZeroClient<Schema extends ZeroSchema, Models exten
|
|
|
24
28
|
action?: "enable" | "disable";
|
|
25
29
|
whenDisabled?: "empty" | "last-value";
|
|
26
30
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
27
|
-
useQuery:
|
|
31
|
+
useQuery: UseQueryHook<Schema>;
|
|
32
|
+
useQueryDirect: UseQueryHook<Schema>;
|
|
28
33
|
usePermission: (table: (keyof Schema["tables"] & string) | (string & {}), objOrId: string | Partial<Row<any>> | undefined, enabled?: boolean, debug?: boolean) => boolean | null;
|
|
29
|
-
|
|
34
|
+
usePermissionDirect: (table: (keyof Schema["tables"] & string) | (string & {}), objOrId: string | Partial<Row<any>> | undefined, enabled?: boolean, debug?: boolean) => boolean | null;
|
|
35
|
+
zero: ZeroClient<Schema, GetZeroMutators<Models>, unknown>;
|
|
30
36
|
preload: {
|
|
31
37
|
<TArg, TTable extends keyof Schema["tables"] & string, TReturn>(fn: PlainQueryFn<TArg, Query<TTable, Schema, TReturn>>, params: TArg, options?: PreloadOptions): {
|
|
32
38
|
cleanup: () => void;
|