@automerge/automerge-repo-react-hooks 1.0.2 → 1.0.4
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/index.d.ts +89 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -0
- package/dist/useBootstrap.d.ts +14 -12
- package/dist/useBootstrap.d.ts.map +1 -1
- package/dist/useBootstrap.js +31 -17
- package/dist/useDocument.d.ts +9 -1
- package/dist/useDocument.d.ts.map +1 -1
- package/dist/useDocument.js +8 -0
- package/dist/useHandle.d.ts +5 -0
- package/dist/useHandle.d.ts.map +1 -1
- package/dist/useHandle.js +5 -0
- package/dist/useLocalAwareness.d.ts +10 -7
- package/dist/useLocalAwareness.d.ts.map +1 -1
- package/dist/useLocalAwareness.js +15 -0
- package/dist/useRemoteAwareness.d.ts +14 -8
- package/dist/useRemoteAwareness.d.ts.map +1 -1
- package/dist/useRemoteAwareness.js +12 -0
- package/dist/useRepo.d.ts +2 -0
- package/dist/useRepo.d.ts.map +1 -1
- package/dist/useRepo.js +2 -0
- package/package.json +8 -9
- package/src/index.ts +89 -3
- package/src/useBootstrap.ts +30 -34
- package/src/useDocument.ts +10 -2
- package/src/useHandle.ts +5 -0
- package/src/useLocalAwareness.ts +10 -7
- package/src/useRemoteAwareness.ts +16 -10
- package/src/useRepo.ts +2 -0
- package/typedoc.json +5 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* # React Hooks for Automerge Repo
|
|
5
|
+
*
|
|
6
|
+
* These hooks are provided as helpers for using Automerge in your React project.
|
|
7
|
+
*
|
|
8
|
+
* #### {@link useBootstrap}
|
|
9
|
+
* This hook is used to load a document based on the URL hash, for example `//myapp/#documentId=[document ID]`.
|
|
10
|
+
* It can also load the document ID from localStorage, or create a new document if none is specified.
|
|
11
|
+
*
|
|
12
|
+
* #### {@link useLocalAwareness} & {@link useRemoteAwareness}
|
|
13
|
+
* These hooks implement ephemeral awareness/presence, similar to [Yjs Awareness](https://docs.yjs.dev/getting-started/adding-awareness).
|
|
14
|
+
* They allow temporary state to be shared, such as cursor positions or peer online/offline status.
|
|
15
|
+
*
|
|
16
|
+
* Ephemeral messages are replicated between peers, but not saved to the Automerge doc, and are used for temporary updates that will be discarded.
|
|
17
|
+
*
|
|
18
|
+
* #### {@link useRepo}/{@link RepoContext}
|
|
19
|
+
* Use RepoContext to set up react context for an Automerge repo.
|
|
20
|
+
* Use useRepo to lookup the repo from context.
|
|
21
|
+
* Most hooks depend on RepoContext being available.
|
|
22
|
+
*
|
|
23
|
+
* #### {@link useDocument }
|
|
24
|
+
* Return a document & updater fn, by ID.
|
|
25
|
+
*
|
|
26
|
+
* #### {@link useHandle }
|
|
27
|
+
* Return a handle, by ID.
|
|
28
|
+
*
|
|
29
|
+
* ## Example usage
|
|
30
|
+
*
|
|
31
|
+
* ### App Setup
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import React, { StrictMode } from "react"
|
|
35
|
+
* import ReactDOM from "react-dom/client"
|
|
36
|
+
*
|
|
37
|
+
* import { Repo, DocCollection } from "@automerge/automerge-repo"
|
|
38
|
+
*
|
|
39
|
+
* import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel"
|
|
40
|
+
*
|
|
41
|
+
* import App, { RootDocument } from "./App.js"
|
|
42
|
+
* import { RepoContext } from "@automerge/automerge-repo-react-hooks"
|
|
43
|
+
*
|
|
44
|
+
* // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
|
+
* const sharedWorker = new SharedWorker(
|
|
46
|
+
* new URL("./shared-worker.js", import.meta.url),
|
|
47
|
+
* { type: "module", name: "@automerge/automerge-repo-shared-worker" }
|
|
48
|
+
* )
|
|
49
|
+
*
|
|
50
|
+
* async function getRepo(): Promise<DocCollection> {
|
|
51
|
+
* return await Repo({
|
|
52
|
+
* network: [
|
|
53
|
+
* new BroadcastChannelNetworkAdapter(),
|
|
54
|
+
* ],
|
|
55
|
+
* sharePolicy: peerId => peerId.includes("shared-worker"),
|
|
56
|
+
* })
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* const initFunction = (d: RootDocument) => {
|
|
60
|
+
* d.items = []
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* const queryString = window.location.search // Returns:'?q=123'
|
|
64
|
+
*
|
|
65
|
+
* // Further parsing:
|
|
66
|
+
* const params = new URLSearchParams(queryString)
|
|
67
|
+
* const hostname = params.get("host") || "automerge-storage-demo.glitch.me"
|
|
68
|
+
*
|
|
69
|
+
* getRepo().then(repo => {
|
|
70
|
+
* useBootstrap(repo, initFunction).then(rootDoc => {
|
|
71
|
+
* const rootElem = document.getElementById("root")
|
|
72
|
+
* if (!rootElem) {
|
|
73
|
+
* throw new Error("The 'root' element wasn't found in the host HTML doc.")
|
|
74
|
+
* }
|
|
75
|
+
* const root = ReactDOM.createRoot(rootElem)
|
|
76
|
+
* root.render(
|
|
77
|
+
* <StrictMode>
|
|
78
|
+
* <RepoContext.Provider value={repo}>
|
|
79
|
+
* <App rootDocumentId={rootDoc.documentId} />
|
|
80
|
+
* </RepoContext.Provider>
|
|
81
|
+
* </StrictMode>
|
|
82
|
+
* )
|
|
83
|
+
* })
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
1
87
|
export { useDocument } from "./useDocument.js";
|
|
2
|
-
export { useBootstrap } from './useBootstrap.js';
|
|
88
|
+
export { useBootstrap, type UseBootstrapOptions } from './useBootstrap.js';
|
|
3
89
|
export { useHandle } from "./useHandle.js";
|
|
4
90
|
export { RepoContext, useRepo } from "./useRepo.js";
|
|
5
|
-
export { useLocalAwareness } from './useLocalAwareness.js';
|
|
6
|
-
export { useRemoteAwareness } from './useRemoteAwareness.js';
|
|
91
|
+
export { useLocalAwareness, type UseLocalAwarenessProps } from './useLocalAwareness.js';
|
|
92
|
+
export { useRemoteAwareness, type PeerStates, type Heartbeats, type UseRemoteAwarenessProps } from './useRemoteAwareness.js';
|
|
7
93
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AACvF,OAAO,EAAE,kBAAkB,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,uBAAuB,EAAE,MAAM,yBAAyB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* # React Hooks for Automerge Repo
|
|
5
|
+
*
|
|
6
|
+
* These hooks are provided as helpers for using Automerge in your React project.
|
|
7
|
+
*
|
|
8
|
+
* #### {@link useBootstrap}
|
|
9
|
+
* This hook is used to load a document based on the URL hash, for example `//myapp/#documentId=[document ID]`.
|
|
10
|
+
* It can also load the document ID from localStorage, or create a new document if none is specified.
|
|
11
|
+
*
|
|
12
|
+
* #### {@link useLocalAwareness} & {@link useRemoteAwareness}
|
|
13
|
+
* These hooks implement ephemeral awareness/presence, similar to [Yjs Awareness](https://docs.yjs.dev/getting-started/adding-awareness).
|
|
14
|
+
* They allow temporary state to be shared, such as cursor positions or peer online/offline status.
|
|
15
|
+
*
|
|
16
|
+
* Ephemeral messages are replicated between peers, but not saved to the Automerge doc, and are used for temporary updates that will be discarded.
|
|
17
|
+
*
|
|
18
|
+
* #### {@link useRepo}/{@link RepoContext}
|
|
19
|
+
* Use RepoContext to set up react context for an Automerge repo.
|
|
20
|
+
* Use useRepo to lookup the repo from context.
|
|
21
|
+
* Most hooks depend on RepoContext being available.
|
|
22
|
+
*
|
|
23
|
+
* #### {@link useDocument }
|
|
24
|
+
* Return a document & updater fn, by ID.
|
|
25
|
+
*
|
|
26
|
+
* #### {@link useHandle }
|
|
27
|
+
* Return a handle, by ID.
|
|
28
|
+
*
|
|
29
|
+
* ## Example usage
|
|
30
|
+
*
|
|
31
|
+
* ### App Setup
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import React, { StrictMode } from "react"
|
|
35
|
+
* import ReactDOM from "react-dom/client"
|
|
36
|
+
*
|
|
37
|
+
* import { Repo, DocCollection } from "@automerge/automerge-repo"
|
|
38
|
+
*
|
|
39
|
+
* import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel"
|
|
40
|
+
*
|
|
41
|
+
* import App, { RootDocument } from "./App.js"
|
|
42
|
+
* import { RepoContext } from "@automerge/automerge-repo-react-hooks"
|
|
43
|
+
*
|
|
44
|
+
* // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
|
+
* const sharedWorker = new SharedWorker(
|
|
46
|
+
* new URL("./shared-worker.js", import.meta.url),
|
|
47
|
+
* { type: "module", name: "@automerge/automerge-repo-shared-worker" }
|
|
48
|
+
* )
|
|
49
|
+
*
|
|
50
|
+
* async function getRepo(): Promise<DocCollection> {
|
|
51
|
+
* return await Repo({
|
|
52
|
+
* network: [
|
|
53
|
+
* new BroadcastChannelNetworkAdapter(),
|
|
54
|
+
* ],
|
|
55
|
+
* sharePolicy: peerId => peerId.includes("shared-worker"),
|
|
56
|
+
* })
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* const initFunction = (d: RootDocument) => {
|
|
60
|
+
* d.items = []
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* const queryString = window.location.search // Returns:'?q=123'
|
|
64
|
+
*
|
|
65
|
+
* // Further parsing:
|
|
66
|
+
* const params = new URLSearchParams(queryString)
|
|
67
|
+
* const hostname = params.get("host") || "automerge-storage-demo.glitch.me"
|
|
68
|
+
*
|
|
69
|
+
* getRepo().then(repo => {
|
|
70
|
+
* useBootstrap(repo, initFunction).then(rootDoc => {
|
|
71
|
+
* const rootElem = document.getElementById("root")
|
|
72
|
+
* if (!rootElem) {
|
|
73
|
+
* throw new Error("The 'root' element wasn't found in the host HTML doc.")
|
|
74
|
+
* }
|
|
75
|
+
* const root = ReactDOM.createRoot(rootElem)
|
|
76
|
+
* root.render(
|
|
77
|
+
* <StrictMode>
|
|
78
|
+
* <RepoContext.Provider value={repo}>
|
|
79
|
+
* <App rootDocumentId={rootDoc.documentId} />
|
|
80
|
+
* </RepoContext.Provider>
|
|
81
|
+
* </StrictMode>
|
|
82
|
+
* )
|
|
83
|
+
* })
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
1
87
|
export { useDocument } from "./useDocument.js";
|
|
2
88
|
export { useBootstrap } from './useBootstrap.js';
|
|
3
89
|
export { useHandle } from "./useHandle.js";
|
package/dist/useBootstrap.d.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import { DocHandle, Repo } from "@automerge/automerge-repo";
|
|
2
2
|
export declare const setHash: (hash: string, pushState?: boolean) => void;
|
|
3
3
|
export declare const useHash: () => string;
|
|
4
|
+
export interface UseBootstrapOptions<T> {
|
|
5
|
+
/** Key to use for the URL hash and localStorage */
|
|
6
|
+
key?: string;
|
|
7
|
+
/** Function returning a document handle called if lookup fails. Defaults to repo.create() */
|
|
8
|
+
onNoDocument?: (repo: Repo) => DocHandle<T>;
|
|
9
|
+
/** Function to call if automerge URL is invalid */
|
|
10
|
+
onInvalidAutomergeUrl?(repo: Repo, error: Error): DocHandle<T>;
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
13
|
* This hook is used to set up a single document as the base of an app session.
|
|
6
14
|
* This is a common pattern for simple multiplayer apps with shareable URLs.
|
|
7
15
|
*
|
|
8
|
-
* It will first check for the
|
|
9
|
-
* //myapp/#
|
|
10
|
-
* Failing that, it will check for a `
|
|
16
|
+
* It will first check for the automergeUrl in the URL hash:
|
|
17
|
+
* //myapp/#automergeUrl=[document URL]
|
|
18
|
+
* Failing that, it will check for a `automergeUrl` key in localStorage.
|
|
11
19
|
* Failing that, it will call onNoDocument, expecting a handle to be returned.
|
|
12
20
|
*
|
|
13
21
|
* The URL and localStorage will then be updated.
|
|
14
|
-
* Finally, it will return the document
|
|
22
|
+
* Finally, it will return the Automerge document's URL.
|
|
15
23
|
*
|
|
16
24
|
* @param {string?} props.key Key to use for the URL hash and localStorage
|
|
17
25
|
* @param {function?} props.fallback Function returning a document handle called if lookup fails. Defaults to repo.create()
|
|
18
|
-
* @param {function?} props.
|
|
26
|
+
* @param {function?} props.onInvalidAutomergeUrl Function to call if URL is invalid; signature (error) => (repo, onCreate)
|
|
19
27
|
* @returns {DocHandle} The document handle
|
|
20
28
|
*/
|
|
21
|
-
|
|
22
|
-
key?: string;
|
|
23
|
-
onNoDocument?: (repo: Repo) => DocHandle<T>;
|
|
24
|
-
onInvalidDocumentId?(repo: Repo, error: Error): DocHandle<T>;
|
|
25
|
-
}
|
|
26
|
-
export declare const useBootstrap: <T>({ key, onNoDocument, onInvalidDocumentId, }?: UseBootstrapOptions<T>) => DocHandle<T>;
|
|
27
|
-
export {};
|
|
29
|
+
export declare const useBootstrap: <T>({ key, onNoDocument, onInvalidAutomergeUrl, }?: UseBootstrapOptions<T>) => DocHandle<T>;
|
|
28
30
|
//# sourceMappingURL=useBootstrap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBootstrap.d.ts","sourceRoot":"","sources":["../src/useBootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"useBootstrap.d.ts","sourceRoot":"","sources":["../src/useBootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAqB,MAAM,2BAA2B,CAAA;AAK9E,eAAO,MAAM,OAAO,SAAU,MAAM,8BAUnC,CAAA;AAGD,eAAO,MAAM,OAAO,cAQnB,CAAA;AAwBD,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,6FAA6F;IAC7F,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAA;IAC3C,mDAAmD;IACnD,qBAAqB,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;CAC/D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,6FA8BxB,CAAA"}
|
package/dist/useBootstrap.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { generateAutomergeUrl, } from "@automerge/automerge-repo";
|
|
2
1
|
import { useEffect, useState, useMemo } from "react";
|
|
3
2
|
import { useRepo } from "./useRepo.js";
|
|
4
3
|
// Set URL hash
|
|
@@ -28,39 +27,54 @@ const setQueryParamValue = (key, value, hash) => {
|
|
|
28
27
|
u.set(key, value);
|
|
29
28
|
return u.toString();
|
|
30
29
|
};
|
|
31
|
-
const
|
|
32
|
-
const
|
|
30
|
+
const getAutomergeUrl = (key, hash) => key && (getQueryParamValue(key, hash) || localStorage.getItem(key));
|
|
31
|
+
const setAutomergeUrl = (key, automergeUrl) => {
|
|
33
32
|
if (key) {
|
|
34
|
-
// Only set URL hash if
|
|
35
|
-
if (
|
|
36
|
-
setHash(setQueryParamValue(key,
|
|
33
|
+
// Only set URL hash if automerge URL changed
|
|
34
|
+
if (automergeUrl !== getQueryParamValue(key, window.location.hash))
|
|
35
|
+
setHash(setQueryParamValue(key, automergeUrl, window.location.hash));
|
|
37
36
|
}
|
|
38
37
|
if (key)
|
|
39
|
-
localStorage.setItem(key,
|
|
38
|
+
localStorage.setItem(key, automergeUrl);
|
|
40
39
|
};
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* This hook is used to set up a single document as the base of an app session.
|
|
42
|
+
* This is a common pattern for simple multiplayer apps with shareable URLs.
|
|
43
|
+
*
|
|
44
|
+
* It will first check for the automergeUrl in the URL hash:
|
|
45
|
+
* //myapp/#automergeUrl=[document URL]
|
|
46
|
+
* Failing that, it will check for a `automergeUrl` key in localStorage.
|
|
47
|
+
* Failing that, it will call onNoDocument, expecting a handle to be returned.
|
|
48
|
+
*
|
|
49
|
+
* The URL and localStorage will then be updated.
|
|
50
|
+
* Finally, it will return the Automerge document's URL.
|
|
51
|
+
*
|
|
52
|
+
* @param {string?} props.key Key to use for the URL hash and localStorage
|
|
53
|
+
* @param {function?} props.fallback Function returning a document handle called if lookup fails. Defaults to repo.create()
|
|
54
|
+
* @param {function?} props.onInvalidAutomergeUrl Function to call if URL is invalid; signature (error) => (repo, onCreate)
|
|
55
|
+
* @returns {DocHandle} The document handle
|
|
56
|
+
*/
|
|
57
|
+
export const useBootstrap = ({ key = "automergeUrl", onNoDocument = repo => repo.create(), onInvalidAutomergeUrl, } = {}) => {
|
|
42
58
|
const repo = useRepo();
|
|
43
59
|
const hash = useHash();
|
|
44
60
|
// Try to get existing document; else create a new one
|
|
45
61
|
const handle = useMemo(() => {
|
|
46
|
-
const
|
|
62
|
+
const url = getAutomergeUrl(key, hash);
|
|
47
63
|
try {
|
|
48
|
-
return
|
|
49
|
-
? repo.find(generateAutomergeUrl({ documentId }))
|
|
50
|
-
: onNoDocument(repo);
|
|
64
|
+
return url ? repo.find(url) : onNoDocument(repo);
|
|
51
65
|
}
|
|
52
66
|
catch (error) {
|
|
53
|
-
// Presumably the
|
|
54
|
-
if (
|
|
55
|
-
return
|
|
67
|
+
// Presumably the URL was invalid
|
|
68
|
+
if (url && onInvalidAutomergeUrl)
|
|
69
|
+
return onInvalidAutomergeUrl(repo, error);
|
|
56
70
|
// Forward other errors
|
|
57
71
|
throw error;
|
|
58
72
|
}
|
|
59
|
-
}, [hash, repo, onNoDocument,
|
|
73
|
+
}, [hash, repo, onNoDocument, onInvalidAutomergeUrl]);
|
|
60
74
|
// Update hashroute & localStorage on changes
|
|
61
75
|
useEffect(() => {
|
|
62
76
|
if (handle) {
|
|
63
|
-
|
|
77
|
+
setAutomergeUrl(key, handle.url);
|
|
64
78
|
}
|
|
65
79
|
}, [hash, handle]);
|
|
66
80
|
return handle;
|
package/dist/useDocument.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { ChangeFn, Doc } from "@automerge/automerge/next";
|
|
2
2
|
import { AutomergeUrl } from "@automerge/automerge-repo";
|
|
3
|
-
|
|
3
|
+
/** A hook which returns a document identified by a URL and a function to change the document.
|
|
4
|
+
*
|
|
5
|
+
* @returns a tuple of the document and a function to change the document.
|
|
6
|
+
* The document will be `undefined` if the document is not available in storage or from any peers
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* This requires a {@link RepoContext} to be provided by a parent component.
|
|
10
|
+
* */
|
|
11
|
+
export declare function useDocument<T>(documentUrl?: AutomergeUrl): [Doc<T> | undefined, (changeFn: ChangeFn<T>) => void];
|
|
4
12
|
//# sourceMappingURL=useDocument.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDocument.d.ts","sourceRoot":"","sources":["../src/useDocument.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAiB,GAAG,EAAE,MAAM,2BAA2B,CAAA;AACxE,OAAO,EAAE,YAAY,EAA0B,MAAM,2BAA2B,CAAA;AAIhF,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"useDocument.d.ts","sourceRoot":"","sources":["../src/useDocument.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAiB,GAAG,EAAE,MAAM,2BAA2B,CAAA;AACxE,OAAO,EAAE,YAAY,EAA0B,MAAM,2BAA2B,CAAA;AAIhF;;;;;;;KAOK;AACL,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,CAAC,EAAE,YAAY,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CA6BhH"}
|
package/dist/useDocument.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { useRepo } from "./useRepo.js";
|
|
3
|
+
/** A hook which returns a document identified by a URL and a function to change the document.
|
|
4
|
+
*
|
|
5
|
+
* @returns a tuple of the document and a function to change the document.
|
|
6
|
+
* The document will be `undefined` if the document is not available in storage or from any peers
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* This requires a {@link RepoContext} to be provided by a parent component.
|
|
10
|
+
* */
|
|
3
11
|
export function useDocument(documentUrl) {
|
|
4
12
|
const [doc, setDoc] = useState();
|
|
5
13
|
const repo = useRepo();
|
package/dist/useHandle.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { AutomergeUrl, DocHandle } from "@automerge/automerge-repo";
|
|
2
|
+
/** A hook which returns a {@link DocHandle} identified by a URL.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This requires a {@link RepoContext} to be provided by a parent component.
|
|
6
|
+
*/
|
|
2
7
|
export declare function useHandle<T>(automergeUrl: AutomergeUrl): DocHandle<T>;
|
|
3
8
|
//# sourceMappingURL=useHandle.d.ts.map
|
package/dist/useHandle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useHandle.d.ts","sourceRoot":"","sources":["../src/useHandle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAInE,wBAAgB,SAAS,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC,CAAC,CAAC,CAIrE"}
|
|
1
|
+
{"version":3,"file":"useHandle.d.ts","sourceRoot":"","sources":["../src/useHandle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAInE;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC,CAAC,CAAC,CAIrE"}
|
package/dist/useHandle.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useRepo } from "./useRepo.js";
|
|
3
|
+
/** A hook which returns a {@link DocHandle} identified by a URL.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* This requires a {@link RepoContext} to be provided by a parent component.
|
|
7
|
+
*/
|
|
3
8
|
export function useHandle(automergeUrl) {
|
|
4
9
|
const repo = useRepo();
|
|
5
10
|
const [handle] = useState(repo.find(automergeUrl));
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { DocHandle } from "@automerge/automerge-repo";
|
|
2
|
+
export interface UseLocalAwarenessProps {
|
|
3
|
+
/** The document handle to send ephemeral state on */
|
|
4
|
+
handle: DocHandle<unknown>;
|
|
5
|
+
/** Our user ID **/
|
|
6
|
+
userId: string;
|
|
7
|
+
/** The initial state object/primitive we should advertise */
|
|
8
|
+
initialState: any;
|
|
9
|
+
/** How frequently to send heartbeats */
|
|
10
|
+
heartbeatTime?: number;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* This hook maintains state for the local client.
|
|
4
14
|
* Like React.useState, it returns a [state, setState] array.
|
|
@@ -8,18 +18,11 @@ import { DocHandle } from "@automerge/automerge-repo";
|
|
|
8
18
|
* It also broadcasts a heartbeat to let other clients know it is online.
|
|
9
19
|
*
|
|
10
20
|
* Note that userIds aren't secure (yet). Any client can lie about theirs.
|
|
11
|
-
* ChannelID is usually just your documentID with some extra characters.
|
|
12
21
|
*
|
|
13
22
|
* @param {string} props.userId Unique user ID. Clients can lie about this.
|
|
14
23
|
* @param {any} props.initialState Initial state object/primitive
|
|
15
24
|
* @param {number?1500} props.heartbeatTime How often to send a heartbeat (in ms)
|
|
16
25
|
* @returns [state, setState]
|
|
17
26
|
*/
|
|
18
|
-
export interface UseLocalAwarenessProps {
|
|
19
|
-
handle: DocHandle<unknown>;
|
|
20
|
-
userId: string;
|
|
21
|
-
initialState: any;
|
|
22
|
-
heartbeatTime?: number;
|
|
23
|
-
}
|
|
24
27
|
export declare const useLocalAwareness: ({ handle, userId, initialState, heartbeatTime, }: UseLocalAwarenessProps) => any[];
|
|
25
28
|
//# sourceMappingURL=useLocalAwareness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLocalAwareness.d.ts","sourceRoot":"","sources":["../src/useLocalAwareness.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAErD
|
|
1
|
+
{"version":3,"file":"useLocalAwareness.d.ts","sourceRoot":"","sources":["../src/useLocalAwareness.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAErD,MAAM,WAAW,sBAAsB;IACrC,qDAAqD;IACrD,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,6DAA6D;IAC7D,YAAY,EAAE,GAAG,CAAA;IACjB,wCAAwC;IACxC,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AACD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,qDAK3B,sBAAsB,UA6CxB,CAAA"}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import useStateRef from "react-usestateref";
|
|
3
3
|
import { peerEvents } from "./useRemoteAwareness.js";
|
|
4
|
+
/**
|
|
5
|
+
* This hook maintains state for the local client.
|
|
6
|
+
* Like React.useState, it returns a [state, setState] array.
|
|
7
|
+
* It is intended to be used alongside useRemoteAwareness.
|
|
8
|
+
*
|
|
9
|
+
* When state is changed it is broadcast to all clients.
|
|
10
|
+
* It also broadcasts a heartbeat to let other clients know it is online.
|
|
11
|
+
*
|
|
12
|
+
* Note that userIds aren't secure (yet). Any client can lie about theirs.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} props.userId Unique user ID. Clients can lie about this.
|
|
15
|
+
* @param {any} props.initialState Initial state object/primitive
|
|
16
|
+
* @param {number?1500} props.heartbeatTime How often to send a heartbeat (in ms)
|
|
17
|
+
* @returns [state, setState]
|
|
18
|
+
*/
|
|
4
19
|
export const useLocalAwareness = ({ handle, userId, initialState, heartbeatTime = 15000, }) => {
|
|
5
20
|
const [localState, setLocalState, localStateRef] = useStateRef(initialState);
|
|
6
21
|
const setState = stateOrUpdater => {
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { DocHandle } from "@automerge/automerge-repo";
|
|
2
2
|
import { EventEmitter } from "eventemitter3";
|
|
3
3
|
export declare const peerEvents: EventEmitter<string | symbol, any>;
|
|
4
|
+
export interface UseRemoteAwarenessProps {
|
|
5
|
+
/** The handle to receive ephemeral state on */
|
|
6
|
+
handle: DocHandle<unknown>;
|
|
7
|
+
/** Our user ID */
|
|
8
|
+
localUserId?: string;
|
|
9
|
+
/** How long to wait (in ms) before marking a peer as offline */
|
|
10
|
+
offlineTimeout?: number;
|
|
11
|
+
/** Function to provide current epoch time */
|
|
12
|
+
getTime?: () => number;
|
|
13
|
+
}
|
|
14
|
+
/** A map from peer ID to their state */
|
|
15
|
+
export type PeerStates = Record<string, any>;
|
|
16
|
+
/** A map from peer ID to their last heartbeat timestamp */
|
|
17
|
+
export type Heartbeats = Record<string, number>;
|
|
4
18
|
/**
|
|
5
19
|
*
|
|
6
20
|
* This hook returns read-only state for remote clients.
|
|
@@ -13,13 +27,5 @@ export declare const peerEvents: EventEmitter<string | symbol, any>;
|
|
|
13
27
|
* @param {function?} props.getTime Function to provide current epoch time (used for testing)
|
|
14
28
|
* @returns [ peerStates: { [userId]: state, ... }, { [userId]: heartbeatEpochTime, ...} ]
|
|
15
29
|
*/
|
|
16
|
-
export interface UseRemoteAwarenessProps {
|
|
17
|
-
handle: DocHandle<unknown>;
|
|
18
|
-
localUserId?: string;
|
|
19
|
-
offlineTimeout?: number;
|
|
20
|
-
getTime?: () => number;
|
|
21
|
-
}
|
|
22
|
-
export type PeerStates = Record<string, any>;
|
|
23
|
-
export type Heartbeats = Record<string, number>;
|
|
24
30
|
export declare const useRemoteAwareness: ({ handle, localUserId, offlineTimeout, getTime, }: UseRemoteAwarenessProps) => [PeerStates, Heartbeats];
|
|
25
31
|
//# sourceMappingURL=useRemoteAwareness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRemoteAwareness.d.ts","sourceRoot":"","sources":["../src/useRemoteAwareness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAGrD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAG5C,eAAO,MAAM,UAAU,oCAAqB,CAAA;AAE5C
|
|
1
|
+
{"version":3,"file":"useRemoteAwareness.d.ts","sourceRoot":"","sources":["../src/useRemoteAwareness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAGrD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAG5C,eAAO,MAAM,UAAU,oCAAqB,CAAA;AAE5C,MAAM,WAAW,uBAAuB;IACtC,+CAA+C;IAC/C,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,kBAAkB;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,MAAM,CAAA;CACvB;AAED,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAC5C,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,sDAK5B,uBAAuB,KAAG,CAAC,UAAU,EAAE,UAAU,CA+CnD,CAAA"}
|
|
@@ -3,6 +3,18 @@ import useStateRef from "react-usestateref";
|
|
|
3
3
|
import { EventEmitter } from "eventemitter3";
|
|
4
4
|
// Emits new_peer event when a new peer is seen
|
|
5
5
|
export const peerEvents = new EventEmitter();
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* This hook returns read-only state for remote clients.
|
|
9
|
+
* It also returns their heartbeat status.
|
|
10
|
+
* It is intended to be used alongside useLocalAwareness.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} props.handle A document handle to associate with
|
|
13
|
+
* @param {string?} props.localUserId Automerge BroadcastChannel sometimes sends us our own messages; optionally filters them
|
|
14
|
+
* @param {number?30000} props.offlineTimeout How long to wait (in ms) before marking a peer as offline
|
|
15
|
+
* @param {function?} props.getTime Function to provide current epoch time (used for testing)
|
|
16
|
+
* @returns [ peerStates: { [userId]: state, ... }, { [userId]: heartbeatEpochTime, ...} ]
|
|
17
|
+
*/
|
|
6
18
|
export const useRemoteAwareness = ({ handle, localUserId, offlineTimeout = 30000, getTime = () => new Date().getTime(), }) => {
|
|
7
19
|
// TODO: You should be able to use multiple instances of this hook on the same handle (write test)
|
|
8
20
|
// TODO: This should support some kind of caching or memoization when switching between channelIDs
|
package/dist/useRepo.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { Repo } from "@automerge/automerge-repo";
|
|
3
|
+
/** A [React context](https://react.dev/learn/passing-data-deeply-with-context) which provides access to an Automerge repo. */
|
|
3
4
|
export declare const RepoContext: import("react").Context<Repo>;
|
|
5
|
+
/** A [React hook](https://reactjs.org/docs/hooks-intro.html) which returns the Automerge repo from {@link RepoContext}. */
|
|
4
6
|
export declare function useRepo(): Repo;
|
|
5
7
|
//# sourceMappingURL=useRepo.d.ts.map
|
package/dist/useRepo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRepo.d.ts","sourceRoot":"","sources":["../src/useRepo.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAA;AAGhD,eAAO,MAAM,WAAW,+BAAmC,CAAA;AAE3D,wBAAgB,OAAO,IAAI,IAAI,CAI9B"}
|
|
1
|
+
{"version":3,"file":"useRepo.d.ts","sourceRoot":"","sources":["../src/useRepo.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAA;AAGhD,8HAA8H;AAC9H,eAAO,MAAM,WAAW,+BAAmC,CAAA;AAE3D,2HAA2H;AAC3H,wBAAgB,OAAO,IAAI,IAAI,CAI9B"}
|
package/dist/useRepo.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
+
/** A [React context](https://react.dev/learn/passing-data-deeply-with-context) which provides access to an Automerge repo. */
|
|
2
3
|
export const RepoContext = createContext(null);
|
|
4
|
+
/** A [React hook](https://reactjs.org/docs/hooks-intro.html) which returns the Automerge repo from {@link RepoContext}. */
|
|
3
5
|
export function useRepo() {
|
|
4
6
|
const repo = useContext(RepoContext);
|
|
5
7
|
if (!repo)
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-react-hooks",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Hooks to access an Automerge Repo from your react app.",
|
|
5
|
-
"repository": "https://github.com/automerge/automerge-repo",
|
|
5
|
+
"repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo-react-hooks",
|
|
6
6
|
"author": "Peter van Hardenberg <pvh@pvh.ca>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"private": false,
|
|
9
8
|
"type": "module",
|
|
10
9
|
"main": "dist/index.js",
|
|
11
10
|
"scripts": {
|
|
@@ -17,18 +16,18 @@
|
|
|
17
16
|
"@automerge/automerge": "^2.1.0"
|
|
18
17
|
},
|
|
19
18
|
"dependencies": {
|
|
20
|
-
"@automerge/automerge-repo": "^1.0.
|
|
21
|
-
"eventemitter3": "^5.0.
|
|
19
|
+
"@automerge/automerge-repo": "^1.0.4",
|
|
20
|
+
"eventemitter3": "^5.0.1",
|
|
22
21
|
"react": "^18.2.0",
|
|
23
22
|
"react-usestateref": "^1.0.8"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
25
|
+
"@automerge/automerge": "^2.1.0",
|
|
26
26
|
"@testing-library/react": "^14.0.0",
|
|
27
|
-
"@vitejs/plugin-react": "^
|
|
27
|
+
"@vitejs/plugin-react": "^3.0.0",
|
|
28
28
|
"jsdom": "^22.1.0",
|
|
29
|
-
"react": "^18.2.0",
|
|
30
29
|
"react-dom": "^18.2.0",
|
|
31
|
-
"vite-plugin-top-level-await": "^1.3.
|
|
30
|
+
"vite-plugin-top-level-await": "^1.3.0",
|
|
32
31
|
"vite-plugin-wasm": "^3.2.2",
|
|
33
32
|
"vitest": "^0.33.0"
|
|
34
33
|
},
|
|
@@ -43,5 +42,5 @@
|
|
|
43
42
|
"publishConfig": {
|
|
44
43
|
"access": "public"
|
|
45
44
|
},
|
|
46
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "17fd5260f9af3e65da636fef084e8c04d6c4bed0"
|
|
47
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* # React Hooks for Automerge Repo
|
|
5
|
+
*
|
|
6
|
+
* These hooks are provided as helpers for using Automerge in your React project.
|
|
7
|
+
*
|
|
8
|
+
* #### {@link useBootstrap}
|
|
9
|
+
* This hook is used to load a document based on the URL hash, for example `//myapp/#documentId=[document ID]`.
|
|
10
|
+
* It can also load the document ID from localStorage, or create a new document if none is specified.
|
|
11
|
+
*
|
|
12
|
+
* #### {@link useLocalAwareness} & {@link useRemoteAwareness}
|
|
13
|
+
* These hooks implement ephemeral awareness/presence, similar to [Yjs Awareness](https://docs.yjs.dev/getting-started/adding-awareness).
|
|
14
|
+
* They allow temporary state to be shared, such as cursor positions or peer online/offline status.
|
|
15
|
+
*
|
|
16
|
+
* Ephemeral messages are replicated between peers, but not saved to the Automerge doc, and are used for temporary updates that will be discarded.
|
|
17
|
+
*
|
|
18
|
+
* #### {@link useRepo}/{@link RepoContext}
|
|
19
|
+
* Use RepoContext to set up react context for an Automerge repo.
|
|
20
|
+
* Use useRepo to lookup the repo from context.
|
|
21
|
+
* Most hooks depend on RepoContext being available.
|
|
22
|
+
*
|
|
23
|
+
* #### {@link useDocument }
|
|
24
|
+
* Return a document & updater fn, by ID.
|
|
25
|
+
*
|
|
26
|
+
* #### {@link useHandle }
|
|
27
|
+
* Return a handle, by ID.
|
|
28
|
+
*
|
|
29
|
+
* ## Example usage
|
|
30
|
+
*
|
|
31
|
+
* ### App Setup
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import React, { StrictMode } from "react"
|
|
35
|
+
* import ReactDOM from "react-dom/client"
|
|
36
|
+
*
|
|
37
|
+
* import { Repo, DocCollection } from "@automerge/automerge-repo"
|
|
38
|
+
*
|
|
39
|
+
* import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel"
|
|
40
|
+
*
|
|
41
|
+
* import App, { RootDocument } from "./App.js"
|
|
42
|
+
* import { RepoContext } from "@automerge/automerge-repo-react-hooks"
|
|
43
|
+
*
|
|
44
|
+
* // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
|
+
* const sharedWorker = new SharedWorker(
|
|
46
|
+
* new URL("./shared-worker.js", import.meta.url),
|
|
47
|
+
* { type: "module", name: "@automerge/automerge-repo-shared-worker" }
|
|
48
|
+
* )
|
|
49
|
+
*
|
|
50
|
+
* async function getRepo(): Promise<DocCollection> {
|
|
51
|
+
* return await Repo({
|
|
52
|
+
* network: [
|
|
53
|
+
* new BroadcastChannelNetworkAdapter(),
|
|
54
|
+
* ],
|
|
55
|
+
* sharePolicy: peerId => peerId.includes("shared-worker"),
|
|
56
|
+
* })
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* const initFunction = (d: RootDocument) => {
|
|
60
|
+
* d.items = []
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* const queryString = window.location.search // Returns:'?q=123'
|
|
64
|
+
*
|
|
65
|
+
* // Further parsing:
|
|
66
|
+
* const params = new URLSearchParams(queryString)
|
|
67
|
+
* const hostname = params.get("host") || "automerge-storage-demo.glitch.me"
|
|
68
|
+
*
|
|
69
|
+
* getRepo().then(repo => {
|
|
70
|
+
* useBootstrap(repo, initFunction).then(rootDoc => {
|
|
71
|
+
* const rootElem = document.getElementById("root")
|
|
72
|
+
* if (!rootElem) {
|
|
73
|
+
* throw new Error("The 'root' element wasn't found in the host HTML doc.")
|
|
74
|
+
* }
|
|
75
|
+
* const root = ReactDOM.createRoot(rootElem)
|
|
76
|
+
* root.render(
|
|
77
|
+
* <StrictMode>
|
|
78
|
+
* <RepoContext.Provider value={repo}>
|
|
79
|
+
* <App rootDocumentId={rootDoc.documentId} />
|
|
80
|
+
* </RepoContext.Provider>
|
|
81
|
+
* </StrictMode>
|
|
82
|
+
* )
|
|
83
|
+
* })
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
1
87
|
export { useDocument } from "./useDocument.js"
|
|
2
|
-
export { useBootstrap } from './useBootstrap.js'
|
|
88
|
+
export { useBootstrap, type UseBootstrapOptions } from './useBootstrap.js'
|
|
3
89
|
export { useHandle } from "./useHandle.js"
|
|
4
90
|
export { RepoContext, useRepo } from "./useRepo.js"
|
|
5
|
-
export { useLocalAwareness } from './useLocalAwareness.js'
|
|
6
|
-
export { useRemoteAwareness } from './useRemoteAwareness.js'
|
|
91
|
+
export { useLocalAwareness, type UseLocalAwarenessProps } from './useLocalAwareness.js'
|
|
92
|
+
export { useRemoteAwareness, type PeerStates, type Heartbeats, type UseRemoteAwarenessProps } from './useRemoteAwareness.js'
|
package/src/useBootstrap.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DocHandle,
|
|
3
|
-
Repo,
|
|
4
|
-
type DocumentId,
|
|
5
|
-
generateAutomergeUrl,
|
|
6
|
-
} from "@automerge/automerge-repo"
|
|
1
|
+
import { DocHandle, Repo, type AutomergeUrl } from "@automerge/automerge-repo"
|
|
7
2
|
import { useEffect, useState, useMemo } from "react"
|
|
8
3
|
import { useRepo } from "./useRepo.js"
|
|
9
4
|
|
|
@@ -41,69 +36,70 @@ const setQueryParamValue = (key: string, value, hash): string => {
|
|
|
41
36
|
return u.toString()
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
const
|
|
39
|
+
const getAutomergeUrl = (key: string, hash: string) =>
|
|
45
40
|
key && (getQueryParamValue(key, hash) || localStorage.getItem(key))
|
|
46
41
|
|
|
47
|
-
const
|
|
42
|
+
const setAutomergeUrl = (key: string, automergeUrl: AutomergeUrl) => {
|
|
48
43
|
if (key) {
|
|
49
|
-
// Only set URL hash if
|
|
50
|
-
if (
|
|
51
|
-
setHash(setQueryParamValue(key,
|
|
44
|
+
// Only set URL hash if automerge URL changed
|
|
45
|
+
if (automergeUrl !== getQueryParamValue(key, window.location.hash))
|
|
46
|
+
setHash(setQueryParamValue(key, automergeUrl, window.location.hash))
|
|
52
47
|
}
|
|
53
|
-
if (key) localStorage.setItem(key,
|
|
48
|
+
if (key) localStorage.setItem(key, automergeUrl)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface UseBootstrapOptions<T> {
|
|
52
|
+
/** Key to use for the URL hash and localStorage */
|
|
53
|
+
key?: string
|
|
54
|
+
/** Function returning a document handle called if lookup fails. Defaults to repo.create() */
|
|
55
|
+
onNoDocument?: (repo: Repo) => DocHandle<T>
|
|
56
|
+
/** Function to call if automerge URL is invalid */
|
|
57
|
+
onInvalidAutomergeUrl?(repo: Repo, error: Error): DocHandle<T>
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/**
|
|
57
61
|
* This hook is used to set up a single document as the base of an app session.
|
|
58
62
|
* This is a common pattern for simple multiplayer apps with shareable URLs.
|
|
59
63
|
*
|
|
60
|
-
* It will first check for the
|
|
61
|
-
* //myapp/#
|
|
62
|
-
* Failing that, it will check for a `
|
|
64
|
+
* It will first check for the automergeUrl in the URL hash:
|
|
65
|
+
* //myapp/#automergeUrl=[document URL]
|
|
66
|
+
* Failing that, it will check for a `automergeUrl` key in localStorage.
|
|
63
67
|
* Failing that, it will call onNoDocument, expecting a handle to be returned.
|
|
64
68
|
*
|
|
65
69
|
* The URL and localStorage will then be updated.
|
|
66
|
-
* Finally, it will return the document
|
|
70
|
+
* Finally, it will return the Automerge document's URL.
|
|
67
71
|
*
|
|
68
72
|
* @param {string?} props.key Key to use for the URL hash and localStorage
|
|
69
73
|
* @param {function?} props.fallback Function returning a document handle called if lookup fails. Defaults to repo.create()
|
|
70
|
-
* @param {function?} props.
|
|
74
|
+
* @param {function?} props.onInvalidAutomergeUrl Function to call if URL is invalid; signature (error) => (repo, onCreate)
|
|
71
75
|
* @returns {DocHandle} The document handle
|
|
72
76
|
*/
|
|
73
|
-
interface UseBootstrapOptions<T> {
|
|
74
|
-
key?: string
|
|
75
|
-
onNoDocument?: (repo: Repo) => DocHandle<T>
|
|
76
|
-
onInvalidDocumentId?(repo: Repo, error: Error): DocHandle<T>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
77
|
export const useBootstrap = <T>({
|
|
80
|
-
key = "
|
|
78
|
+
key = "automergeUrl",
|
|
81
79
|
onNoDocument = repo => repo.create(),
|
|
82
|
-
|
|
80
|
+
onInvalidAutomergeUrl,
|
|
83
81
|
}: UseBootstrapOptions<T> = {}): DocHandle<T> => {
|
|
84
82
|
const repo = useRepo()
|
|
85
83
|
const hash = useHash()
|
|
86
84
|
|
|
87
85
|
// Try to get existing document; else create a new one
|
|
88
86
|
const handle = useMemo((): DocHandle<T> => {
|
|
89
|
-
const
|
|
87
|
+
const url = getAutomergeUrl(key, hash) as AutomergeUrl | undefined
|
|
90
88
|
try {
|
|
91
|
-
return
|
|
92
|
-
? repo.find(generateAutomergeUrl({ documentId }))
|
|
93
|
-
: onNoDocument(repo)
|
|
89
|
+
return url ? repo.find(url) : onNoDocument(repo)
|
|
94
90
|
} catch (error) {
|
|
95
|
-
// Presumably the
|
|
96
|
-
if (
|
|
97
|
-
return
|
|
91
|
+
// Presumably the URL was invalid
|
|
92
|
+
if (url && onInvalidAutomergeUrl)
|
|
93
|
+
return onInvalidAutomergeUrl(repo, error)
|
|
98
94
|
// Forward other errors
|
|
99
95
|
throw error
|
|
100
96
|
}
|
|
101
|
-
}, [hash, repo, onNoDocument,
|
|
97
|
+
}, [hash, repo, onNoDocument, onInvalidAutomergeUrl])
|
|
102
98
|
|
|
103
99
|
// Update hashroute & localStorage on changes
|
|
104
100
|
useEffect(() => {
|
|
105
101
|
if (handle) {
|
|
106
|
-
|
|
102
|
+
setAutomergeUrl(key, handle.url)
|
|
107
103
|
}
|
|
108
104
|
}, [hash, handle])
|
|
109
105
|
|
package/src/useDocument.ts
CHANGED
|
@@ -3,7 +3,15 @@ import { AutomergeUrl, DocHandleChangePayload } from "@automerge/automerge-repo"
|
|
|
3
3
|
import { useEffect, useState } from "react"
|
|
4
4
|
import { useRepo } from "./useRepo.js"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** A hook which returns a document identified by a URL and a function to change the document.
|
|
7
|
+
*
|
|
8
|
+
* @returns a tuple of the document and a function to change the document.
|
|
9
|
+
* The document will be `undefined` if the document is not available in storage or from any peers
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* This requires a {@link RepoContext} to be provided by a parent component.
|
|
13
|
+
* */
|
|
14
|
+
export function useDocument<T>(documentUrl?: AutomergeUrl): [Doc<T> | undefined, (changeFn: ChangeFn<T>) => void] {
|
|
7
15
|
const [doc, setDoc] = useState<Doc<T>>()
|
|
8
16
|
const repo = useRepo()
|
|
9
17
|
|
|
@@ -31,5 +39,5 @@ export function useDocument<T>(documentUrl?: AutomergeUrl) {
|
|
|
31
39
|
handle.change(changeFn, options)
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
return [doc, changeDoc]
|
|
42
|
+
return [doc, changeDoc]
|
|
35
43
|
}
|
package/src/useHandle.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { AutomergeUrl, DocHandle } from "@automerge/automerge-repo"
|
|
|
2
2
|
import { useState } from "react"
|
|
3
3
|
import { useRepo } from "./useRepo.js"
|
|
4
4
|
|
|
5
|
+
/** A hook which returns a {@link DocHandle} identified by a URL.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This requires a {@link RepoContext} to be provided by a parent component.
|
|
9
|
+
*/
|
|
5
10
|
export function useHandle<T>(automergeUrl: AutomergeUrl): DocHandle<T> {
|
|
6
11
|
const repo = useRepo()
|
|
7
12
|
const [handle] = useState<DocHandle<T>>(repo.find(automergeUrl))
|
package/src/useLocalAwareness.ts
CHANGED
|
@@ -3,6 +3,16 @@ import useStateRef from "react-usestateref"
|
|
|
3
3
|
import { peerEvents } from "./useRemoteAwareness.js"
|
|
4
4
|
import { DocHandle } from "@automerge/automerge-repo"
|
|
5
5
|
|
|
6
|
+
export interface UseLocalAwarenessProps {
|
|
7
|
+
/** The document handle to send ephemeral state on */
|
|
8
|
+
handle: DocHandle<unknown>
|
|
9
|
+
/** Our user ID **/
|
|
10
|
+
userId: string
|
|
11
|
+
/** The initial state object/primitive we should advertise */
|
|
12
|
+
initialState: any
|
|
13
|
+
/** How frequently to send heartbeats */
|
|
14
|
+
heartbeatTime?: number
|
|
15
|
+
}
|
|
6
16
|
/**
|
|
7
17
|
* This hook maintains state for the local client.
|
|
8
18
|
* Like React.useState, it returns a [state, setState] array.
|
|
@@ -12,19 +22,12 @@ import { DocHandle } from "@automerge/automerge-repo"
|
|
|
12
22
|
* It also broadcasts a heartbeat to let other clients know it is online.
|
|
13
23
|
*
|
|
14
24
|
* Note that userIds aren't secure (yet). Any client can lie about theirs.
|
|
15
|
-
* ChannelID is usually just your documentID with some extra characters.
|
|
16
25
|
*
|
|
17
26
|
* @param {string} props.userId Unique user ID. Clients can lie about this.
|
|
18
27
|
* @param {any} props.initialState Initial state object/primitive
|
|
19
28
|
* @param {number?1500} props.heartbeatTime How often to send a heartbeat (in ms)
|
|
20
29
|
* @returns [state, setState]
|
|
21
30
|
*/
|
|
22
|
-
export interface UseLocalAwarenessProps {
|
|
23
|
-
handle: DocHandle<unknown>
|
|
24
|
-
userId: string
|
|
25
|
-
initialState: any
|
|
26
|
-
heartbeatTime?: number
|
|
27
|
-
}
|
|
28
31
|
export const useLocalAwareness = ({
|
|
29
32
|
handle,
|
|
30
33
|
userId,
|
|
@@ -6,6 +6,22 @@ import { EventEmitter } from "eventemitter3"
|
|
|
6
6
|
// Emits new_peer event when a new peer is seen
|
|
7
7
|
export const peerEvents = new EventEmitter()
|
|
8
8
|
|
|
9
|
+
export interface UseRemoteAwarenessProps {
|
|
10
|
+
/** The handle to receive ephemeral state on */
|
|
11
|
+
handle: DocHandle<unknown>
|
|
12
|
+
/** Our user ID */
|
|
13
|
+
localUserId?: string
|
|
14
|
+
/** How long to wait (in ms) before marking a peer as offline */
|
|
15
|
+
offlineTimeout?: number
|
|
16
|
+
/** Function to provide current epoch time */
|
|
17
|
+
getTime?: () => number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A map from peer ID to their state */
|
|
21
|
+
export type PeerStates = Record<string, any>
|
|
22
|
+
/** A map from peer ID to their last heartbeat timestamp */
|
|
23
|
+
export type Heartbeats = Record<string, number>
|
|
24
|
+
|
|
9
25
|
/**
|
|
10
26
|
*
|
|
11
27
|
* This hook returns read-only state for remote clients.
|
|
@@ -18,16 +34,6 @@ export const peerEvents = new EventEmitter()
|
|
|
18
34
|
* @param {function?} props.getTime Function to provide current epoch time (used for testing)
|
|
19
35
|
* @returns [ peerStates: { [userId]: state, ... }, { [userId]: heartbeatEpochTime, ...} ]
|
|
20
36
|
*/
|
|
21
|
-
export interface UseRemoteAwarenessProps {
|
|
22
|
-
handle: DocHandle<unknown>
|
|
23
|
-
localUserId?: string
|
|
24
|
-
offlineTimeout?: number
|
|
25
|
-
getTime?: () => number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type PeerStates = Record<string, any>
|
|
29
|
-
export type Heartbeats = Record<string, number>
|
|
30
|
-
|
|
31
37
|
export const useRemoteAwareness = ({
|
|
32
38
|
handle,
|
|
33
39
|
localUserId,
|
package/src/useRepo.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Repo } from "@automerge/automerge-repo"
|
|
2
2
|
import { createContext, useContext } from "react"
|
|
3
3
|
|
|
4
|
+
/** A [React context](https://react.dev/learn/passing-data-deeply-with-context) which provides access to an Automerge repo. */
|
|
4
5
|
export const RepoContext = createContext<Repo | null>(null)
|
|
5
6
|
|
|
7
|
+
/** A [React hook](https://reactjs.org/docs/hooks-intro.html) which returns the Automerge repo from {@link RepoContext}. */
|
|
6
8
|
export function useRepo(): Repo {
|
|
7
9
|
const repo = useContext(RepoContext)
|
|
8
10
|
if (!repo) throw new Error("Repo was not found on RepoContext.")
|