@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 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
@@ -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;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,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";
@@ -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 document ID in the URL hash:
9
- * //myapp/#documentId=[document ID]
10
- * Failing that, it will check for a `documentId` key in localStorage.
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 ID.
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.onInvalidDocumentId Function to call if documentId is invalid; signature (error) => (repo, onCreate)
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
- interface UseBootstrapOptions<T> {
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,EACL,SAAS,EACT,IAAI,EAGL,MAAM,2BAA2B,CAAA;AAKlC,eAAO,MAAM,OAAO,SAAU,MAAM,8BAUnC,CAAA;AAGD,eAAO,MAAM,OAAO,cAQnB,CAAA;AAwBD;;;;;;;;;;;;;;;;GAgBG;AACH,UAAU,mBAAmB,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAA;IAC3C,mBAAmB,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;CAC7D;AAED,eAAO,MAAM,YAAY,2FAgCxB,CAAA"}
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"}
@@ -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 getDocumentId = (key, hash) => key && (getQueryParamValue(key, hash) || localStorage.getItem(key));
32
- const setDocumentId = (key, documentId) => {
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 document ID changed
35
- if (documentId !== getQueryParamValue(key, window.location.hash))
36
- setHash(setQueryParamValue(key, documentId, window.location.hash));
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, documentId);
38
+ localStorage.setItem(key, automergeUrl);
40
39
  };
41
- export const useBootstrap = ({ key = "documentId", onNoDocument = repo => repo.create(), onInvalidDocumentId, } = {}) => {
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 documentId = getDocumentId(key, hash);
62
+ const url = getAutomergeUrl(key, hash);
47
63
  try {
48
- return documentId
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 documentId was invalid
54
- if (documentId && onInvalidDocumentId)
55
- return onInvalidDocumentId(repo, error);
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, onInvalidDocumentId]);
73
+ }, [hash, repo, onNoDocument, onInvalidAutomergeUrl]);
60
74
  // Update hashroute & localStorage on changes
61
75
  useEffect(() => {
62
76
  if (handle) {
63
- setDocumentId(key, handle.documentId);
77
+ setAutomergeUrl(key, handle.url);
64
78
  }
65
79
  }, [hash, handle]);
66
80
  return handle;
@@ -1,4 +1,12 @@
1
1
  import { ChangeFn, Doc } from "@automerge/automerge/next";
2
2
  import { AutomergeUrl } from "@automerge/automerge-repo";
3
- export declare function useDocument<T>(documentUrl?: AutomergeUrl): [Doc<T>, (changeFn: ChangeFn<T>) => void];
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,uBA4BR,SAAS,CAAC,CAAC,KAAK,IAAI,EACpE"}
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"}
@@ -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();
@@ -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
@@ -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;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,GAAG,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AACD,eAAO,MAAM,iBAAiB,qDAK3B,sBAAsB,UA6CxB,CAAA"}
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;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAA;CACvB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAC5C,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE/C,eAAO,MAAM,kBAAkB,sDAK5B,uBAAuB,KAAG,CAAC,UAAU,EAAE,UAAU,CA+CnD,CAAA"}
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
@@ -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.2",
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.2",
21
- "eventemitter3": "^5.0.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": "^4.0.2",
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.1",
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": "c72b9ec33c3a1f7f39ad20cb4fa0d766afe42158"
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'
@@ -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 getDocumentId = (key: string, hash: string) =>
39
+ const getAutomergeUrl = (key: string, hash: string) =>
45
40
  key && (getQueryParamValue(key, hash) || localStorage.getItem(key))
46
41
 
47
- const setDocumentId = (key: string, documentId: DocumentId) => {
42
+ const setAutomergeUrl = (key: string, automergeUrl: AutomergeUrl) => {
48
43
  if (key) {
49
- // Only set URL hash if document ID changed
50
- if (documentId !== getQueryParamValue(key, window.location.hash))
51
- setHash(setQueryParamValue(key, documentId, window.location.hash))
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, documentId)
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 document ID in the URL hash:
61
- * //myapp/#documentId=[document ID]
62
- * Failing that, it will check for a `documentId` key in localStorage.
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 ID.
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.onInvalidDocumentId Function to call if documentId is invalid; signature (error) => (repo, onCreate)
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 = "documentId",
78
+ key = "automergeUrl",
81
79
  onNoDocument = repo => repo.create(),
82
- onInvalidDocumentId,
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 documentId = getDocumentId(key, hash) as DocumentId | undefined
87
+ const url = getAutomergeUrl(key, hash) as AutomergeUrl | undefined
90
88
  try {
91
- return documentId
92
- ? repo.find(generateAutomergeUrl({ documentId }))
93
- : onNoDocument(repo)
89
+ return url ? repo.find(url) : onNoDocument(repo)
94
90
  } catch (error) {
95
- // Presumably the documentId was invalid
96
- if (documentId && onInvalidDocumentId)
97
- return onInvalidDocumentId(repo, error)
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, onInvalidDocumentId])
97
+ }, [hash, repo, onNoDocument, onInvalidAutomergeUrl])
102
98
 
103
99
  // Update hashroute & localStorage on changes
104
100
  useEffect(() => {
105
101
  if (handle) {
106
- setDocumentId(key, handle.documentId)
102
+ setAutomergeUrl(key, handle.url)
107
103
  }
108
104
  }, [hash, handle])
109
105
 
@@ -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
- export function useDocument<T>(documentUrl?: AutomergeUrl) {
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] as [Doc<T>, (changeFn: ChangeFn<T>) => void]
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))
@@ -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.")
package/typedoc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../typedoc.base.json",
3
+ "entryPoints": ["src/index.ts"],
4
+ "readme": "none"
5
+ }