@automerge/automerge-repo 0.2.1 → 1.0.0-alpha.2

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 (59) 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 -13
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +104 -53
  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 +9 -9
  13. package/dist/helpers/headsAreSame.d.ts +2 -2
  14. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  15. package/dist/helpers/headsAreSame.js +1 -4
  16. package/dist/helpers/tests/network-adapter-tests.js +10 -10
  17. package/dist/index.d.ts +3 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/network/NetworkAdapter.d.ts +2 -3
  21. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  22. package/dist/network/NetworkSubsystem.d.ts +2 -3
  23. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  24. package/dist/network/NetworkSubsystem.js +9 -13
  25. package/dist/storage/StorageAdapter.d.ts +9 -5
  26. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  27. package/dist/storage/StorageSubsystem.d.ts +4 -4
  28. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  29. package/dist/storage/StorageSubsystem.js +109 -31
  30. package/dist/synchronizer/CollectionSynchronizer.d.ts +1 -1
  31. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  32. package/dist/synchronizer/CollectionSynchronizer.js +5 -1
  33. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  34. package/dist/synchronizer/DocSynchronizer.js +6 -5
  35. package/dist/types.d.ts +6 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +8 -5
  38. package/src/DocCollection.ts +32 -22
  39. package/src/DocHandle.ts +119 -77
  40. package/src/DocUrl.ts +90 -0
  41. package/src/Repo.ts +10 -11
  42. package/src/helpers/headsAreSame.ts +3 -5
  43. package/src/helpers/tests/network-adapter-tests.ts +10 -10
  44. package/src/index.ts +7 -5
  45. package/src/network/NetworkAdapter.ts +2 -3
  46. package/src/network/NetworkSubsystem.ts +9 -14
  47. package/src/storage/StorageAdapter.ts +7 -5
  48. package/src/storage/StorageSubsystem.ts +133 -36
  49. package/src/synchronizer/CollectionSynchronizer.ts +10 -2
  50. package/src/synchronizer/DocSynchronizer.ts +7 -6
  51. package/src/types.ts +4 -1
  52. package/test/CollectionSynchronizer.test.ts +1 -1
  53. package/test/DocCollection.test.ts +3 -2
  54. package/test/DocHandle.test.ts +40 -35
  55. package/test/DocSynchronizer.test.ts +3 -2
  56. package/test/Repo.test.ts +134 -27
  57. package/test/StorageSubsystem.test.ts +13 -10
  58. package/test/helpers/DummyNetworkAdapter.ts +2 -2
  59. 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,23 +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>>;
19
- /** `load` is called by the repo when the document is found in storage */
20
- load(binary: Uint8Array): void;
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;
21
53
  /** `update` is called by the repo when we receive changes from the network */
22
54
  update(callback: (doc: A.Doc<T>) => A.Doc<T>): void;
23
55
  /** `change` is called by the repo when the document is changed locally */
@@ -37,21 +69,22 @@ export interface DocHandleMessagePayload {
37
69
  channelId: ChannelId;
38
70
  data: Uint8Array;
39
71
  }
40
- export interface DocHandleChangePayload<T> {
72
+ export interface DocHandleEncodedChangePayload<T> {
41
73
  handle: DocHandle<T>;
42
74
  doc: A.Doc<T>;
43
75
  }
44
76
  export interface DocHandleDeletePayload<T> {
45
77
  handle: DocHandle<T>;
46
78
  }
47
- export interface DocHandlePatchPayload<T> {
79
+ export interface DocHandleChangePayload<T> {
48
80
  handle: DocHandle<T>;
81
+ doc: A.Doc<T>;
49
82
  patches: A.Patch[];
50
83
  patchInfo: A.PatchInfo<T>;
51
84
  }
52
85
  export interface DocHandleEvents<T> {
86
+ "heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void;
53
87
  change: (payload: DocHandleChangePayload<T>) => void;
54
- patch: (payload: DocHandlePatchPayload<T>) => void;
55
88
  delete: (payload: DocHandleDeletePayload<T>) => void;
56
89
  }
57
90
  export declare const HandleState: {
@@ -59,13 +92,12 @@ export declare const HandleState: {
59
92
  readonly LOADING: "loading";
60
93
  readonly REQUESTING: "requesting";
61
94
  readonly READY: "ready";
62
- readonly ERROR: "error";
95
+ readonly FAILED: "failed";
63
96
  readonly DELETED: "deleted";
64
97
  };
65
98
  export type HandleState = (typeof HandleState)[keyof typeof HandleState];
66
99
  export declare const Event: {
67
100
  readonly CREATE: "CREATE";
68
- readonly LOAD: "LOAD";
69
101
  readonly FIND: "FIND";
70
102
  readonly REQUEST: "REQUEST";
71
103
  readonly REQUEST_COMPLETE: "REQUEST_COMPLETE";
@@ -73,5 +105,6 @@ export declare const Event: {
73
105
  readonly TIMEOUT: "TIMEOUT";
74
106
  readonly DELETE: "DELETE";
75
107
  };
108
+ export declare const IDLE: "idle", LOADING: "loading", REQUESTING: "requesting", READY: "ready", FAILED: "failed", DELETED: "deleted";
76
109
  export {};
77
110
  //# 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: {
@@ -51,12 +54,18 @@ export class DocHandle//
51
54
  },
52
55
  loading: {
53
56
  on: {
54
- // LOAD is called by the Repo if the document is found in storage
55
- LOAD: { actions: "onLoad", target: READY },
57
+ // UPDATE is called by the Repo if the document is found in storage
58
+ UPDATE: { actions: "onUpdate", target: READY },
56
59
  // REQUEST is called by the Repo if the document is not found in storage
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,19 +89,16 @@ 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: {
82
- /** Apply the binary changes from storage and put the updated doc on context */
83
- onLoad: assign((context, { payload }) => {
84
- const { binary } = payload;
85
- const { doc } = context;
86
- const newDoc = A.loadIncremental(doc, binary);
87
- return { doc: newDoc };
88
- }),
89
- /** Put the updated doc on context; if it's different, emit a `change` event */
101
+ /** Put the updated doc on context */
90
102
  onUpdate: assign((context, { payload }) => {
91
103
  const { doc: oldDoc } = context;
92
104
  const { callback } = payload;
@@ -102,26 +114,30 @@ export class DocHandle//
102
114
  .onTransition(({ value: state, history, context }, event) => {
103
115
  const oldDoc = history?.context?.doc;
104
116
  const newDoc = context.doc;
105
- const docChanged = newDoc && oldDoc && !headsAreSame(newDoc, oldDoc);
117
+ console.log(`${event} ${state}`, newDoc);
118
+ const docChanged = newDoc && oldDoc && !headsAreSame(A.getHeads(newDoc), A.getHeads(oldDoc));
106
119
  if (docChanged) {
107
- this.emit("change", { handle: this, doc: newDoc });
120
+ this.emit("heads-changed", { handle: this, doc: newDoc });
121
+ const patches = A.diff(newDoc, A.getHeads(oldDoc), A.getHeads(newDoc));
122
+ if (patches.length > 0) {
123
+ const source = "change"; // TODO: pass along the source (load/change/network)
124
+ this.emit("change", {
125
+ handle: this,
126
+ doc: newDoc,
127
+ patches,
128
+ patchInfo: { before: oldDoc, after: newDoc, source },
129
+ });
130
+ }
108
131
  if (!this.isReady()) {
109
132
  this.#machine.send(REQUEST_COMPLETE);
110
133
  }
111
134
  }
112
- this.#log(`${event} → ${state}`, this.#doc);
113
135
  })
114
136
  .start();
115
137
  this.#machine.send(isNew ? CREATE : FIND);
116
138
  }
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
139
  // PRIVATE
124
- /** Returns the current document */
140
+ /** Returns the current document, regardless of state */
125
141
  get #doc() {
126
142
  return this.#machine?.getSnapshot().context.doc;
127
143
  }
@@ -134,21 +150,48 @@ export class DocHandle//
134
150
  if (!Array.isArray(awaitStates))
135
151
  awaitStates = [awaitStates];
136
152
  return Promise.any(awaitStates.map(state => waitFor(this.#machine, s => s.matches(state), {
137
- timeout: this.#timeoutDelay, // match the delay above
153
+ timeout: this.#timeoutDelay * 2000, // longer than the delay above for testing
138
154
  })));
139
155
  }
140
156
  // PUBLIC
141
- isReady = () => this.#state === READY;
142
- isReadyOrRequesting = () => this.#state === READY || this.#state === REQUESTING;
143
- isDeleted = () => this.#state === DELETED;
144
157
  /**
145
- * Returns the current document, waiting for the handle to be ready if necessary.
158
+ * Checks if the document is ready for accessing or changes.
159
+ * Note that for documents already stored locally this occurs before synchronization
160
+ * with any peers. We do not currently have an equivalent `whenSynced()`.
161
+ */
162
+ isReady = () => this.inState([HandleState.READY]);
163
+ /**
164
+ * Checks if this document has been marked as deleted.
165
+ * Deleted documents are removed from local storage and the sync process.
166
+ * It's not currently possible at runtime to undelete a document.
167
+ * @returns true if the document has been marked as deleted
146
168
  */
147
- async value(awaitStates = [READY]) {
169
+ isDeleted = () => this.inState([HandleState.DELETED]);
170
+ inState = (states) => states.some(this.#machine?.getSnapshot().matches);
171
+ get state() {
172
+ return this.#machine?.getSnapshot().value;
173
+ }
174
+ /**
175
+ * Use this to block until the document handle has finished loading.
176
+ * The async equivalent to checking `inState()`.
177
+ * @param awaitStates = [READY]
178
+ * @returns
179
+ */
180
+ async whenReady(awaitStates = [READY]) {
181
+ await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay);
182
+ }
183
+ /**
184
+ * Returns the current state of the Automerge document this handle manages.
185
+ * Note that this waits for the handle to be ready if necessary, and currently, if
186
+ * loading (or synchronization) fails, will never resolve.
187
+ *
188
+ * @param {awaitStates=[READY]} optional states to wait for, such as "LOADING". mostly for internal use.
189
+ */
190
+ async doc(awaitStates = [READY]) {
148
191
  await pause(); // yield one tick because reasons
149
192
  try {
150
193
  // wait for the document to enter one of the desired states
151
- await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay);
194
+ await this.#statePromise(awaitStates);
152
195
  }
153
196
  catch (error) {
154
197
  if (error instanceof TimeoutError)
@@ -159,18 +202,27 @@ export class DocHandle//
159
202
  // Return the document
160
203
  return this.#doc;
161
204
  }
162
- async loadAttemptedValue() {
163
- return this.value([READY, REQUESTING]);
164
- }
165
- /** `load` is called by the repo when the document is found in storage */
166
- load(binary) {
167
- if (binary.length) {
168
- this.#machine.send(LOAD, { payload: { binary } });
205
+ /**
206
+ * Returns the current state of the Automerge document this handle manages, or undefined.
207
+ * Useful in a synchronous context. Consider using `await handle.doc()` instead, check `isReady()`,
208
+ * or use `whenReady()` if you want to make sure loading is complete first.
209
+ *
210
+ * Do not confuse this with the SyncState of the document, which describes the state of the synchronization process.
211
+ *
212
+ * Note that `undefined` is not a valid Automerge document so the return from this function is unambigous.
213
+ * @returns the current document, or undefined if the document is not ready
214
+ */
215
+ docSync() {
216
+ if (!this.isReady()) {
217
+ return undefined;
169
218
  }
219
+ return this.#doc;
170
220
  }
171
221
  /** `update` is called by the repo when we receive changes from the network */
172
222
  update(callback) {
173
- this.#machine.send(UPDATE, { payload: { callback } });
223
+ this.#machine.send(UPDATE, {
224
+ payload: { callback },
225
+ });
174
226
  }
175
227
  /** `change` is called by the repo when the document is changed locally */
176
228
  change(callback, options = {}) {
@@ -192,7 +244,7 @@ export class DocHandle//
192
244
  this.#machine.send(UPDATE, {
193
245
  payload: {
194
246
  callback: (doc) => {
195
- return A.changeAt(doc, heads, options, callback);
247
+ return A.changeAt(doc, heads, options, callback).newDoc;
196
248
  },
197
249
  },
198
250
  });
@@ -214,13 +266,12 @@ export const HandleState = {
214
266
  LOADING: "loading",
215
267
  REQUESTING: "requesting",
216
268
  READY: "ready",
217
- ERROR: "error",
269
+ FAILED: "failed",
218
270
  DELETED: "deleted",
219
271
  };
220
272
  // events
221
273
  export const Event = {
222
274
  CREATE: "CREATE",
223
- LOAD: "LOAD",
224
275
  FIND: "FIND",
225
276
  REQUEST: "REQUEST",
226
277
  REQUEST_COMPLETE: "REQUEST_COMPLETE",
@@ -229,5 +280,5 @@ export const Event = {
229
280
  DELETE: "DELETE",
230
281
  };
231
282
  // CONSTANTS
232
- const { IDLE, LOADING, REQUESTING, READY, ERROR, DELETED } = HandleState;
233
- const { CREATE, LOAD, FIND, REQUEST, UPDATE, TIMEOUT, DELETE, REQUEST_COMPLETE, } = Event;
283
+ export const { IDLE, LOADING, REQUESTING, READY, FAILED, DELETED } = HandleState;
284
+ const { CREATE, 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"}