@automerge/automerge-repo-solid-primitives 2.2.0
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/.assets/automerge.png +0 -0
- package/.assets/solid.png +0 -0
- package/.github/workflows/tests.yml +28 -0
- package/.github/workflows/typedoc.yml +53 -0
- package/LICENSE +21 -0
- package/dist/autoproduce.d.ts +13 -0
- package/dist/context.d.ts +9 -0
- package/dist/createDocumentProjection.d.ts +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +912 -0
- package/dist/makeDocumentProjection.d.ts +8 -0
- package/dist/types.d.ts +7 -0
- package/dist/useDocHandle.d.ts +15 -0
- package/dist/useDocument.d.ts +9 -0
- package/dist/useRepo.d.ts +4 -0
- package/package.json +47 -0
- package/readme.md +139 -0
- package/src/autoproduce.ts +18 -0
- package/src/context.ts +11 -0
- package/src/createDocumentProjection.ts +19 -0
- package/src/index.ts +7 -0
- package/src/makeDocumentProjection.ts +73 -0
- package/src/types.ts +9 -0
- package/src/useDocHandle.ts +90 -0
- package/src/useDocument.ts +21 -0
- package/src/useRepo.ts +10 -0
- package/test/createDocumentProjection.test.tsx +394 -0
- package/test/makeDocumentProjection.test.tsx +290 -0
- package/test/useDocHandle.test.tsx +183 -0
- package/test/useDocument.test.tsx +337 -0
- package/test/useRepo.test.tsx +34 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.json +11 -0
- package/tsconfig.test.json +20 -0
- package/typedoc.json +5 -0
- package/vite.config.ts +41 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Repo,
|
|
3
|
+
type PeerId,
|
|
4
|
+
type AutomergeUrl,
|
|
5
|
+
type DocHandle,
|
|
6
|
+
} from "@automerge/automerge-repo"
|
|
7
|
+
import { render, renderHook, testEffect } from "@solidjs/testing-library"
|
|
8
|
+
import { describe, expect, it, vi } from "vitest"
|
|
9
|
+
import { RepoContext } from "../src/context.js"
|
|
10
|
+
import {
|
|
11
|
+
createEffect,
|
|
12
|
+
createSignal,
|
|
13
|
+
type Accessor,
|
|
14
|
+
type ParentComponent,
|
|
15
|
+
} from "solid-js"
|
|
16
|
+
import useDocHandle from "../src/useDocHandle.js"
|
|
17
|
+
import createDocumentProjection from "../src/createDocumentProjection.js"
|
|
18
|
+
|
|
19
|
+
describe("createDocumentProjection", () => {
|
|
20
|
+
function setup() {
|
|
21
|
+
const repo = new Repo({
|
|
22
|
+
peerId: "bob" as PeerId,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const create = () =>
|
|
26
|
+
repo.create<ExampleDoc>({
|
|
27
|
+
key: "value",
|
|
28
|
+
array: [1, 2, 3],
|
|
29
|
+
hellos: [{ hello: "world" }, { hello: "hedgehog" }],
|
|
30
|
+
projects: [
|
|
31
|
+
{ title: "one", items: [{ title: "go shopping" }] },
|
|
32
|
+
{ title: "two", items: [] },
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const handle = create()
|
|
37
|
+
const wrapper: ParentComponent = props => {
|
|
38
|
+
return (
|
|
39
|
+
<RepoContext.Provider value={repo}>
|
|
40
|
+
{props.children}
|
|
41
|
+
</RepoContext.Provider>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
repo,
|
|
47
|
+
handle,
|
|
48
|
+
wrapper,
|
|
49
|
+
create,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it("should notify on a property change", async () => {
|
|
54
|
+
const { handle } = setup()
|
|
55
|
+
const { result: doc, owner } = renderHook(
|
|
56
|
+
createDocumentProjection<ExampleDoc>,
|
|
57
|
+
{
|
|
58
|
+
initialProps: [() => handle],
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const done = testEffect(done => {
|
|
63
|
+
createEffect((run: number = 0) => {
|
|
64
|
+
if (run == 0) {
|
|
65
|
+
expect(doc()?.key).toBe("value")
|
|
66
|
+
handle.change(doc => (doc.key = "hello world!"))
|
|
67
|
+
} else if (run == 1) {
|
|
68
|
+
expect(doc()?.key).toBe("hello world!")
|
|
69
|
+
handle.change(doc => (doc.key = "friday night!"))
|
|
70
|
+
} else if (run == 2) {
|
|
71
|
+
expect(doc()?.key).toBe("friday night!")
|
|
72
|
+
done()
|
|
73
|
+
}
|
|
74
|
+
return run + 1
|
|
75
|
+
})
|
|
76
|
+
}, owner!)
|
|
77
|
+
return done
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("should not apply patches multiple times just because there are multiple projections", async () => {
|
|
81
|
+
const { handle } = setup()
|
|
82
|
+
const { result: one, owner: owner1 } = renderHook(
|
|
83
|
+
createDocumentProjection<ExampleDoc>,
|
|
84
|
+
{
|
|
85
|
+
initialProps: [() => handle],
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
const { result: two, owner: owner2 } = renderHook(
|
|
89
|
+
createDocumentProjection<ExampleDoc>,
|
|
90
|
+
{
|
|
91
|
+
initialProps: [() => handle],
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const done2 = testEffect(done => {
|
|
96
|
+
createEffect((run: number = 0) => {
|
|
97
|
+
if (run == 0) {
|
|
98
|
+
expect(two()?.array).toEqual([1, 2, 3])
|
|
99
|
+
} else if (run == 1) {
|
|
100
|
+
expect(two()?.array).toEqual([1, 2, 3, 4])
|
|
101
|
+
} else if (run == 2) {
|
|
102
|
+
expect(two()?.array).toEqual([1, 2, 3, 4, 5])
|
|
103
|
+
done()
|
|
104
|
+
}
|
|
105
|
+
return run + 1
|
|
106
|
+
})
|
|
107
|
+
}, owner2!)
|
|
108
|
+
|
|
109
|
+
const done1 = testEffect(done => {
|
|
110
|
+
createEffect((run: number = 0) => {
|
|
111
|
+
if (run == 0) {
|
|
112
|
+
expect(one()?.array).toEqual([1, 2, 3])
|
|
113
|
+
handle.change(doc => doc.array.push(4))
|
|
114
|
+
} else if (run == 1) {
|
|
115
|
+
expect(one()?.array).toEqual([1, 2, 3, 4])
|
|
116
|
+
handle.change(doc => doc.array.push(5))
|
|
117
|
+
} else if (run == 2) {
|
|
118
|
+
expect(one()?.array).toEqual([1, 2, 3, 4, 5])
|
|
119
|
+
done()
|
|
120
|
+
}
|
|
121
|
+
return run + 1
|
|
122
|
+
})
|
|
123
|
+
}, owner1!)
|
|
124
|
+
|
|
125
|
+
return Promise.allSettled([done1, done2])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("should work with useDocHandle", async () => {
|
|
129
|
+
const {
|
|
130
|
+
handle: { url: startingUrl },
|
|
131
|
+
wrapper,
|
|
132
|
+
} = setup()
|
|
133
|
+
|
|
134
|
+
const [url, setURL] = createSignal<AutomergeUrl>()
|
|
135
|
+
|
|
136
|
+
const { result: handle } = renderHook(useDocHandle<ExampleDoc>, {
|
|
137
|
+
initialProps: [url],
|
|
138
|
+
wrapper,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const { result: doc, owner } = renderHook(
|
|
142
|
+
createDocumentProjection<ExampleDoc>,
|
|
143
|
+
{
|
|
144
|
+
initialProps: [handle],
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const done = testEffect(done => {
|
|
149
|
+
createEffect((run: number = 0) => {
|
|
150
|
+
if (run == 0) {
|
|
151
|
+
expect(doc()?.key).toBe(undefined)
|
|
152
|
+
setURL(startingUrl)
|
|
153
|
+
} else if (run == 1) {
|
|
154
|
+
expect(doc()?.key).toBe("value")
|
|
155
|
+
handle()?.change(doc => (doc.key = "hello world!"))
|
|
156
|
+
} else if (run == 2) {
|
|
157
|
+
expect(doc()?.key).toBe("hello world!")
|
|
158
|
+
handle()?.change(doc => (doc.key = "friday night!"))
|
|
159
|
+
} else if (run == 3) {
|
|
160
|
+
expect(doc()?.key).toBe("friday night!")
|
|
161
|
+
done()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return run + 1
|
|
165
|
+
})
|
|
166
|
+
}, owner!)
|
|
167
|
+
|
|
168
|
+
return done
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("should work with a signal url", async () => {
|
|
172
|
+
const { create, wrapper } = setup()
|
|
173
|
+
const [url, setURL] = createSignal<AutomergeUrl>()
|
|
174
|
+
const { result: handle } = renderHook(useDocHandle<ExampleDoc>, {
|
|
175
|
+
initialProps: [url],
|
|
176
|
+
wrapper,
|
|
177
|
+
})
|
|
178
|
+
const { result: doc, owner } = renderHook(
|
|
179
|
+
createDocumentProjection<ExampleDoc>,
|
|
180
|
+
{
|
|
181
|
+
initialProps: [handle],
|
|
182
|
+
wrapper,
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
const done = testEffect(done => {
|
|
186
|
+
createEffect((run: number = 0) => {
|
|
187
|
+
if (run == 0) {
|
|
188
|
+
expect(doc()?.key).toBe(undefined)
|
|
189
|
+
setURL(create().url)
|
|
190
|
+
} else if (run == 1) {
|
|
191
|
+
expect(doc()?.key).toBe("value")
|
|
192
|
+
handle()?.change(doc => (doc.key = "hello world!"))
|
|
193
|
+
} else if (run == 2) {
|
|
194
|
+
expect(doc()?.key).toBe("hello world!")
|
|
195
|
+
setURL(create().url)
|
|
196
|
+
} else if (run == 3) {
|
|
197
|
+
expect(doc()?.key).toBe("value")
|
|
198
|
+
handle()?.change(doc => (doc.key = "friday night!"))
|
|
199
|
+
} else if (run == 4) {
|
|
200
|
+
expect(doc()?.key).toBe("friday night!")
|
|
201
|
+
done()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return run + 1
|
|
205
|
+
})
|
|
206
|
+
}, owner!)
|
|
207
|
+
return done
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it("should clear the store when the signal returns to nothing", async () => {
|
|
211
|
+
const { create, wrapper } = setup()
|
|
212
|
+
const [url, setURL] = createSignal<AutomergeUrl>()
|
|
213
|
+
const { result: handle } = renderHook(useDocHandle<ExampleDoc>, {
|
|
214
|
+
initialProps: [url],
|
|
215
|
+
wrapper,
|
|
216
|
+
})
|
|
217
|
+
const { result: doc, owner } = renderHook(
|
|
218
|
+
createDocumentProjection<ExampleDoc>,
|
|
219
|
+
{
|
|
220
|
+
initialProps: [handle],
|
|
221
|
+
wrapper,
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const done = testEffect(done => {
|
|
226
|
+
createEffect((run: number = 0) => {
|
|
227
|
+
if (run == 0) {
|
|
228
|
+
expect(doc()?.key).toBe(undefined)
|
|
229
|
+
setURL(create().url)
|
|
230
|
+
} else if (run == 1) {
|
|
231
|
+
expect(doc()?.key).toBe("value")
|
|
232
|
+
setURL(undefined)
|
|
233
|
+
} else if (run == 2) {
|
|
234
|
+
expect(doc()?.key).toBe(undefined)
|
|
235
|
+
setURL(create().url)
|
|
236
|
+
} else if (run == 3) {
|
|
237
|
+
expect(doc()?.key).toBe("value")
|
|
238
|
+
done()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return run + 1
|
|
242
|
+
})
|
|
243
|
+
}, owner!)
|
|
244
|
+
return done
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("should not return the wrong store when handle changes", async () => {
|
|
248
|
+
const { create } = setup()
|
|
249
|
+
|
|
250
|
+
const h1 = create()
|
|
251
|
+
const h2 = create()
|
|
252
|
+
|
|
253
|
+
const [stableHandle] = createSignal(h1)
|
|
254
|
+
// initially handle2 is the same as handle1
|
|
255
|
+
const [changingHandle, setChangingHandle] = createSignal(h1)
|
|
256
|
+
|
|
257
|
+
const { result } = renderHook<[], () => readonly [string, string]>(() => {
|
|
258
|
+
function Component(props: {
|
|
259
|
+
stableHandle: Accessor<DocHandle<ExampleDoc>>
|
|
260
|
+
changingHandle: Accessor<DocHandle<ExampleDoc>>
|
|
261
|
+
}) {
|
|
262
|
+
const stableDoc = createDocumentProjection<ExampleDoc>(
|
|
263
|
+
// eslint-disable-next-line solid/reactivity
|
|
264
|
+
props.stableHandle
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const changingDoc = createDocumentProjection<ExampleDoc>(
|
|
268
|
+
// eslint-disable-next-line solid/reactivity
|
|
269
|
+
props.changingHandle
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return () => [stableDoc()!.key, changingDoc()!.key] as const
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Component({
|
|
276
|
+
stableHandle,
|
|
277
|
+
changingHandle,
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return testEffect(async done => {
|
|
282
|
+
h2.change(doc => (doc.key = "document-2"))
|
|
283
|
+
expect(result()).toEqual(["value", "value"])
|
|
284
|
+
|
|
285
|
+
h1.change(doc => (doc.key = "hello"))
|
|
286
|
+
await new Promise<void>(setImmediate)
|
|
287
|
+
expect(result()).toEqual(["hello", "hello"])
|
|
288
|
+
|
|
289
|
+
setChangingHandle(() => h2)
|
|
290
|
+
expect(result()).toEqual(["hello", "document-2"])
|
|
291
|
+
|
|
292
|
+
setChangingHandle(() => h1)
|
|
293
|
+
expect(result()).toEqual(["hello", "hello"])
|
|
294
|
+
|
|
295
|
+
setChangingHandle(h2)
|
|
296
|
+
h2.change(doc => (doc.key = "world"))
|
|
297
|
+
await new Promise<void>(setImmediate)
|
|
298
|
+
expect(result()).toEqual(["hello", "world"])
|
|
299
|
+
done()
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it("should work ok with a slow handle", async () => {
|
|
304
|
+
const { repo } = setup()
|
|
305
|
+
|
|
306
|
+
const originalFind = repo.find.bind(repo)
|
|
307
|
+
repo.find = vi.fn().mockImplementation(async (...args) => {
|
|
308
|
+
await new Promise(resolve => setTimeout(resolve, 900))
|
|
309
|
+
// @ts-expect-error this is ok
|
|
310
|
+
return originalFind(...args)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
await testEffect(done => {
|
|
314
|
+
const handle = useDocHandle<{ im: "slow" }>(
|
|
315
|
+
() => repo.create({ im: "slow" }).url,
|
|
316
|
+
{ repo }
|
|
317
|
+
)
|
|
318
|
+
const doc = createDocumentProjection(handle)
|
|
319
|
+
|
|
320
|
+
createEffect((run: number = 0) => {
|
|
321
|
+
if (run == 0) {
|
|
322
|
+
expect(doc()?.im).toBe("slow")
|
|
323
|
+
done()
|
|
324
|
+
}
|
|
325
|
+
return run + 1
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
repo.find = originalFind
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("should not notify on properties nobody cares about", async () => {
|
|
333
|
+
const { handle } = setup()
|
|
334
|
+
let fn = vi.fn()
|
|
335
|
+
|
|
336
|
+
const { result: doc, owner } = renderHook(
|
|
337
|
+
createDocumentProjection<ExampleDoc>,
|
|
338
|
+
{
|
|
339
|
+
initialProps: [() => handle],
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
testEffect(() => {
|
|
343
|
+
createEffect(() => {
|
|
344
|
+
fn(doc()?.projects[1].title)
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
const arrayDotThree = testEffect(done => {
|
|
348
|
+
createEffect((run: number = 0) => {
|
|
349
|
+
if (run == 0) {
|
|
350
|
+
expect(doc()?.array[3]).toBeUndefined()
|
|
351
|
+
handle.change(doc => (doc.array[2] = 22))
|
|
352
|
+
handle.change(doc => (doc.key = "hello world!"))
|
|
353
|
+
handle.change(doc => (doc.array[1] = 11))
|
|
354
|
+
handle.change(doc => (doc.array[3] = 145))
|
|
355
|
+
} else if (run == 1) {
|
|
356
|
+
expect(doc()?.array[3]).toBe(145)
|
|
357
|
+
handle.change(doc => (doc.projects[0].title = "hello world!"))
|
|
358
|
+
handle.change(
|
|
359
|
+
doc => (doc.projects[0].items[0].title = "hello world!")
|
|
360
|
+
)
|
|
361
|
+
handle.change(doc => (doc.array[3] = 147))
|
|
362
|
+
} else if (run == 2) {
|
|
363
|
+
expect(doc()?.array[3]).toBe(147)
|
|
364
|
+
done()
|
|
365
|
+
}
|
|
366
|
+
return run + 1
|
|
367
|
+
})
|
|
368
|
+
}, owner!)
|
|
369
|
+
const projectZeroItemZeroTitle = testEffect(done => {
|
|
370
|
+
createEffect((run: number = 0) => {
|
|
371
|
+
if (run == 0) {
|
|
372
|
+
expect(doc()?.projects[0].items[0].title).toBe("hello world!")
|
|
373
|
+
done()
|
|
374
|
+
}
|
|
375
|
+
return run + 1
|
|
376
|
+
})
|
|
377
|
+
}, owner!)
|
|
378
|
+
|
|
379
|
+
expect(fn).toHaveBeenCalledOnce()
|
|
380
|
+
expect(fn).toHaveBeenCalledWith("two")
|
|
381
|
+
|
|
382
|
+
return Promise.all([arrayDotThree, projectZeroItemZeroTitle])
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
interface ExampleDoc {
|
|
387
|
+
key: string
|
|
388
|
+
array: number[]
|
|
389
|
+
hellos: { hello: string }[]
|
|
390
|
+
projects: {
|
|
391
|
+
title: string
|
|
392
|
+
items: { title: string; complete?: number }[]
|
|
393
|
+
}[]
|
|
394
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { type PeerId, Repo, type DocHandle } from "@automerge/automerge-repo"
|
|
2
|
+
|
|
3
|
+
import { renderHook, testEffect } from "@solidjs/testing-library"
|
|
4
|
+
import { describe, expect, it, vi } from "vitest"
|
|
5
|
+
import {
|
|
6
|
+
createEffect,
|
|
7
|
+
createRoot,
|
|
8
|
+
createSignal,
|
|
9
|
+
type ParentComponent,
|
|
10
|
+
} from "solid-js"
|
|
11
|
+
import makeDocumentProjection from "../src/makeDocumentProjection.js"
|
|
12
|
+
import { RepoContext } from "../src/context.js"
|
|
13
|
+
|
|
14
|
+
describe("makeDocumentProjection", () => {
|
|
15
|
+
function setup() {
|
|
16
|
+
const repo = new Repo({
|
|
17
|
+
peerId: "bob" as PeerId,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const create = () =>
|
|
21
|
+
repo.create<ExampleDoc>({
|
|
22
|
+
key: "value",
|
|
23
|
+
array: [1, 2, 3],
|
|
24
|
+
hellos: [{ hello: "world" }, { hello: "hedgehog" }],
|
|
25
|
+
projects: [
|
|
26
|
+
{ title: "one", items: [{ title: "go shopping" }] },
|
|
27
|
+
{ title: "two", items: [] },
|
|
28
|
+
],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const handle = create()
|
|
32
|
+
const wrapper: ParentComponent = props => {
|
|
33
|
+
return (
|
|
34
|
+
<RepoContext.Provider value={repo}>
|
|
35
|
+
{props.children}
|
|
36
|
+
</RepoContext.Provider>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
repo,
|
|
42
|
+
handle,
|
|
43
|
+
wrapper,
|
|
44
|
+
create,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
it("should notify on a property change", async () => {
|
|
49
|
+
const { handle } = setup()
|
|
50
|
+
const { result: doc, owner } = renderHook(
|
|
51
|
+
makeDocumentProjection as (handle: DocHandle<ExampleDoc>) => ExampleDoc,
|
|
52
|
+
{
|
|
53
|
+
initialProps: [handle],
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const done = testEffect(done => {
|
|
58
|
+
createEffect((run: number = 0) => {
|
|
59
|
+
if (run == 0) {
|
|
60
|
+
expect(doc.key).toBe("value")
|
|
61
|
+
handle.change(doc => (doc.key = "hello world!"))
|
|
62
|
+
} else if (run == 1) {
|
|
63
|
+
expect(doc.key).toBe("hello world!")
|
|
64
|
+
handle.change(doc => (doc.key = "friday night!"))
|
|
65
|
+
} else if (run == 2) {
|
|
66
|
+
expect(doc.key).toBe("friday night!")
|
|
67
|
+
done()
|
|
68
|
+
}
|
|
69
|
+
return run + 1
|
|
70
|
+
})
|
|
71
|
+
}, owner!)
|
|
72
|
+
return done
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("should not apply patches multiple times just because there are multiple projections of the same handle", async () => {
|
|
76
|
+
const { handle } = setup()
|
|
77
|
+
const { result: one, owner: owner1 } = renderHook(
|
|
78
|
+
makeDocumentProjection as (handle: DocHandle<ExampleDoc>) => ExampleDoc,
|
|
79
|
+
{
|
|
80
|
+
initialProps: [handle],
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
const { result: two, owner: owner2 } = renderHook(
|
|
84
|
+
makeDocumentProjection as (handle: DocHandle<ExampleDoc>) => ExampleDoc,
|
|
85
|
+
{
|
|
86
|
+
initialProps: [handle],
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const done2 = testEffect(done => {
|
|
91
|
+
createEffect((run: number = 0) => {
|
|
92
|
+
if (run == 0) {
|
|
93
|
+
expect(two.array).toEqual([1, 2, 3])
|
|
94
|
+
} else if (run == 1) {
|
|
95
|
+
expect(two.array).toEqual([1, 2, 3, 4])
|
|
96
|
+
} else if (run == 2) {
|
|
97
|
+
expect(two.array).toEqual([1, 2, 3, 4, 5])
|
|
98
|
+
done()
|
|
99
|
+
}
|
|
100
|
+
return run + 1
|
|
101
|
+
})
|
|
102
|
+
}, owner2!)
|
|
103
|
+
|
|
104
|
+
const done1 = testEffect(done => {
|
|
105
|
+
createEffect((run: number = 0) => {
|
|
106
|
+
if (run == 0) {
|
|
107
|
+
expect(one.array).toEqual([1, 2, 3])
|
|
108
|
+
handle.change(doc => doc.array.push(4))
|
|
109
|
+
} else if (run == 1) {
|
|
110
|
+
expect(one.array).toEqual([1, 2, 3, 4])
|
|
111
|
+
handle.change(doc => doc.array.push(5))
|
|
112
|
+
} else if (run == 2) {
|
|
113
|
+
expect(one.array).toEqual([1, 2, 3, 4, 5])
|
|
114
|
+
done()
|
|
115
|
+
}
|
|
116
|
+
return run + 1
|
|
117
|
+
})
|
|
118
|
+
}, owner1!)
|
|
119
|
+
|
|
120
|
+
return Promise.allSettled([done1, done2])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should notify on a deep property change", async () => {
|
|
124
|
+
const { handle } = setup()
|
|
125
|
+
return createRoot(() => {
|
|
126
|
+
const doc = makeDocumentProjection<ExampleDoc>(handle)
|
|
127
|
+
return testEffect(done => {
|
|
128
|
+
createEffect((run: number = 0) => {
|
|
129
|
+
if (run == 0) {
|
|
130
|
+
expect(doc.projects[0].title).toBe("one")
|
|
131
|
+
handle.change(doc => (doc.projects[0].title = "hello world!"))
|
|
132
|
+
} else if (run == 1) {
|
|
133
|
+
expect(doc.projects[0].title).toBe("hello world!")
|
|
134
|
+
handle.change(doc => (doc.projects[0].title = "friday night!"))
|
|
135
|
+
} else if (run == 2) {
|
|
136
|
+
expect(doc.projects[0].title).toBe("friday night!")
|
|
137
|
+
done()
|
|
138
|
+
}
|
|
139
|
+
return run + 1
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("should not clean up when it should not clean up", async () => {
|
|
146
|
+
const { handle } = setup()
|
|
147
|
+
|
|
148
|
+
return createRoot(() => {
|
|
149
|
+
const [one, clean1] = createRoot(c => [makeDocumentProjection(handle), c])
|
|
150
|
+
const [two, clean2] = createRoot(c => [makeDocumentProjection(handle), c])
|
|
151
|
+
const [three, clean3] = createRoot(c => [
|
|
152
|
+
makeDocumentProjection(handle),
|
|
153
|
+
c,
|
|
154
|
+
])
|
|
155
|
+
const [signal, setSignal] = createSignal(0)
|
|
156
|
+
return testEffect(done => {
|
|
157
|
+
createEffect((run: number = 0) => {
|
|
158
|
+
signal()
|
|
159
|
+
expect(one.projects[0].title).not.toBeUndefined()
|
|
160
|
+
expect(two.projects[0].title).not.toBeUndefined()
|
|
161
|
+
expect(three.projects[0].title).not.toBeUndefined()
|
|
162
|
+
|
|
163
|
+
if (run == 0) {
|
|
164
|
+
// immediately clean up the first projection. updates should
|
|
165
|
+
// carry on because there is still another reference
|
|
166
|
+
clean1()
|
|
167
|
+
expect(one.projects[0].title).toBe("one")
|
|
168
|
+
expect(two.projects[0].title).toBe("one")
|
|
169
|
+
expect(three.projects[0].title).toBe("one")
|
|
170
|
+
handle.change(doc => (doc.projects[0].title = "hello world!"))
|
|
171
|
+
} else if (run == 1) {
|
|
172
|
+
// clean up another projection. updates should carry on
|
|
173
|
+
// because there is still one left
|
|
174
|
+
clean3()
|
|
175
|
+
expect(one.projects[0].title).toBe("hello world!")
|
|
176
|
+
expect(two.projects[0].title).toBe("hello world!")
|
|
177
|
+
expect(three.projects[0].title).toBe("hello world!")
|
|
178
|
+
setSignal(1)
|
|
179
|
+
} else if (run == 2) {
|
|
180
|
+
// now all the stores are cleaned up so further updates
|
|
181
|
+
// should not show in the store
|
|
182
|
+
clean2()
|
|
183
|
+
setSignal(2)
|
|
184
|
+
} else if (run == 3) {
|
|
185
|
+
handle.change(doc => (doc.projects[0].title = "friday night!"))
|
|
186
|
+
// force the test to run again
|
|
187
|
+
setSignal(3)
|
|
188
|
+
} else if (run == 4) {
|
|
189
|
+
expect(one.projects[0].title).toBe("hello world!")
|
|
190
|
+
expect(two.projects[0].title).toBe("hello world!")
|
|
191
|
+
expect(three.projects[0].title).toBe("hello world!")
|
|
192
|
+
done()
|
|
193
|
+
}
|
|
194
|
+
return run + 1
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("should not notify on properties nobody cares about", async () => {
|
|
201
|
+
const { handle } = setup()
|
|
202
|
+
let fn = vi.fn()
|
|
203
|
+
|
|
204
|
+
const { result: doc, owner } = renderHook(
|
|
205
|
+
makeDocumentProjection as (handle: DocHandle<ExampleDoc>) => ExampleDoc,
|
|
206
|
+
{
|
|
207
|
+
initialProps: [handle],
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
testEffect(() => {
|
|
211
|
+
createEffect(() => {
|
|
212
|
+
fn(doc?.projects[1].title)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
const arrayDotThree = testEffect(done => {
|
|
216
|
+
createEffect((run: number = 0) => {
|
|
217
|
+
if (run == 0) {
|
|
218
|
+
expect(doc.array[3]).toBeUndefined()
|
|
219
|
+
|
|
220
|
+
handle.change(doc => (doc.array[2] = 22))
|
|
221
|
+
|
|
222
|
+
handle.change(doc => (doc.key = "hello world!"))
|
|
223
|
+
handle.change(doc => (doc.array[1] = 11))
|
|
224
|
+
handle.change(doc => (doc.array[3] = 145))
|
|
225
|
+
} else if (run == 1) {
|
|
226
|
+
expect(doc?.array[3]).toBe(145)
|
|
227
|
+
handle.change(doc => (doc.projects[0].title = "hello world!"))
|
|
228
|
+
handle.change(
|
|
229
|
+
doc => (doc.projects[0].items[0].title = "hello world!")
|
|
230
|
+
)
|
|
231
|
+
handle.change(doc => (doc.array[3] = 147))
|
|
232
|
+
} else if (run == 2) {
|
|
233
|
+
expect(doc?.array[3]).toBe(147)
|
|
234
|
+
done()
|
|
235
|
+
}
|
|
236
|
+
return run + 1
|
|
237
|
+
})
|
|
238
|
+
}, owner!)
|
|
239
|
+
const projectZeroItemZeroTitle = testEffect(done => {
|
|
240
|
+
createEffect((run: number = 0) => {
|
|
241
|
+
if (run == 0) {
|
|
242
|
+
expect(doc?.projects[0].items[0].title).toBe("hello world!")
|
|
243
|
+
done()
|
|
244
|
+
}
|
|
245
|
+
return run + 1
|
|
246
|
+
})
|
|
247
|
+
}, owner!)
|
|
248
|
+
|
|
249
|
+
expect(fn).toHaveBeenCalledOnce()
|
|
250
|
+
expect(fn).toHaveBeenCalledWith("two")
|
|
251
|
+
|
|
252
|
+
return Promise.all([arrayDotThree, projectZeroItemZeroTitle])
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("should remain reactive on an mount, unmount, and then remount of the same doc handle", async () => {
|
|
256
|
+
const { handle } = setup()
|
|
257
|
+
|
|
258
|
+
for (let i = 0; i < 2; ++i) {
|
|
259
|
+
const [doc, clean] = createRoot(c => [makeDocumentProjection(handle), c])
|
|
260
|
+
const lastRun = await testEffect<number>(done => {
|
|
261
|
+
createEffect((run: number = 0) => {
|
|
262
|
+
if (run == 0) {
|
|
263
|
+
expect(doc.key).toBe("value")
|
|
264
|
+
handle.change(doc => (doc.key = "hello world!"))
|
|
265
|
+
} else if (run == 1) {
|
|
266
|
+
expect(doc.key).toBe("hello world!")
|
|
267
|
+
handle.change(doc => (doc.key = "friday night!"))
|
|
268
|
+
} else if (run == 2) {
|
|
269
|
+
expect(doc.key).toBe("friday night!")
|
|
270
|
+
handle.change(doc => (doc.key = "value"))
|
|
271
|
+
done(run)
|
|
272
|
+
}
|
|
273
|
+
return run + 1
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
expect(lastRun).toBe(2)
|
|
277
|
+
clean()
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
interface ExampleDoc {
|
|
283
|
+
key: string
|
|
284
|
+
array: number[]
|
|
285
|
+
hellos: { hello: string }[]
|
|
286
|
+
projects: {
|
|
287
|
+
title: string
|
|
288
|
+
items: { title: string; complete?: number }[]
|
|
289
|
+
}[]
|
|
290
|
+
}
|