@automerge/automerge-repo 0.2.0 → 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +7 -24
  2. package/dist/DocCollection.d.ts +4 -4
  3. package/dist/DocCollection.d.ts.map +1 -1
  4. package/dist/DocCollection.js +25 -17
  5. package/dist/DocHandle.d.ts +46 -10
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +101 -36
  8. package/dist/DocUrl.d.ts +38 -18
  9. package/dist/DocUrl.d.ts.map +1 -1
  10. package/dist/DocUrl.js +63 -24
  11. package/dist/Repo.d.ts.map +1 -1
  12. package/dist/Repo.js +4 -6
  13. package/dist/helpers/headsAreSame.d.ts +1 -1
  14. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  15. package/dist/helpers/tests/network-adapter-tests.js +10 -10
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/network/NetworkAdapter.d.ts +2 -3
  20. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  21. package/dist/network/NetworkSubsystem.d.ts +2 -3
  22. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  23. package/dist/network/NetworkSubsystem.js +9 -13
  24. package/dist/storage/StorageAdapter.d.ts +9 -5
  25. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  26. package/dist/storage/StorageSubsystem.d.ts +2 -2
  27. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  28. package/dist/storage/StorageSubsystem.js +74 -26
  29. package/dist/synchronizer/CollectionSynchronizer.d.ts +1 -1
  30. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  31. package/dist/synchronizer/CollectionSynchronizer.js +5 -1
  32. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  33. package/dist/synchronizer/DocSynchronizer.js +6 -5
  34. package/dist/types.d.ts +6 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +8 -5
  37. package/src/DocCollection.ts +32 -22
  38. package/src/DocHandle.ts +121 -47
  39. package/src/DocUrl.ts +90 -0
  40. package/src/Repo.ts +5 -8
  41. package/src/helpers/tests/network-adapter-tests.ts +10 -10
  42. package/src/index.ts +7 -5
  43. package/src/network/NetworkAdapter.ts +2 -3
  44. package/src/network/NetworkSubsystem.ts +9 -14
  45. package/src/storage/StorageAdapter.ts +7 -5
  46. package/src/storage/StorageSubsystem.ts +96 -35
  47. package/src/synchronizer/CollectionSynchronizer.ts +10 -2
  48. package/src/synchronizer/DocSynchronizer.ts +7 -6
  49. package/src/types.ts +4 -1
  50. package/test/CollectionSynchronizer.test.ts +1 -1
  51. package/test/DocCollection.test.ts +3 -2
  52. package/test/DocHandle.test.ts +32 -26
  53. package/test/DocSynchronizer.test.ts +3 -2
  54. package/test/Repo.test.ts +76 -27
  55. package/test/StorageSubsystem.test.ts +10 -7
  56. package/test/helpers/DummyNetworkAdapter.ts +2 -2
  57. package/test/helpers/DummyStorageAdapter.ts +8 -4
package/README.md CHANGED
@@ -13,8 +13,6 @@ Other packages in this monorepo include:
13
13
  application.
14
14
  - [@automerge/automerge-repo-react-hooks](/packages/automerge-repo-react-hooks/): Example hooks for use with
15
15
  React.
16
- - [@automerge/automerge-repo-sync-server](/packages/automerge-repo-sync-server/): A small synchronization
17
- server that facilitates asynchronous communication between peers
18
16
 
19
17
  #### Storage adapters
20
18
 
@@ -55,34 +53,19 @@ A `Repo` exposes these methods:
55
53
  A `DocHandle` is a wrapper around an `Automerge.Doc`. Its primary function is to dispatch changes to
56
54
  the document.
57
55
 
56
+ - `handle.doc()` or `handle.docSync()`
57
+ Returns a `Promise<Doc<T>>` that will contain the current value of the document.
58
+ it waits until the document has finished loading and/or synchronizing over the network before
59
+ returning a value.
58
60
  - `handle.change((doc: T) => void)`
59
61
  Calls the provided callback with an instrumented mutable object
60
62
  representing the document. Any changes made to the document will be recorded and distributed to
61
63
  other nodes.
62
- - `handle.value()`
63
- Returns a `Promise<Doc<T>>` that will contain the current value of the document.
64
- it waits until the document has finished loading and/or synchronizing over the network before
65
- returning a value.
66
-
67
- When required, you can also access the underlying document directly, but only after the handle is ready:
68
-
69
- ```ts
70
- if (handle.ready()) {
71
- doc = handle.doc
72
- } else {
73
- handle.value().then(d => {
74
- doc = d
75
- })
76
- }
77
- ```
78
64
 
79
65
  A `DocHandle` also emits these events:
80
66
 
81
- - `change({handle: DocHandle, doc: Doc<T>})`
82
- Called any time changes are created or received on the document. Request the `value()` from the
83
- handle.
84
- - `patch({handle: DocHandle, patches: Patch[], patchInfo: PatchInfo})`
85
- Useful for manual increment maintenance of a video, most notably for text editors.
67
+ - `change({handle: DocHandle, patches: Patch[], patchInfo: PatchInfo})`
68
+ Called whenever the document changes, the handle's .doc
86
69
  - `delete`
87
70
  Called when the document is deleted locally.
88
71
 
@@ -251,7 +234,7 @@ dev:demo`.
251
234
  ### Adding a sync server
252
235
 
253
236
  First, get a sync-server running locally, following the instructions for the
254
- [automerge-repo-sync-server](/packages/automerge-repo-sync-server/) package.
237
+ [automerge-repo-sync-server](https://github.com/automerge/automerge-repo-sync-server) package.
255
238
 
256
239
  Next, update your application to synchronize with it:
257
240
 
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from "eventemitter3";
2
2
  import { DocHandle } from "./DocHandle.js";
3
- import { type DocumentId } from "./types.js";
3
+ import { DocumentId, AutomergeUrl } from "./types.js";
4
4
  import { type SharePolicy } from "./Repo.js";
5
5
  /**
6
6
  * A DocCollection is a collection of DocHandles. It supports creating new documents and finding
@@ -25,10 +25,10 @@ export declare class DocCollection extends EventEmitter<DocCollectionEvents> {
25
25
  */
26
26
  find<T>(
27
27
  /** The documentId of the handle to retrieve */
28
- documentId: DocumentId): DocHandle<T>;
28
+ automergeUrl: AutomergeUrl): DocHandle<T>;
29
29
  delete(
30
30
  /** The documentId of the handle to delete */
31
- documentId: DocumentId): void;
31
+ id: DocumentId | AutomergeUrl): void;
32
32
  }
33
33
  interface DocCollectionEvents {
34
34
  document: (arg: DocumentPayload) => void;
@@ -38,7 +38,7 @@ interface DocumentPayload {
38
38
  handle: DocHandle<any>;
39
39
  }
40
40
  interface DeleteDocumentPayload {
41
- documentId: DocumentId;
41
+ encodedDocumentId: DocumentId;
42
42
  }
43
43
  export {};
44
44
  //# sourceMappingURL=DocCollection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DocCollection.d.ts","sourceRoot":"","sources":["../src/DocCollection.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAA;AAExC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C;;;KAGK;AACL,qBAAa,aAAc,SAAQ,YAAY,CAAC,mBAAmB,CAAC;;IAGlE,sDAAsD;IACtD,WAAW,EAAE,WAAW,CAAmB;;IAuB3C,8CAA8C;IAC9C,IAAI,OAAO,uCAEV;IAED;;;;OAIG;IACH,MAAM,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC;IAyBzB;;;OAGG;IACH,IAAI,CAAC,CAAC;IACJ,+CAA+C;IAC/C,UAAU,EAAE,UAAU,GACrB,SAAS,CAAC,CAAC,CAAC;IAmBf,MAAM;IACJ,6CAA6C;IAC7C,UAAU,EAAE,UAAU;CAQzB;AAGD,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,iBAAiB,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;CACxD;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;CACvB;AAED,UAAU,qBAAqB;IAC7B,UAAU,EAAE,UAAU,CAAA;CACvB"}
1
+ {"version":3,"file":"DocCollection.d.ts","sourceRoot":"","sources":["../src/DocCollection.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAyB,YAAY,EAAE,MAAM,YAAY,CAAA;AAC5E,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAS5C;;;KAGK;AACL,qBAAa,aAAc,SAAQ,YAAY,CAAC,mBAAmB,CAAC;;IAGlE,sDAAsD;IACtD,WAAW,EAAE,WAAW,CAAmB;;IAwB3C,8CAA8C;IAC9C,IAAI,OAAO,uCAEV;IAED;;;;OAIG;IACH,MAAM,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC;IA0BzB;;;OAGG;IACH,IAAI,CAAC,CAAC;IACJ,+CAA+C;IAC/C,YAAY,EAAE,YAAY,GACzB,SAAS,CAAC,CAAC,CAAC;IAef,MAAM;IACJ,6CAA6C;IAC7C,EAAE,EAAE,UAAU,GAAG,YAAY;CAchC;AAGD,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,iBAAiB,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;CACxD;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;CACvB;AAED,UAAU,qBAAqB;IAC7B,iBAAiB,EAAE,UAAU,CAAA;CAC9B"}
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from "eventemitter3";
2
- import { v4 as uuid } from "uuid";
3
2
  import { DocHandle } from "./DocHandle.js";
3
+ import { generateAutomergeUrl, isValidAutomergeUrl, parseAutomergeUrl, } from "./DocUrl.js";
4
4
  /**
5
5
  * A DocCollection is a collection of DocHandles. It supports creating new documents and finding
6
6
  * documents by ID.
@@ -22,6 +22,8 @@ export class DocCollection extends EventEmitter {
22
22
  if (this.#handleCache[documentId])
23
23
  return this.#handleCache[documentId];
24
24
  // If not, create a new handle, cache it, and return it
25
+ if (!documentId)
26
+ throw new Error(`Invalid documentId ${documentId}`);
25
27
  const handle = new DocHandle(documentId, { isNew });
26
28
  this.#handleCache[documentId] = handle;
27
29
  return handle;
@@ -50,8 +52,9 @@ export class DocCollection extends EventEmitter {
50
52
  // }
51
53
  // or
52
54
  // - pass a "reify" function that takes a `<any>` and returns `<T>`
53
- const documentId = uuid();
54
- const handle = this.#getHandle(documentId, true);
55
+ // Generate a new UUID and store it in the buffer
56
+ const { encodedDocumentId } = parseAutomergeUrl(generateAutomergeUrl());
57
+ const handle = this.#getHandle(encodedDocumentId, true);
55
58
  this.emit("document", { handle });
56
59
  return handle;
57
60
  }
@@ -61,25 +64,30 @@ export class DocCollection extends EventEmitter {
61
64
  */
62
65
  find(
63
66
  /** The documentId of the handle to retrieve */
64
- documentId) {
65
- // TODO: we want a way to make sure we don't yield intermediate document states during initial synchronization
66
- // If we already have a handle, return it
67
- if (this.#handleCache[documentId])
68
- return this.#handleCache[documentId];
69
- // Otherwise, create a new handle
70
- const handle = this.#getHandle(documentId, false);
71
- // we don't directly initialize a value here because the StorageSubsystem and Synchronizers go
72
- // and get the data asynchronously and block on read instead of on create
73
- // emit a document event to advertise interest in this document
67
+ automergeUrl) {
68
+ if (!isValidAutomergeUrl(automergeUrl)) {
69
+ throw new Error(`Invalid AutomergeUrl: '${automergeUrl}'`);
70
+ }
71
+ const { encodedDocumentId } = parseAutomergeUrl(automergeUrl);
72
+ // If we have the handle cached, return it
73
+ if (this.#handleCache[encodedDocumentId])
74
+ return this.#handleCache[encodedDocumentId];
75
+ const handle = this.#getHandle(encodedDocumentId, false);
74
76
  this.emit("document", { handle });
75
77
  return handle;
76
78
  }
77
79
  delete(
78
80
  /** The documentId of the handle to delete */
79
- documentId) {
80
- const handle = this.#getHandle(documentId, false);
81
+ id) {
82
+ if (isValidAutomergeUrl(id)) {
83
+ ;
84
+ ({ encodedDocumentId: id } = parseAutomergeUrl(id));
85
+ }
86
+ const handle = this.#getHandle(id, false);
81
87
  handle.delete();
82
- delete this.#handleCache[documentId];
83
- this.emit("delete-document", { documentId });
88
+ delete this.#handleCache[id];
89
+ this.emit("delete-document", {
90
+ encodedDocumentId: id,
91
+ });
84
92
  }
85
93
  }
@@ -1,21 +1,55 @@
1
1
  import * as A from "@automerge/automerge";
2
2
  import EventEmitter from "eventemitter3";
3
- import type { ChannelId, DocumentId, PeerId } from "./types.js";
3
+ import { StateValue } from "xstate";
4
+ import type { ChannelId, DocumentId, PeerId, AutomergeUrl } from "./types.js";
4
5
  /** DocHandle is a wrapper around a single Automerge document that lets us listen for changes. */
5
6
  export declare class DocHandle<T>//
6
7
  extends EventEmitter<DocHandleEvents<T>> {
7
8
  #private;
8
9
  documentId: DocumentId;
10
+ get url(): AutomergeUrl;
9
11
  constructor(documentId: DocumentId, { isNew, timeoutDelay }?: DocHandleOptions);
10
- get doc(): A.unstable.Doc<T>;
12
+ /**
13
+ * Checks if the document is ready for accessing or changes.
14
+ * Note that for documents already stored locally this occurs before synchronization
15
+ * with any peers. We do not currently have an equivalent `whenSynced()`.
16
+ */
11
17
  isReady: () => boolean;
12
- isReadyOrRequesting: () => boolean;
18
+ /**
19
+ * Checks if this document has been marked as deleted.
20
+ * Deleted documents are removed from local storage and the sync process.
21
+ * It's not currently possible at runtime to undelete a document.
22
+ * @returns true if the document has been marked as deleted
23
+ */
13
24
  isDeleted: () => boolean;
25
+ inState: (states: HandleState[]) => boolean;
26
+ get state(): StateValue;
27
+ /**
28
+ * Use this to block until the document handle has finished loading.
29
+ * The async equivalent to checking `inState()`.
30
+ * @param awaitStates = [READY]
31
+ * @returns
32
+ */
33
+ whenReady(awaitStates?: HandleState[]): Promise<void>;
14
34
  /**
15
- * Returns the current document, waiting for the handle to be ready if necessary.
35
+ * Returns the current state of the Automerge document this handle manages.
36
+ * Note that this waits for the handle to be ready if necessary, and currently, if
37
+ * loading (or synchronization) fails, will never resolve.
38
+ *
39
+ * @param {awaitStates=[READY]} optional states to wait for, such as "LOADING". mostly for internal use.
16
40
  */
17
- value(awaitStates?: HandleState[]): Promise<A.unstable.Doc<T>>;
18
- loadAttemptedValue(): Promise<A.unstable.Doc<T>>;
41
+ doc(awaitStates?: HandleState[]): Promise<A.Doc<T>>;
42
+ /**
43
+ * Returns the current state of the Automerge document this handle manages, or undefined.
44
+ * Useful in a synchronous context. Consider using `await handle.doc()` instead, check `isReady()`,
45
+ * or use `whenReady()` if you want to make sure loading is complete first.
46
+ *
47
+ * Do not confuse this with the SyncState of the document, which describes the state of the synchronization process.
48
+ *
49
+ * Note that `undefined` is not a valid Automerge document so the return from this function is unambigous.
50
+ * @returns the current document, or undefined if the document is not ready
51
+ */
52
+ docSync(): A.Doc<T> | undefined;
19
53
  /** `load` is called by the repo when the document is found in storage */
20
54
  load(binary: Uint8Array): void;
21
55
  /** `update` is called by the repo when we receive changes from the network */
@@ -37,21 +71,22 @@ export interface DocHandleMessagePayload {
37
71
  channelId: ChannelId;
38
72
  data: Uint8Array;
39
73
  }
40
- export interface DocHandleChangePayload<T> {
74
+ export interface DocHandleEncodedChangePayload<T> {
41
75
  handle: DocHandle<T>;
42
76
  doc: A.Doc<T>;
43
77
  }
44
78
  export interface DocHandleDeletePayload<T> {
45
79
  handle: DocHandle<T>;
46
80
  }
47
- export interface DocHandlePatchPayload<T> {
81
+ export interface DocHandleChangePayload<T> {
48
82
  handle: DocHandle<T>;
83
+ doc: A.Doc<T>;
49
84
  patches: A.Patch[];
50
85
  patchInfo: A.PatchInfo<T>;
51
86
  }
52
87
  export interface DocHandleEvents<T> {
88
+ "heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void;
53
89
  change: (payload: DocHandleChangePayload<T>) => void;
54
- patch: (payload: DocHandlePatchPayload<T>) => void;
55
90
  delete: (payload: DocHandleDeletePayload<T>) => void;
56
91
  }
57
92
  export declare const HandleState: {
@@ -59,7 +94,7 @@ export declare const HandleState: {
59
94
  readonly LOADING: "loading";
60
95
  readonly REQUESTING: "requesting";
61
96
  readonly READY: "ready";
62
- readonly ERROR: "error";
97
+ readonly FAILED: "failed";
63
98
  readonly DELETED: "deleted";
64
99
  };
65
100
  export type HandleState = (typeof HandleState)[keyof typeof HandleState];
@@ -73,5 +108,6 @@ export declare const Event: {
73
108
  readonly TIMEOUT: "TIMEOUT";
74
109
  readonly DELETE: "DELETE";
75
110
  };
111
+ export declare const IDLE: "idle", LOADING: "loading", REQUESTING: "requesting", READY: "ready", FAILED: "failed", DELETED: "deleted";
76
112
  export {};
77
113
  //# sourceMappingURL=DocHandle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DocHandle.d.ts","sourceRoot":"","sources":["../src/DocHandle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,sBAAsB,CAAA;AAEzC,OAAO,YAAY,MAAM,eAAe,CAAA;AAiBxC,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAE/D,iGAAiG;AACjG,qBAAa,SAAS,CAAC,CAAC,CAAE,EAAE;AAC1B,SAAQ,YAAY,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;;IAQ/B,UAAU,EAAE,UAAU;gBAAtB,UAAU,EAAE,UAAU,EAC7B,EAAE,KAAa,EAAE,YAAqB,EAAE,GAAE,gBAAqB;IAqHjE,IAAI,GAAG,sBAQN;IA4BD,OAAO,gBAA8B;IACrC,mBAAmB,gBACkC;IACrD,SAAS,gBAAgC;IAEzC;;OAEG;IACG,KAAK,CAAC,WAAW,GAAE,WAAW,EAAY;IAc1C,kBAAkB;IAIxB,yEAAyE;IACzE,IAAI,CAAC,MAAM,EAAE,UAAU;IAMvB,8EAA8E;IAC9E,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAI5C,2EAA2E;IAC3E,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAM;IAehE,QAAQ,CACN,KAAK,EAAE,CAAC,CAAC,KAAK,EACd,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EACvB,OAAO,GAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAM;IAgBlC,gFAAgF;IAChF,OAAO;IAIP,kEAAkE;IAClE,MAAM;CAGP;AAID,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,SAAS,CAAA;IACpB,IAAI,EAAE,UAAU,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;CACrB;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,CAAA;IAClB,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;CAC1B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,MAAM,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACpD,KAAK,EAAE,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAClD,MAAM,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;CACrD;AAMD,eAAO,MAAM,WAAW;;;;;;;CAOd,CAAA;AACV,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAA;AAkBxE,eAAO,MAAM,KAAK;;;;;;;;;CASR,CAAA"}
1
+ {"version":3,"file":"DocHandle.d.ts","sourceRoot":"","sources":["../src/DocHandle.ts"],"names":[],"mappings":"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"}
package/dist/DocHandle.js CHANGED
@@ -6,6 +6,7 @@ import { waitFor } from "xstate/lib/waitFor.js";
6
6
  import { headsAreSame } from "./helpers/headsAreSame.js";
7
7
  import { pause } from "./helpers/pause.js";
8
8
  import { TimeoutError, withTimeout } from "./helpers/withTimeout.js";
9
+ import { stringifyAutomergeUrl } from "./DocUrl.js";
9
10
  /** DocHandle is a wrapper around a single Automerge document that lets us listen for changes. */
10
11
  export class DocHandle//
11
12
  extends EventEmitter {
@@ -13,31 +14,33 @@ export class DocHandle//
13
14
  #log;
14
15
  #machine;
15
16
  #timeoutDelay;
16
- constructor(documentId, { isNew = false, timeoutDelay = 700000 } = {}) {
17
+ get url() {
18
+ return stringifyAutomergeUrl({ documentId: this.documentId });
19
+ }
20
+ constructor(documentId, { isNew = false, timeoutDelay = 60_000 } = {}) {
17
21
  super();
18
22
  this.documentId = documentId;
19
23
  this.#timeoutDelay = timeoutDelay;
20
- this.#log = debug(`automerge-repo:dochandle:${documentId.slice(0, 5)}`);
24
+ this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`);
21
25
  // initial doc
22
- const doc = A.init({
23
- patchCallback: (patches, patchInfo) => this.emit("patch", { handle: this, patches, patchInfo }),
24
- });
26
+ const doc = A.init();
25
27
  /**
26
28
  * Internally we use a state machine to orchestrate document loading and/or syncing, in order to
27
29
  * avoid requesting data we already have, or surfacing intermediate values to the consumer.
28
30
  *
29
- * ┌─────────┐ ┌────────────┐
30
- * ┌───────┐ ┌──FIND──┤ loading ├─REQUEST──►│ requesting ├─UPDATE──┐
31
+ * ┌─────────────────────┬─────────TIMEOUT────►┌────────┐
32
+ * ┌───┴─────┐ ┌───┴────────┐ │ failed
33
+ * ┌───────┐ ┌──FIND──┤ loading ├─REQUEST──►│ requesting ├─UPDATE──┐ └────────┘
31
34
  * │ idle ├──┤ └───┬─────┘ └────────────┘ │
32
- * └───────┘ │ │ └─►┌─────────┐
33
- * │ └───────LOAD───────────────────────────────►│ ready │
34
- * └──CREATE───────────────────────────────────────────────►└─────────┘
35
+ * └───────┘ │ │ └─►┌────────┐
36
+ * │ └───────LOAD───────────────────────────────►│ ready │
37
+ * └──CREATE───────────────────────────────────────────────►└────────┘
35
38
  */
36
39
  this.#machine = interpret(createMachine({
37
40
  predictableActionArguments: true,
38
41
  id: "docHandle",
39
42
  initial: IDLE,
40
- context: { documentId, doc },
43
+ context: { documentId: this.documentId, doc },
41
44
  states: {
42
45
  idle: {
43
46
  on: {
@@ -57,6 +60,12 @@ export class DocHandle//
57
60
  REQUEST: { target: REQUESTING },
58
61
  DELETE: { actions: "onDelete", target: DELETED },
59
62
  },
63
+ after: [
64
+ {
65
+ delay: this.#timeoutDelay,
66
+ target: FAILED,
67
+ },
68
+ ],
60
69
  },
61
70
  requesting: {
62
71
  on: {
@@ -66,6 +75,12 @@ export class DocHandle//
66
75
  REQUEST_COMPLETE: { target: READY },
67
76
  DELETE: { actions: "onDelete", target: DELETED },
68
77
  },
78
+ after: [
79
+ {
80
+ delay: this.#timeoutDelay,
81
+ target: FAILED,
82
+ },
83
+ ],
69
84
  },
70
85
  ready: {
71
86
  on: {
@@ -74,8 +89,12 @@ export class DocHandle//
74
89
  DELETE: { actions: "onDelete", target: DELETED },
75
90
  },
76
91
  },
77
- error: {},
78
- deleted: {},
92
+ failed: {
93
+ type: "final",
94
+ },
95
+ deleted: {
96
+ type: "final",
97
+ },
79
98
  },
80
99
  }, {
81
100
  actions: {
@@ -102,26 +121,30 @@ export class DocHandle//
102
121
  .onTransition(({ value: state, history, context }, event) => {
103
122
  const oldDoc = history?.context?.doc;
104
123
  const newDoc = context.doc;
124
+ this.#log(`${event} → ${state}`, newDoc);
105
125
  const docChanged = newDoc && oldDoc && !headsAreSame(newDoc, oldDoc);
106
126
  if (docChanged) {
107
- this.emit("change", { handle: this, doc: newDoc });
127
+ this.emit("heads-changed", { handle: this, doc: newDoc });
128
+ const patches = A.diff(newDoc, A.getHeads(oldDoc), A.getHeads(newDoc));
129
+ if (patches.length > 0) {
130
+ const source = "change"; // TODO: pass along the source (load/change/network)
131
+ this.emit("change", {
132
+ handle: this,
133
+ doc: newDoc,
134
+ patches,
135
+ patchInfo: { before: oldDoc, after: newDoc, source },
136
+ });
137
+ }
108
138
  if (!this.isReady()) {
109
139
  this.#machine.send(REQUEST_COMPLETE);
110
140
  }
111
141
  }
112
- this.#log(`${event} → ${state}`, this.#doc);
113
142
  })
114
143
  .start();
115
144
  this.#machine.send(isNew ? CREATE : FIND);
116
145
  }
117
- get doc() {
118
- if (!this.isReady()) {
119
- throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
120
- }
121
- return this.#doc;
122
- }
123
146
  // PRIVATE
124
- /** Returns the current document */
147
+ /** Returns the current document, regardless of state */
125
148
  get #doc() {
126
149
  return this.#machine?.getSnapshot().context.doc;
127
150
  }
@@ -134,21 +157,48 @@ export class DocHandle//
134
157
  if (!Array.isArray(awaitStates))
135
158
  awaitStates = [awaitStates];
136
159
  return Promise.any(awaitStates.map(state => waitFor(this.#machine, s => s.matches(state), {
137
- timeout: this.#timeoutDelay, // match the delay above
160
+ timeout: this.#timeoutDelay * 2000, // longer than the delay above for testing
138
161
  })));
139
162
  }
140
163
  // PUBLIC
141
- isReady = () => this.#state === READY;
142
- isReadyOrRequesting = () => this.#state === READY || this.#state === REQUESTING;
143
- isDeleted = () => this.#state === DELETED;
144
164
  /**
145
- * Returns the current document, waiting for the handle to be ready if necessary.
165
+ * Checks if the document is ready for accessing or changes.
166
+ * Note that for documents already stored locally this occurs before synchronization
167
+ * with any peers. We do not currently have an equivalent `whenSynced()`.
168
+ */
169
+ isReady = () => this.inState([HandleState.READY]);
170
+ /**
171
+ * Checks if this document has been marked as deleted.
172
+ * Deleted documents are removed from local storage and the sync process.
173
+ * It's not currently possible at runtime to undelete a document.
174
+ * @returns true if the document has been marked as deleted
175
+ */
176
+ isDeleted = () => this.inState([HandleState.DELETED]);
177
+ inState = (states) => states.some(this.#machine?.getSnapshot().matches);
178
+ get state() {
179
+ return this.#machine?.getSnapshot().value;
180
+ }
181
+ /**
182
+ * Use this to block until the document handle has finished loading.
183
+ * The async equivalent to checking `inState()`.
184
+ * @param awaitStates = [READY]
185
+ * @returns
146
186
  */
147
- async value(awaitStates = [READY]) {
187
+ async whenReady(awaitStates = [READY]) {
188
+ await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay);
189
+ }
190
+ /**
191
+ * Returns the current state of the Automerge document this handle manages.
192
+ * Note that this waits for the handle to be ready if necessary, and currently, if
193
+ * loading (or synchronization) fails, will never resolve.
194
+ *
195
+ * @param {awaitStates=[READY]} optional states to wait for, such as "LOADING". mostly for internal use.
196
+ */
197
+ async doc(awaitStates = [READY]) {
148
198
  await pause(); // yield one tick because reasons
149
199
  try {
150
200
  // wait for the document to enter one of the desired states
151
- await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay);
201
+ await this.#statePromise(awaitStates);
152
202
  }
153
203
  catch (error) {
154
204
  if (error instanceof TimeoutError)
@@ -159,18 +209,33 @@ export class DocHandle//
159
209
  // Return the document
160
210
  return this.#doc;
161
211
  }
162
- async loadAttemptedValue() {
163
- return this.value([READY, REQUESTING]);
212
+ /**
213
+ * Returns the current state of the Automerge document this handle manages, or undefined.
214
+ * Useful in a synchronous context. Consider using `await handle.doc()` instead, check `isReady()`,
215
+ * or use `whenReady()` if you want to make sure loading is complete first.
216
+ *
217
+ * Do not confuse this with the SyncState of the document, which describes the state of the synchronization process.
218
+ *
219
+ * Note that `undefined` is not a valid Automerge document so the return from this function is unambigous.
220
+ * @returns the current document, or undefined if the document is not ready
221
+ */
222
+ docSync() {
223
+ if (!this.isReady()) {
224
+ return undefined;
225
+ }
226
+ return this.#doc;
164
227
  }
165
228
  /** `load` is called by the repo when the document is found in storage */
166
229
  load(binary) {
167
- if (binary.length) {
230
+ if (binary.length && binary.length > 0) {
168
231
  this.#machine.send(LOAD, { payload: { binary } });
169
232
  }
170
233
  }
171
234
  /** `update` is called by the repo when we receive changes from the network */
172
235
  update(callback) {
173
- this.#machine.send(UPDATE, { payload: { callback } });
236
+ this.#machine.send(UPDATE, {
237
+ payload: { callback },
238
+ });
174
239
  }
175
240
  /** `change` is called by the repo when the document is changed locally */
176
241
  change(callback, options = {}) {
@@ -192,7 +257,7 @@ export class DocHandle//
192
257
  this.#machine.send(UPDATE, {
193
258
  payload: {
194
259
  callback: (doc) => {
195
- return A.changeAt(doc, heads, options, callback);
260
+ return A.changeAt(doc, heads, options, callback).newDoc;
196
261
  },
197
262
  },
198
263
  });
@@ -214,7 +279,7 @@ export const HandleState = {
214
279
  LOADING: "loading",
215
280
  REQUESTING: "requesting",
216
281
  READY: "ready",
217
- ERROR: "error",
282
+ FAILED: "failed",
218
283
  DELETED: "deleted",
219
284
  };
220
285
  // events
@@ -229,5 +294,5 @@ export const Event = {
229
294
  DELETE: "DELETE",
230
295
  };
231
296
  // CONSTANTS
232
- const { IDLE, LOADING, REQUESTING, READY, ERROR, DELETED } = HandleState;
297
+ export const { IDLE, LOADING, REQUESTING, READY, FAILED, DELETED } = HandleState;
233
298
  const { CREATE, LOAD, FIND, REQUEST, UPDATE, TIMEOUT, DELETE, REQUEST_COMPLETE, } = Event;
package/dist/DocUrl.d.ts CHANGED
@@ -1,20 +1,40 @@
1
- /// <reference types="node" />
2
- export declare const linkForDocumentId: (id: any) => string;
3
- export declare const documentIdFromShareLink: (link: any) => any;
4
- export declare const isValidShareLink: (str: any) => boolean;
5
- export declare const parts: (str: any) => {
6
- key: any;
7
- nonCrc: any;
8
- crc: any;
1
+ import { type AutomergeUrl, type BinaryDocumentId, type DocumentId } from "./types";
2
+ export declare const urlPrefix = "automerge:";
3
+ /**
4
+ * given an Automerge URL, return a decoded DocumentId (and the encoded DocumentId)
5
+ *
6
+ * @param url
7
+ * @returns { documentId: Uint8Array(16), encodedDocumentId: bs58check.encode(documentId) }
8
+ */
9
+ export declare const parseAutomergeUrl: (url: AutomergeUrl) => {
10
+ binaryDocumentId: BinaryDocumentId;
11
+ encodedDocumentId: DocumentId;
9
12
  };
10
- export declare const encodedParts: (str: any) => {
11
- nonCrc: any;
12
- key: any;
13
- crc: any;
14
- };
15
- export declare const withCrc: (str: any) => string;
16
- export declare const encode: (str: any) => any;
17
- export declare const decode: (str: any) => any;
18
- export declare const hexToBuffer: (key: any) => Buffer;
19
- export declare const bufferToHex: (key: any) => any;
13
+ interface StringifyAutomergeUrlOptions {
14
+ documentId: DocumentId | BinaryDocumentId;
15
+ }
16
+ /**
17
+ * Given a documentId in either canonical form, return an Automerge URL
18
+ * Throws on invalid input.
19
+ * Note: this is an object because we anticipate adding fields in the future.
20
+ * @param { documentId: EncodedDocumentId | DocumentId }
21
+ * @returns AutomergeUrl
22
+ */
23
+ export declare const stringifyAutomergeUrl: ({ documentId, }: StringifyAutomergeUrlOptions) => AutomergeUrl;
24
+ /**
25
+ * Given a string, return true if it is a valid Automerge URL
26
+ * also acts as a type discriminator in Typescript.
27
+ * @param str: URL candidate
28
+ * @returns boolean
29
+ */
30
+ export declare const isValidAutomergeUrl: (str: string) => str is AutomergeUrl;
31
+ /**
32
+ * generateAutomergeUrl produces a new AutomergeUrl.
33
+ * generally only called by create(), but used in tests as well.
34
+ * @returns a new Automerge URL with a random UUID documentId
35
+ */
36
+ export declare const generateAutomergeUrl: () => AutomergeUrl;
37
+ export declare const documentIdToBinary: (docId: DocumentId) => BinaryDocumentId | undefined;
38
+ export declare const binaryToDocumentId: (docId: BinaryDocumentId) => DocumentId;
39
+ export {};
20
40
  //# sourceMappingURL=DocUrl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DocUrl.d.ts","sourceRoot":"","sources":["../src/DocUrl.ts"],"names":[],"mappings":";AAIA,eAAO,MAAM,iBAAiB,qBAA6C,CAAA;AAE3E,eAAO,MAAM,uBAAuB,oBAInC,CAAA;AAED,eAAO,MAAM,gBAAgB,uBAG5B,CAAA;AAED,eAAO,MAAM,KAAK;;;;CAQjB,CAAA;AAED,eAAO,MAAM,YAAY;;;;CAIxB,CAAA;AAED,eAAO,MAAM,OAAO,sBAAwC,CAAA;AAE5D,eAAO,MAAM,MAAM,mBAAyC,CAAA;AAE5D,eAAO,MAAM,MAAM,mBAAyC,CAAA;AAE5D,eAAO,MAAM,WAAW,sBAC8B,CAAA;AAEtD,eAAO,MAAM,WAAW,mBAC0B,CAAA"}
1
+ {"version":3,"file":"DocUrl.d.ts","sourceRoot":"","sources":["../src/DocUrl.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAChB,MAAM,SAAS,CAAA;AAIhB,eAAO,MAAM,SAAS,eAAe,CAAA;AAErC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAAS,YAAY;;;CAIlD,CAAA;AAED,UAAU,4BAA4B;IACpC,UAAU,EAAE,UAAU,GAAG,gBAAgB,CAAA;CAC1C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,oBAE/B,4BAA4B,KAAG,YAQjC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,QAAS,MAAM,wBAK9C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,QAAO,YAGpC,CAAA;AAEJ,eAAO,MAAM,kBAAkB,UACtB,UAAU,KAChB,gBAAgB,GAAG,SACyC,CAAA;AAE/D,eAAO,MAAM,kBAAkB,UAAW,gBAAgB,KAAG,UACtB,CAAA"}