@automerge/automerge-repo-solid-primitives 2.5.4 → 2.5.6

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.
@@ -0,0 +1,10 @@
1
+ import { Accessor } from 'solid-js';
2
+ import { Doc, DocHandle } from '@automerge/automerge-repo/slim';
3
+
4
+ /**
5
+ * a light coarse-grained primitive when you care only _that_ a doc has changed,
6
+ * and not _how_.
7
+ * @param handle an Automerge
8
+ * [DocHandle](https://automerge.org/automerge-repo/classes/_automerge_automerge_repo.DocHandle.html)
9
+ */
10
+ export default function makeDocSignal<T extends object>(handle: DocHandle<T>): Accessor<Doc<T> | undefined>;
@@ -0,0 +1,10 @@
1
+ import { AutomergeUrl, Doc, DocHandle } from '@automerge/automerge-repo/slim';
2
+ import { MaybeAccessor, UseDocHandleOptions } from './types.js';
3
+ import { Accessor, Resource } from 'solid-js';
4
+
5
+ /**
6
+ * a light coarse-grained primitive when you care only _that_ a doc has changed,
7
+ * and not _how_. returns [doc, handle] from a URL.
8
+ * @param url a function that returns a url
9
+ */
10
+ export default function useDocSignal<T extends object>(url: MaybeAccessor<AutomergeUrl | undefined>, options?: UseDocHandleOptions): [Accessor<Doc<T> | undefined>, Resource<DocHandle<T> | undefined>];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-solid-primitives",
3
- "version": "2.5.4",
3
+ "version": "2.5.6",
4
4
  "description": "Access Automerge Repo in your SolidJS application",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,7 @@
14
14
  "license": "MIT",
15
15
  "devDependencies": {
16
16
  "@automerge/automerge": "3.0.0",
17
- "@automerge/automerge-repo": "2.5.4",
17
+ "@automerge/automerge-repo": "2.5.6",
18
18
  "@solidjs/testing-library": "^0.8.10",
19
19
  "@testing-library/jest-dom": "^6.6.3",
20
20
  "@testing-library/user-event": "^14.5.2",
@@ -43,5 +43,5 @@
43
43
  ]
44
44
  }
45
45
  },
46
- "gitHead": "70e3703e39f7151dbc446e865d9f9753f132ab3a"
46
+ "gitHead": "38d06b9d9ef0bbe1e0693d3b859015f38897f34b"
47
47
  }
package/readme.md CHANGED
@@ -75,6 +75,60 @@ const doc = makeDocumentProjection<{ items: { title: string }[] }>(handle)
75
75
  return <h1>{doc.items[1].title}</h1>
76
76
  ```
77
77
 
78
+ ## useDocSignal
79
+
80
+ A light coarse-grained primitive when you care only _that_ a doc has changed,
81
+ and not _how_. Returns `[doc, handle]`.
82
+
83
+ ```ts
84
+ useDocSignal<T>(
85
+ () => AutomergeURL,
86
+ options?: {repo: Repo}
87
+ ): [Accessor<Doc<T>>, Resource<DocHandle<T>>]
88
+ ```
89
+
90
+ ```tsx
91
+ // example
92
+ const [url, setURL] = createSignal<AutomergeUrl>(props.url)
93
+ const [doc, handle] = useDocSignal(url, { repo })
94
+
95
+ const inc = () => handle()?.change(d => d.count++)
96
+ return <button onclick={inc}>{doc()?.count}</button>
97
+ ```
98
+
99
+ The `{repo}` option can be left out if you are using [RepoContext](#repocontext).
100
+
101
+ ## createDocSignal
102
+
103
+ A light coarse-grained primitive when you care only _that_ a doc has changed,
104
+ and not _how_. Takes a signal `DocHandle`.
105
+
106
+ Underlying primitive for [`useDocSignal`](#usedocsignal).
107
+
108
+ Works with [`useDocHandle`](#usedochandle).
109
+
110
+ ```ts
111
+ createDocSignal<T>(() => DocHandle<T>): Accessor<Doc<T>>
112
+ ```
113
+
114
+ ## makeDocSignal
115
+
116
+ Just like `createDocSignal`, but without a reactive input.
117
+
118
+ Underlying primitive for [`createDocSignal`](#createdocsignal).
119
+
120
+ ```ts
121
+ makeDocSignal<T>(handle: DocHandle<T>): Accessor<Doc<T>>
122
+ ```
123
+
124
+ ```tsx
125
+ // example
126
+ const handle = repo.find(url)
127
+ const doc = makeDocSignal<{ count: number }>(handle)
128
+
129
+ return <span>{doc()?.count}</span>
130
+ ```
131
+
78
132
  ## useDocHandle
79
133
 
80
134
  Get a [DocHandle](https://automerge.org/docs/repositories/dochandles/) from the
@@ -0,0 +1,33 @@
1
+ import { createEffect, onCleanup, type Accessor } from "solid-js"
2
+ import { createSignal } from "solid-js"
3
+ import type { Doc, DocHandle } from "@automerge/automerge-repo/slim"
4
+
5
+ /**
6
+ * a light coarse-grained primitive when you care only _that_ a doc has changed,
7
+ * and not _how_. works with {@link useDocHandle}.
8
+ * @param handle an accessor (signal/resource) of a
9
+ * [DocHandle](https://automerge.org/automerge-repo/classes/_automerge_automerge_repo.DocHandle.html)
10
+ */
11
+ export default function createDocSignal<T extends object>(
12
+ handle: Accessor<DocHandle<T> | undefined>
13
+ ): Accessor<Doc<T> | undefined> {
14
+ const [signal, setSignal] = createSignal<Doc<T> | undefined>(handle()?.doc())
15
+
16
+ createEffect(() => {
17
+ const h = handle()
18
+
19
+ function update() {
20
+ setSignal(() => h?.doc() as Doc<T> | undefined)
21
+ }
22
+
23
+ // sync the signal with the current handle's doc
24
+ update()
25
+
26
+ if (h) {
27
+ h.on("change", update)
28
+ onCleanup(() => h.off("change", update))
29
+ }
30
+ })
31
+
32
+ return signal
33
+ }
package/src/index.ts CHANGED
@@ -3,5 +3,8 @@ export { default as useDocument } from "./useDocument.js"
3
3
  export { default as useDocHandle } from "./useDocHandle.js"
4
4
  export { default as makeDocumentProjection } from "./makeDocumentProjection.js"
5
5
  export { default as createDocumentProjection } from "./createDocumentProjection.js"
6
+ export { default as makeDocSignal } from "./makeDocSignal.js"
7
+ export { default as createDocSignal } from "./createDocSignal.js"
8
+ export { default as useDocSignal } from "./useDocSignal.js"
6
9
  export { default as useRepo } from "./useRepo.js"
7
10
  export { RepoContext } from "./context.js"
@@ -0,0 +1,24 @@
1
+ import { onCleanup, type Accessor } from "solid-js"
2
+ import { createSignal } from "solid-js"
3
+ import type { Doc, DocHandle } from "@automerge/automerge-repo/slim"
4
+
5
+ /**
6
+ * a light coarse-grained primitive when you care only _that_ a doc has changed,
7
+ * and not _how_.
8
+ * @param handle an Automerge
9
+ * [DocHandle](https://automerge.org/automerge-repo/classes/_automerge_automerge_repo.DocHandle.html)
10
+ */
11
+ export default function makeDocSignal<T extends object>(
12
+ handle: DocHandle<T>
13
+ ): Accessor<Doc<T> | undefined> {
14
+ const [signal, setSignal] = createSignal<Doc<T> | undefined>(handle.doc())
15
+
16
+ function update() {
17
+ setSignal(() => handle.doc() as Doc<T> | undefined)
18
+ }
19
+
20
+ handle.on("change", update)
21
+ onCleanup(() => handle.off("change", update))
22
+
23
+ return signal
24
+ }
@@ -0,0 +1,22 @@
1
+ import type {
2
+ AutomergeUrl,
3
+ Doc,
4
+ DocHandle,
5
+ } from "@automerge/automerge-repo/slim"
6
+ import useDocHandle from "./useDocHandle.js"
7
+ import createDocSignal from "./createDocSignal.js"
8
+ import type { MaybeAccessor, UseDocHandleOptions } from "./types.js"
9
+ import type { Accessor, Resource } from "solid-js"
10
+
11
+ /**
12
+ * a light coarse-grained primitive when you care only _that_ a doc has changed,
13
+ * and not _how_. returns [doc, handle] from a URL.
14
+ * @param url a function that returns a url
15
+ */
16
+ export default function useDocSignal<T extends object>(
17
+ url: MaybeAccessor<AutomergeUrl | undefined>,
18
+ options?: UseDocHandleOptions
19
+ ): [Accessor<Doc<T> | undefined>, Resource<DocHandle<T> | undefined>] {
20
+ const handle = useDocHandle<T>(url, options)
21
+ return [createDocSignal<T>(handle), handle] as const
22
+ }
@@ -0,0 +1,302 @@
1
+ import { type PeerId, Repo, type AutomergeUrl } from "@automerge/automerge-repo"
2
+ import { renderHook, testEffect } from "@solidjs/testing-library"
3
+ import { describe, expect, it, vi } from "vitest"
4
+ import { RepoContext } from "../src/context.js"
5
+ import { createEffect, createSignal, type ParentComponent } from "solid-js"
6
+ import useDocSignal from "../src/useDocSignal.js"
7
+
8
+ interface ExampleDoc {
9
+ key: string
10
+ array: number[]
11
+ nested: { title: string }
12
+ }
13
+
14
+ describe("useDocSignal", () => {
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
+ nested: { title: "hello" },
25
+ })
26
+
27
+ const wrapper: ParentComponent = props => {
28
+ return (
29
+ <RepoContext.Provider value={repo}>
30
+ {props.children}
31
+ </RepoContext.Provider>
32
+ )
33
+ }
34
+
35
+ return {
36
+ repo,
37
+ wrapper,
38
+ create,
39
+ options: { repo },
40
+ }
41
+ }
42
+
43
+ it("should return the initial document value", async () => {
44
+ const { create, options } = setup()
45
+
46
+ await testEffect(done => {
47
+ const [doc] = useDocSignal<ExampleDoc>(create().url, options)
48
+ createEffect((run: number = 0) => {
49
+ if (run == 0) {
50
+ expect(doc()?.key).toBe("value")
51
+ done()
52
+ }
53
+ return run + 1
54
+ })
55
+ })
56
+ })
57
+
58
+ it("should notify on a property change", async () => {
59
+ const { create, options } = setup()
60
+
61
+ await testEffect(done => {
62
+ const [doc, handle] = useDocSignal<ExampleDoc>(create().url, options)
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
+ })
77
+ })
78
+
79
+ it("should return the handle as the second element", async () => {
80
+ const { create, options } = setup()
81
+ const created = create()
82
+
83
+ await testEffect(done => {
84
+ const [, handle] = useDocSignal<ExampleDoc>(created.url, options)
85
+ createEffect((run: number = 0) => {
86
+ if (run == 0) {
87
+ expect(handle()).not.toBe(undefined)
88
+ expect(handle()?.url).toBe(created.url)
89
+ done()
90
+ }
91
+ return run + 1
92
+ })
93
+ })
94
+ })
95
+
96
+ it("should update when an array element changes", async () => {
97
+ const { create, options } = setup()
98
+
99
+ await testEffect(done => {
100
+ const [doc, handle] = useDocSignal<ExampleDoc>(create().url, options)
101
+ createEffect((run: number = 0) => {
102
+ if (run == 0) {
103
+ expect(doc()?.array).toEqual([1, 2, 3])
104
+ handle()?.change(doc => doc.array.push(4))
105
+ } else if (run == 1) {
106
+ expect(doc()?.array).toEqual([1, 2, 3, 4])
107
+ done()
108
+ }
109
+ return run + 1
110
+ })
111
+ })
112
+ })
113
+
114
+ it("should update when a nested property changes", async () => {
115
+ const { create, options } = setup()
116
+
117
+ await testEffect(done => {
118
+ const [doc, handle] = useDocSignal<ExampleDoc>(create().url, options)
119
+ createEffect((run: number = 0) => {
120
+ if (run == 0) {
121
+ expect(doc()?.nested.title).toBe("hello")
122
+ handle()?.change(doc => (doc.nested.title = "world"))
123
+ } else if (run == 1) {
124
+ expect(doc()?.nested.title).toBe("world")
125
+ done()
126
+ }
127
+ return run + 1
128
+ })
129
+ })
130
+ })
131
+
132
+ it("should work with a signal url", async () => {
133
+ const { create, wrapper } = setup()
134
+ const [url, setURL] = createSignal<AutomergeUrl>()
135
+ const {
136
+ result: [doc, handle],
137
+ owner,
138
+ } = renderHook(useDocSignal<ExampleDoc>, {
139
+ initialProps: [url],
140
+ wrapper,
141
+ })
142
+
143
+ const done = testEffect(done => {
144
+ createEffect((run: number = 0) => {
145
+ if (run == 0) {
146
+ expect(doc()).toBeUndefined()
147
+ setURL(create().url)
148
+ } else if (run == 1) {
149
+ expect(doc()?.key).toBe("value")
150
+ handle()?.change(doc => (doc.key = "hello world!"))
151
+ } else if (run == 2) {
152
+ expect(doc()?.key).toBe("hello world!")
153
+ setURL(create().url)
154
+ } else if (run == 3) {
155
+ expect(doc()?.key).toBe("value")
156
+ done()
157
+ }
158
+ return run + 1
159
+ })
160
+ }, owner!)
161
+ return done
162
+ })
163
+
164
+ it("should clear the signal when the url returns to nothing", async () => {
165
+ const { create, options } = setup()
166
+ const [url, setURL] = createSignal<AutomergeUrl>()
167
+
168
+ const done = testEffect(done => {
169
+ const [doc, handle] = useDocSignal<ExampleDoc>(url, options)
170
+ createEffect((run: number = 0) => {
171
+ if (run == 0) {
172
+ expect(handle()).toBe(undefined)
173
+ setURL(create().url)
174
+ } else if (run == 1) {
175
+ expect(doc()?.key).toBe("value")
176
+ expect(handle()).not.toBe(undefined)
177
+ setURL(undefined)
178
+ } else if (run == 2) {
179
+ expect(handle()).toBe(undefined)
180
+ setURL(create().url)
181
+ } else if (run == 3) {
182
+ expect(doc()?.key).toBe("value")
183
+ expect(handle()).not.toBe(undefined)
184
+ done()
185
+ }
186
+ return run + 1
187
+ })
188
+ })
189
+ return done
190
+ })
191
+
192
+ it("should work without a context if given a repo in options", async () => {
193
+ const { create, repo } = setup()
194
+
195
+ await testEffect(done => {
196
+ const [doc] = useDocSignal<ExampleDoc>(create().url, { repo })
197
+ createEffect((run: number = 0) => {
198
+ if (run == 0) {
199
+ expect(doc()?.key).toBe("value")
200
+ done()
201
+ }
202
+ return run + 1
203
+ })
204
+ })
205
+ })
206
+
207
+ it("should be coarse-grained: any change triggers re-read of the whole doc", async () => {
208
+ const { create, options } = setup()
209
+
210
+ const signalFn = vi.fn()
211
+
212
+ await testEffect(done => {
213
+ const [doc, handle] = useDocSignal<ExampleDoc>(create().url, options)
214
+ createEffect((run: number = 0) => {
215
+ signalFn(doc()?.key, doc()?.array)
216
+ if (run == 0) {
217
+ expect(doc()?.key).toBe("value")
218
+ expect(doc()?.array).toEqual([1, 2, 3])
219
+ // Change only the array — should still trigger the signal
220
+ handle()?.change(doc => doc.array.push(4))
221
+ } else if (run == 1) {
222
+ // The whole doc is refreshed, so we see the array change
223
+ expect(doc()?.array).toEqual([1, 2, 3, 4])
224
+ // key remains unchanged
225
+ expect(doc()?.key).toBe("value")
226
+ // Now change only the key
227
+ handle()?.change(doc => (doc.key = "updated"))
228
+ } else if (run == 2) {
229
+ expect(doc()?.key).toBe("updated")
230
+ expect(doc()?.array).toEqual([1, 2, 3, 4])
231
+ done()
232
+ }
233
+ return run + 1
234
+ })
235
+ })
236
+
237
+ // The signal callback should have been called 3 times (once per run)
238
+ expect(signalFn).toHaveBeenCalledTimes(3)
239
+ })
240
+
241
+ it("should work with a slow handle", async () => {
242
+ const { repo } = setup()
243
+
244
+ const slowHandle = repo.create({
245
+ key: "slow",
246
+ array: [],
247
+ nested: { title: "slow" },
248
+ })
249
+ const originalFind = repo.find.bind(repo)
250
+ repo.find = vi.fn().mockImplementation(async (...args) => {
251
+ await new Promise(resolve => setTimeout(resolve, 100))
252
+ // @ts-expect-error i'm ok i promise
253
+ return await originalFind(...args)
254
+ })
255
+
256
+ const done = testEffect(done => {
257
+ const [doc] = useDocSignal<ExampleDoc>(() => slowHandle.url, {
258
+ repo,
259
+ "~skipInitialValue": true,
260
+ })
261
+ createEffect((run: number = 0) => {
262
+ if (run == 0) {
263
+ expect(doc()?.key).toBeUndefined()
264
+ } else if (run == 1) {
265
+ expect(doc()?.key).toBe("slow")
266
+ done()
267
+ }
268
+ return run + 1
269
+ })
270
+ })
271
+ repo.find = originalFind
272
+ return done
273
+ })
274
+
275
+ it("should not apply updates from a previous handle after url changes", async () => {
276
+ const { create, options } = setup()
277
+ const h1 = create()
278
+ const h2 = create()
279
+
280
+ const [url, setURL] = createSignal<AutomergeUrl>(h1.url)
281
+
282
+ const done = testEffect(done => {
283
+ const [doc, handle] = useDocSignal<ExampleDoc>(url, options)
284
+ createEffect((run: number = 0) => {
285
+ if (run == 0) {
286
+ expect(doc()?.key).toBe("value")
287
+ // Switch to h2
288
+ setURL(h2.url)
289
+ } else if (run == 1) {
290
+ expect(doc()?.key).toBe("value")
291
+ // Change h2
292
+ handle()?.change(doc => (doc.key = "from h2"))
293
+ } else if (run == 2) {
294
+ expect(doc()?.key).toBe("from h2")
295
+ done()
296
+ }
297
+ return run + 1
298
+ })
299
+ })
300
+ return done
301
+ })
302
+ })