Skip to content

Commit b3bdf99

Browse files
committed
enable lazy creation of the stateful singleton in createZero
1 parent c83abc0 commit b3bdf99

File tree

4 files changed

+134
-40
lines changed

4 files changed

+134
-40
lines changed

src/create-zero.test.ts

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { createSchema, number, string, table, Zero } from '@rocicorp/zero'
1+
import { createBuilder, createSchema, number, string, syncedQuery, table, Zero } from '@rocicorp/zero'
22
import { assert, describe, expect, it } from 'vitest'
3-
import { computed, ref } from 'vue'
3+
import { computed, nextTick, ref } from 'vue'
44
import { createZero } from './create-zero'
55

66
const testSchema = createSchema({
@@ -24,8 +24,8 @@ describe('createZero', () => {
2424
})
2525

2626
const zero = useZero()
27-
assert(zero?.value)
28-
expect(zero?.value.userID).toEqual('test-user')
27+
assert(zero.value)
28+
expect(zero.value.userID).toEqual('test-user')
2929
})
3030

3131
it('accepts Zero instance instead of options', () => {
@@ -38,7 +38,7 @@ describe('createZero', () => {
3838
const { useZero } = createZero({ zero })
3939

4040
const usedZero = useZero()
41-
assert(usedZero?.value)
41+
assert(usedZero.value)
4242
expect(usedZero.value).toEqual(zero)
4343
})
4444

@@ -54,20 +54,60 @@ describe('createZero', () => {
5454
const { useZero } = createZero(zeroOptions)
5555

5656
const zero = useZero()
57-
assert(zero?.value)
57+
assert(zero.value)
5858

5959
expect(zero.value.userID).toEqual('test-user')
6060

6161
const oldZero = zero.value
6262

6363
userID.value = 'test-user-2'
64-
await 1
64+
await nextTick()
6565

6666
expect(zero.value.userID).toEqual('test-user-2')
6767
expect(zero.value.closed).toBe(false)
6868
expect(oldZero.closed).toBe(true)
6969
})
7070

71+
it('useQuery works whithout explicitly calling useZero', async () => {
72+
const z = new Zero({
73+
userID: 'test-user',
74+
server: null,
75+
schema: testSchema,
76+
kvStore: 'mem' as const,
77+
})
78+
79+
await z.mutate.test.insert({ id: 1, name: 'test1' })
80+
await z.mutate.test.insert({ id: 2, name: 'test2' })
81+
82+
const builder = createBuilder(testSchema)
83+
const byIdQuery = syncedQuery(
84+
'byId',
85+
([id]) => {
86+
if (typeof id !== 'number') {
87+
throw new TypeError('id must be a number')
88+
}
89+
return [id] as const
90+
},
91+
(id: number) => {
92+
return builder.test.where('id', id)
93+
},
94+
)
95+
96+
const { useQuery } = createZero({
97+
zero: z,
98+
})
99+
100+
const { data } = useQuery(() => byIdQuery(1))
101+
expect(data.value).toMatchInlineSnapshot(`
102+
[
103+
{
104+
"id": 1,
105+
"name": "test1",
106+
Symbol(rc): 1,
107+
},
108+
]`)
109+
})
110+
71111
it('updates when Zero instance changes', async () => {
72112
const userID = ref('test-user')
73113

@@ -87,10 +127,48 @@ describe('createZero', () => {
87127
const oldZero = usedZero.value
88128

89129
userID.value = 'test-user-2'
90-
await 1
130+
await nextTick()
91131

92132
expect(usedZero.value.userID).toEqual('test-user-2')
93133
expect(usedZero.value.closed).toBe(false)
94134
expect(oldZero.closed).toBe(true)
95135
})
136+
137+
it('is created lazily and once', async () => {
138+
const z = new Zero({
139+
userID: 'test-user',
140+
server: null,
141+
schema: testSchema,
142+
kvStore: 'mem' as const,
143+
})
144+
145+
let zeroAccessCount = 0
146+
const accessCountPerCreation = 2
147+
148+
const proxiedOpts = new Proxy(
149+
{ zero: z },
150+
{
151+
get(target, prop) {
152+
if (prop === 'zero') {
153+
zeroAccessCount++
154+
}
155+
return target[prop as keyof typeof target]
156+
},
157+
},
158+
)
159+
160+
const { useZero } = createZero(proxiedOpts)
161+
162+
expect(zeroAccessCount).toBe(0)
163+
164+
useZero()
165+
expect(zeroAccessCount).toBe(accessCountPerCreation)
166+
167+
await nextTick()
168+
expect(zeroAccessCount).toBe(accessCountPerCreation)
169+
170+
useZero()
171+
await nextTick()
172+
expect(zeroAccessCount).toBe(accessCountPerCreation)
173+
})
96174
})

src/create-zero.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,25 @@ const zeroCleanups = new Set()
1010
export function createZero<S extends Schema = Schema, MD extends CustomMutatorDefs | undefined = undefined>(optsOrZero: MaybeRefOrGetter<ZeroOptions<S, MD> | { zero: Zero<S, MD> }>) {
1111
const z = shallowRef() as ShallowRef<Zero<S, MD>>
1212

13-
const opts = toValue(optsOrZero)
14-
z.value = 'zero' in opts ? opts.zero : new Zero(opts)
13+
function useZero() {
14+
if (z.value) {
15+
return z
16+
}
1517

16-
watch(() => toValue(optsOrZero), (opts) => {
17-
const cleanupZeroPromise = z.value.close()
18-
zeroCleanups.add(cleanupZeroPromise)
19-
cleanupZeroPromise.finally(() => {
20-
zeroCleanups.delete(cleanupZeroPromise)
21-
})
18+
watch(() => toValue(optsOrZero), (opts) => {
19+
if (z.value && !z.value.closed) {
20+
const cleanupZeroPromise = z.value.close()
21+
zeroCleanups.add(cleanupZeroPromise)
22+
cleanupZeroPromise.finally(() => {
23+
zeroCleanups.delete(cleanupZeroPromise)
24+
})
25+
}
2226

23-
z.value = 'zero' in opts ? opts.zero : new Zero(opts)
24-
}, { deep: true })
27+
z.value = 'zero' in opts ? opts.zero : new Zero(opts)
28+
}, { deep: true, immediate: true })
29+
30+
return z
31+
}
2532

2633
function useQuery<
2734
TTable extends keyof S['tables'] & string,
@@ -30,11 +37,12 @@ export function createZero<S extends Schema = Schema, MD extends CustomMutatorDe
3037
query: MaybeRefOrGetter<Query<S, TTable, TReturn>>,
3138
options?: MaybeRefOrGetter<UseQueryOptions>,
3239
): QueryResult<TReturn> {
40+
const z = useZero()
3341
return useQueryWithZero(query, options, z)
3442
}
3543

3644
return {
37-
useZero: () => z,
45+
useZero,
3846
useQuery,
3947
}
4048
}

src/query.test.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import type { TTL } from '@rocicorp/zero'
22
import type { MockInstance } from 'vitest'
3-
import { createBuilder, createSchema, number, string, syncedQuery, table } from '@rocicorp/zero'
3+
import { createBuilder, createSchema, number, string, syncedQuery, table, Zero } from '@rocicorp/zero'
44
import { describe, expect, it, vi } from 'vitest'
5-
import { ref, watchEffect } from 'vue'
5+
import { nextTick, ref, watchEffect } from 'vue'
66
import { createZero } from './create-zero'
77
import { useQuery } from './query'
88
import { VueView, vueViewFactory } from './view'
99

10-
async function setupTestEnvironment() {
11-
const schema = createSchema({
12-
tables: [
13-
table('table')
14-
.columns({
15-
a: number(),
16-
b: string(),
17-
})
18-
.primaryKey('a'),
19-
],
20-
})
10+
const schema = createSchema({
11+
tables: [
12+
table('table')
13+
.columns({
14+
a: number(),
15+
b: string(),
16+
})
17+
.primaryKey('a'),
18+
],
19+
})
2120

21+
async function setupTestEnvironment() {
2222
const userID = ref('asdf')
2323

2424
const { useZero, useQuery } = createZero(() => ({
@@ -72,7 +72,7 @@ describe('useQuery', () => {
7272
expect(status.value).toEqual('unknown')
7373

7474
await z.value.mutate.table.insert({ a: 3, b: 'c' })
75-
await 1
75+
await nextTick()
7676

7777
expect(rows.value).toMatchInlineSnapshot(`[
7878
{
@@ -123,7 +123,7 @@ describe('useQuery', () => {
123123
materializeSpy.mockClear()
124124

125125
ttl.value = '10m'
126-
await 1
126+
await nextTick()
127127

128128
expect(materializeSpy).toHaveBeenCalledTimes(0)
129129
expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m')
@@ -176,7 +176,7 @@ describe('useQuery', () => {
176176
materializeSpy.mockClear()
177177

178178
ttl.value = '10m'
179-
await 1
179+
await nextTick()
180180

181181
expect(materializeSpy).toHaveBeenCalledTimes(0)
182182
expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m')
@@ -225,7 +225,7 @@ describe('useQuery', () => {
225225
resetLogs()
226226

227227
a.value = 2
228-
await 1
228+
await nextTick()
229229

230230
expect(rowLog).toMatchInlineSnapshot(`[
231231
[
@@ -320,9 +320,16 @@ describe('useQuery', () => {
320320
})
321321

322322
it('can still be used without createZero', async () => {
323-
const { z, tableQuery } = await setupTestEnvironment()
323+
const z = new Zero({
324+
userID: 'test-user',
325+
server: null,
326+
schema,
327+
kvStore: 'mem' as const,
328+
})
329+
await z.mutate.table.insert({ a: 1, b: 'a' })
330+
await z.mutate.table.insert({ a: 2, b: 'b' })
324331

325-
const { data: rows, status } = useQuery(() => tableQuery)
332+
const { data: rows, status } = useQuery(() => z.query.table)
326333
expect(rows.value).toMatchInlineSnapshot(`[
327334
{
328335
"a": 1,
@@ -337,6 +344,6 @@ describe('useQuery', () => {
337344
]`)
338345
expect(status.value).toEqual('unknown')
339346

340-
z.value.close()
347+
z.close()
341348
})
342349
})

src/view.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Zero,
1111
} from '@rocicorp/zero'
1212
import { describe, expect, it, vi } from 'vitest'
13+
import { nextTick } from 'vue'
1314
import { VueView, vueViewFactory } from './view'
1415

1516
const simpleSchema = createSchema({
@@ -1275,7 +1276,7 @@ describe('vueView', () => {
12751276
expect(view.status).toEqual('unknown')
12761277

12771278
queryCompleteResolver.resolve(true)
1278-
await 1
1279+
await nextTick()
12791280
expect(view.status).toEqual('complete')
12801281

12811282
z.close()

0 commit comments

Comments
 (0)