@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.
@@ -0,0 +1,8 @@
1
+ import { Doc, DocHandle } from '@automerge/automerge-repo/slim';
2
+
3
+ /**
4
+ * make a fine-grained live view of a document from its handle.
5
+ * @param handle an Automerge
6
+ * [DocHandle](https://automerge.org/automerge-repo/classes/_automerge_automerge_repo.DocHandle.html)
7
+ */
8
+ export default function makeDocumentProjection<T extends object>(handle: DocHandle<T>): Doc<T>;
@@ -0,0 +1,7 @@
1
+ import { Repo } from '@automerge/automerge-repo/slim';
2
+
3
+ export interface UseDocHandleOptions {
4
+ repo?: Repo;
5
+ "~skipInitialValue"?: boolean;
6
+ }
7
+ export type MaybeAccessor<T> = T | (() => T);
@@ -0,0 +1,15 @@
1
+ import { AutomergeUrl, DocHandle } from '@automerge/automerge-repo/slim';
2
+ import { Resource } from 'solid-js';
3
+ import { MaybeAccessor, UseDocHandleOptions } from './types.js';
4
+
5
+ /**
6
+ * get a
7
+ * [DocHandle](https://automerge.org/automerge-repo/classes/_automerge_automerge_repo.DocHandle.html)
8
+ * from an
9
+ * [AutomergeUrl](https://automerge.org/automerge-repo/types/_automerge_automerge_repo.AutomergeUrl.html)
10
+ * as a
11
+ * [Resource](https://docs.solidjs.com/reference/basic-reactivity/create-resource).
12
+ * Waits for the handle to be
13
+ * [ready](https://automerge.org/automerge-repo/variables/_automerge_automerge_repo.HandleState-1.html).
14
+ */
15
+ export default function useDocHandle<T>(url: MaybeAccessor<AutomergeUrl | undefined>, options?: UseDocHandleOptions): Resource<DocHandle<T> | undefined>;
@@ -0,0 +1,9 @@
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
+ * get a fine-grained live view of a document, and its handle, from a URL.
7
+ * @param url a function that returns a url
8
+ */
9
+ export default function useDocument<T extends object>(url: MaybeAccessor<AutomergeUrl | undefined>, options?: UseDocHandleOptions): [Accessor<Doc<T> | undefined>, Resource<DocHandle<T> | undefined>];
@@ -0,0 +1,4 @@
1
+ import { Repo } from '@automerge/automerge-repo/slim';
2
+
3
+ /** grab the repo from the {@link RepoContext} */
4
+ export default function useRepo(): Repo;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@automerge/automerge-repo-solid-primitives",
3
+ "version": "2.2.0",
4
+ "description": "Access Automerge Repo in your SolidJS application",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc --noEmit && vite build",
9
+ "test": "vitest run",
10
+ "watch": "npm-watch build",
11
+ "visualize": "VISUALIZE=true vite build"
12
+ },
13
+ "author": "chee <chee@rabbits.computer>",
14
+ "license": "MIT",
15
+ "devDependencies": {
16
+ "@automerge/automerge": "3.0.0",
17
+ "@automerge/automerge-repo": "2.2.0",
18
+ "@solidjs/testing-library": "^0.8.9",
19
+ "@testing-library/jest-dom": "^6.6.3",
20
+ "@testing-library/user-event": "^14.5.2",
21
+ "rollup-plugin-visualizer": "^5.9.3",
22
+ "solid-js": "^1.9.4",
23
+ "vite-plugin-dts": "^3.9.1",
24
+ "vite-plugin-solid": "^2.11.0"
25
+ },
26
+ "peerDependencies": {
27
+ "@automerge/automerge": "3.0.0",
28
+ "solid-js": "^1.9.4"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/automerge/automerge-repo.git"
33
+ },
34
+ "homepage": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo-solid-primitives",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "watch": {
39
+ "build": {
40
+ "patterns": "./src/**/*",
41
+ "extensions": [
42
+ ".ts"
43
+ ]
44
+ }
45
+ },
46
+ "gitHead": "0eab1c635365712017d7ccc000e7cd92f14e7143"
47
+ }
package/readme.md ADDED
@@ -0,0 +1,139 @@
1
+ # Solid Automerge
2
+
3
+ <a href="https://www.solidjs.com/"> <img alt="" src=.assets/solid.png width=22
4
+ height=22> Solid </a> primitives for <a
5
+ href="https://automerge.org/docs/repositories/"> <img alt=""
6
+ src=.assets/automerge.png width=22 height=22>Automerge</a> .
7
+
8
+ ```sh
9
+ pnpm add solidjs @automerge/automerge-repo
10
+ pnpm add solid-automerge
11
+ ```
12
+
13
+ or, say:
14
+
15
+ ```sh
16
+ deno add --npm solidjs @automerge/vanillajs
17
+ deno add jsr:@chee/solid-automerge
18
+ ```
19
+
20
+ ## useDocument ✨
21
+
22
+ Get a fine-grained live view of an automerge document from its URL.
23
+
24
+ When the handle receives changes, it converts the incoming automerge patch ops
25
+ to precise solid store updates, giving you fine-grained reactivity that's
26
+ consistent across space and time.
27
+
28
+ Returns `[doc, handle]`.
29
+
30
+ ```ts
31
+ useDocument<T>(
32
+ () => AutomergeURL,
33
+ options?: {repo: Repo}
34
+ ): [Doc<T>, DocHandle<T>]
35
+ ```
36
+
37
+ ```tsx
38
+ // example
39
+ const [url, setURL] = createSignal<AutomergeUrl>(props.url)
40
+ const [doc, handle] = useDocument(url, { repo })
41
+
42
+ const inc = () => handle()?.change(d => d.count++)
43
+ return <button onclick={inc}>{doc()?.count}</button>
44
+ ```
45
+
46
+ The `{repo}` option can be left out if you are using [RepoContext](#repocontext).
47
+
48
+ ## createDocumentProjection
49
+
50
+ Get a fine-grained live view from a signal automerge `DocHandle`.
51
+
52
+ Underlying primitive for [`useDocument`](#usedocument-).
53
+
54
+ Works with [`useHandle`](#usehandle).
55
+
56
+ ```ts
57
+ createDocumentProjection<T>(() => AutomergeUrl): Doc<T>
58
+ ```
59
+
60
+ ```tsx
61
+ // example
62
+ const handle = repo.find(url)
63
+ const doc = makeDocumentProjection<{ items: { title: string }[] }>(handle)
64
+
65
+ // subscribes fine-grained to doc.items[1].title
66
+ return <h1>{doc.items[1].title}</h1>
67
+ ```
68
+
69
+ ## makeDocumentProjection
70
+
71
+ Just like `createDocumentProjection`, but without a reactive input.
72
+
73
+ Underlying primitive for [`createDocumentProjection`](#createDocumentProjection).
74
+
75
+ ```ts
76
+ makeDocumentProjection<T>(handle: Handle<T>): Doc<T>
77
+ ```
78
+
79
+ ```tsx
80
+ // example
81
+ const handle = repo.find(url)
82
+ const doc = makeDocumentProjection<{ items: { title: string }[] }>(handle)
83
+
84
+ // subscribes fine-grained to doc.items[1].title
85
+ return <h1>{doc.items[1].title}</h1>
86
+ ```
87
+
88
+ ## useDocHandle
89
+
90
+ Get a [DocHandle](https://automerge.org/docs/repositories/dochandles/) from the
91
+ repo as a
92
+ [resource](https://docs.solidjs.com/reference/basic-reactivity/create-resource).
93
+
94
+ Perfect for handing to `createDocumentProjection`.
95
+
96
+ ```ts
97
+ useDocHandle<T>(
98
+ () => AnyDocumentId,
99
+ options?: {repo: Repo}
100
+ ): Resource<Handle<T>>
101
+ ```
102
+
103
+ ```tsx
104
+ const handle = useDocHandle(id, { repo })
105
+ // or
106
+ const handle = useDocHandle(id)
107
+ ```
108
+
109
+ The `repo` option can be left out if you are using [RepoContext](#repocontext).
110
+
111
+ ## context
112
+
113
+ If you prefer the context pattern for some reason, you can pass the repo higher
114
+ up in your app with `RepoContext`
115
+
116
+ ### `RepoContext`
117
+
118
+ A convenience context for Automerge-Repo Solid apps. Optional: if you prefer you
119
+ can pass a repo as an option to `useDocHandle` and `useDocument`.
120
+
121
+ ```tsx
122
+ <RepoContext.Provider repo={Repo}>
123
+ <App />
124
+ </RepoContext.Provider>
125
+ ```
126
+
127
+ ### `useRepo`
128
+
129
+ Get the repo from the [context](#repocontext).
130
+
131
+ ```ts
132
+ useRepo(): Repo
133
+ ```
134
+
135
+ #### e.g.
136
+
137
+ ```ts
138
+ const repo = useRepo()
139
+ ```
@@ -0,0 +1,18 @@
1
+ import type { DocHandleChangePayload } from "@automerge/automerge-repo/slim"
2
+ import { applyPatches } from "@automerge/automerge/slim"
3
+
4
+ /**
5
+ * convert automerge patches to solid producer operations
6
+ * @param payload the
7
+ * [DocHandleChangePayload](https://automerge.org/automerge-repo/interfaces/_automerge_automerge_repo.DocHandleChangePayload.html)
8
+ * from the handle.on("change
9
+ * @returns a callback for an immer-like function. e.g.
10
+ * [produce](https://docs.solidjs.com/reference/store-utilities/produce) for
11
+ * [Solid
12
+ * Stores](https://docs.solidjs.com/reference/store-utilities/create-store)
13
+ */
14
+ export default function autoproduce<T>(
15
+ payload: DocHandleChangePayload<T>
16
+ ): (doc: T) => void {
17
+ return (doc: T) => applyPatches(doc, payload.patches)
18
+ }
package/src/context.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Repo } from "@automerge/automerge-repo/slim"
2
+ import { createContext, type Context } from "solid-js"
3
+
4
+ /**
5
+ * a [context](https://docs.solidjs.com/concepts/context) that provides access
6
+ * to an Automerge Repo. you don't need this, you can pass the repo in the
7
+ * second arg to the functions that need it.
8
+ */
9
+ export const RepoContext: Context<Repo | null> = createContext<Repo | null>(
10
+ null
11
+ )
@@ -0,0 +1,19 @@
1
+ import { createMemo, type Accessor } from "solid-js"
2
+ import { type DocHandle, type Doc } from "@automerge/automerge-repo/slim"
3
+ import makeDocumentProjection from "./makeDocumentProjection.js"
4
+
5
+ /**
6
+ * get a fine-grained live view of a document from a handle. works with
7
+ * {@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 createDocumentProjection<T extends object>(
12
+ handle: Accessor<DocHandle<T> | undefined>
13
+ ): Accessor<Doc<T> | undefined> {
14
+ const projection = createMemo<Doc<T> | undefined>(() => {
15
+ const unwrappedHandle = typeof handle == "function" ? handle() : handle
16
+ return unwrappedHandle && makeDocumentProjection<T>(unwrappedHandle)
17
+ })
18
+ return projection
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { default as autoproduce } from "./autoproduce.js"
2
+ export { default as useDocument } from "./useDocument.js"
3
+ export { default as useDocHandle } from "./useDocHandle.js"
4
+ export { default as makeDocumentProjection } from "./makeDocumentProjection.js"
5
+ export { default as createDocumentProjection } from "./createDocumentProjection.js"
6
+ export { default as useRepo } from "./useRepo.js"
7
+ export { RepoContext } from "./context.js"
@@ -0,0 +1,73 @@
1
+ import { onCleanup } from "solid-js"
2
+ import type {
3
+ Doc,
4
+ DocHandle,
5
+ DocHandleChangePayload,
6
+ } from "@automerge/automerge-repo/slim"
7
+ import autoproduce from "./autoproduce.js"
8
+ import { createStore, produce, reconcile, type Store } from "solid-js/store"
9
+ import { applyPatches, diff, getHeads } from "@automerge/automerge/slim"
10
+
11
+ const cache = new WeakMap<
12
+ DocHandle<unknown>,
13
+ {
14
+ refs: number
15
+ store: Store<Doc<unknown>>
16
+ cleanup(): void
17
+ }
18
+ >()
19
+
20
+ function initial<T>(handle: DocHandle<T>): T {
21
+ const template = {} as T
22
+ applyPatches(template, diff(handle.doc(), [], getHeads(handle.doc())))
23
+ return template
24
+ }
25
+
26
+ /**
27
+ * make a fine-grained live view of a document from its handle.
28
+ * @param handle an Automerge
29
+ * [DocHandle](https://automerge.org/automerge-repo/classes/_automerge_automerge_repo.DocHandle.html)
30
+ */
31
+ export default function makeDocumentProjection<T extends object>(
32
+ handle: DocHandle<T>
33
+ ): Doc<T> {
34
+ onCleanup(() => {
35
+ const item = cache.get(handle)!
36
+ if (!item) return
37
+ if (!item.refs--) {
38
+ item.cleanup()
39
+ }
40
+ })
41
+
42
+ if (cache.has(handle)) {
43
+ const item = cache.get(handle)!
44
+ item.refs++
45
+ return item.store as T
46
+ }
47
+
48
+ const [doc, set] = createStore<T>(initial(handle))
49
+
50
+ cache.set(handle, {
51
+ refs: 0,
52
+ store: doc,
53
+ cleanup() {
54
+ handle.off("change", patch)
55
+ handle.off("delete", ondelete)
56
+ // https://github.com/chee/solid-automerge/pull/5
57
+ cache.delete(handle)
58
+ },
59
+ })
60
+
61
+ function patch(payload: DocHandleChangePayload<T>) {
62
+ set(produce(autoproduce(payload)))
63
+ }
64
+
65
+ function ondelete() {
66
+ set(reconcile({} as T))
67
+ }
68
+
69
+ handle.on("change", patch)
70
+ handle.on("delete", ondelete)
71
+
72
+ return doc
73
+ }
package/src/types.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Repo } from "@automerge/automerge-repo/slim"
2
+
3
+ export interface UseDocHandleOptions {
4
+ repo?: Repo
5
+ // @internal
6
+ "~skipInitialValue"?: boolean
7
+ }
8
+
9
+ export type MaybeAccessor<T> = T | (() => T)
@@ -0,0 +1,90 @@
1
+ import type {
2
+ AutomergeUrl,
3
+ DocHandle,
4
+ DocumentId,
5
+ HandleState,
6
+ } from "@automerge/automerge-repo/slim"
7
+ import {
8
+ createEffect,
9
+ createResource,
10
+ useContext,
11
+ type Resource,
12
+ } from "solid-js"
13
+ import { RepoContext } from "./context.js"
14
+ import type { MaybeAccessor, UseDocHandleOptions } from "./types.js"
15
+ const readyStates = ["ready", "deleted", "unavailable"] as HandleState[]
16
+ const badStates = ["deleted", "unavailable"] as HandleState[]
17
+
18
+ /**
19
+ * get a
20
+ * [DocHandle](https://automerge.org/automerge-repo/classes/_automerge_automerge_repo.DocHandle.html)
21
+ * from an
22
+ * [AutomergeUrl](https://automerge.org/automerge-repo/types/_automerge_automerge_repo.AutomergeUrl.html)
23
+ * as a
24
+ * [Resource](https://docs.solidjs.com/reference/basic-reactivity/create-resource).
25
+ * Waits for the handle to be
26
+ * [ready](https://automerge.org/automerge-repo/variables/_automerge_automerge_repo.HandleState-1.html).
27
+ */
28
+ export default function useDocHandle<T>(
29
+ url: MaybeAccessor<AutomergeUrl | undefined>,
30
+ options?: UseDocHandleOptions
31
+ ): Resource<DocHandle<T> | undefined> {
32
+ const contextRepo = useContext(RepoContext)
33
+
34
+ if (!options?.repo && !contextRepo) {
35
+ throw new Error("use outside <RepoContext> requires options.repo")
36
+ }
37
+
38
+ const repo = (options?.repo || contextRepo)!
39
+
40
+ function getExistingHandle() {
41
+ if (options?.["~skipInitialValue"]) return undefined
42
+ const unwrappedURL = typeof url == "function" ? url() : url
43
+ if (!unwrappedURL) return undefined
44
+ try {
45
+ const documentId = new URL(unwrappedURL).pathname as DocumentId
46
+ const existingHandle = repo.handles[documentId]
47
+ if (existingHandle?.isReady()) {
48
+ return existingHandle as DocHandle<T>
49
+ }
50
+ } catch (error) {
51
+ console.error("Error parsing URL:", error)
52
+ }
53
+ }
54
+
55
+ const [handle, { mutate }] = createResource(
56
+ url,
57
+ async url => {
58
+ const handle = await repo.find<T>(url, {
59
+ allowableStates: readyStates,
60
+ })
61
+ const reject = (state: HandleState) =>
62
+ Promise.reject(new Error(`document not available: [${state}]`))
63
+
64
+ if (handle.isReady()) {
65
+ return handle
66
+ } else if (handle.inState(badStates)) {
67
+ return reject(handle.state)
68
+ }
69
+
70
+ return handle.whenReady(readyStates).then(() => {
71
+ if (handle.isReady()) {
72
+ return handle
73
+ }
74
+ return reject(handle.state)
75
+ })
76
+ },
77
+ {
78
+ initialValue: getExistingHandle(),
79
+ }
80
+ )
81
+
82
+ createEffect(() => {
83
+ const unwrappedURL = typeof url == "function" ? url() : url
84
+ if (!unwrappedURL) {
85
+ mutate()
86
+ }
87
+ })
88
+
89
+ return handle
90
+ }
@@ -0,0 +1,21 @@
1
+ import type {
2
+ AutomergeUrl,
3
+ Doc,
4
+ DocHandle,
5
+ } from "@automerge/automerge-repo/slim"
6
+ import createDocumentProjection from "./createDocumentProjection.js"
7
+ import useDocHandle from "./useDocHandle.js"
8
+ import type { MaybeAccessor, UseDocHandleOptions } from "./types.js"
9
+ import type { Accessor, Resource } from "solid-js"
10
+
11
+ /**
12
+ * get a fine-grained live view of a document, and its handle, from a URL.
13
+ * @param url a function that returns a url
14
+ */
15
+ export default function useDocument<T extends object>(
16
+ url: MaybeAccessor<AutomergeUrl | undefined>,
17
+ options?: UseDocHandleOptions
18
+ ): [Accessor<Doc<T> | undefined>, Resource<DocHandle<T> | undefined>] {
19
+ const handle = useDocHandle<T>(url, options)
20
+ return [createDocumentProjection<T>(handle), handle] as const
21
+ }
package/src/useRepo.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Repo } from "@automerge/automerge-repo/slim"
2
+ import { RepoContext } from "./context.js"
3
+ import { useContext } from "solid-js"
4
+
5
+ /** grab the repo from the {@link RepoContext} */
6
+ export default function useRepo(): Repo {
7
+ const repo = useContext(RepoContext)
8
+ if (!repo) throw new Error("Please wrap me in a <RepoContext value={repo}>")
9
+ return repo
10
+ }