@automerge/automerge-repo 0.2.1 → 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.
- package/README.md +7 -24
- package/dist/DocCollection.d.ts +4 -4
- package/dist/DocCollection.d.ts.map +1 -1
- package/dist/DocCollection.js +25 -17
- package/dist/DocHandle.d.ts +46 -10
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +101 -36
- package/dist/DocUrl.d.ts +38 -18
- package/dist/DocUrl.d.ts.map +1 -1
- package/dist/DocUrl.js +63 -24
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +4 -6
- package/dist/helpers/headsAreSame.d.ts +1 -1
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +10 -10
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/network/NetworkAdapter.d.ts +2 -3
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.d.ts +2 -3
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +9 -13
- package/dist/storage/StorageAdapter.d.ts +9 -5
- package/dist/storage/StorageAdapter.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.d.ts +2 -2
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +73 -25
- package/dist/synchronizer/CollectionSynchronizer.d.ts +1 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +5 -1
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +6 -5
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -5
- package/src/DocCollection.ts +32 -22
- package/src/DocHandle.ts +121 -47
- package/src/DocUrl.ts +90 -0
- package/src/Repo.ts +5 -8
- package/src/helpers/tests/network-adapter-tests.ts +10 -10
- package/src/index.ts +7 -5
- package/src/network/NetworkAdapter.ts +2 -3
- package/src/network/NetworkSubsystem.ts +9 -14
- package/src/storage/StorageAdapter.ts +7 -5
- package/src/storage/StorageSubsystem.ts +95 -34
- package/src/synchronizer/CollectionSynchronizer.ts +10 -2
- package/src/synchronizer/DocSynchronizer.ts +7 -6
- package/src/types.ts +4 -1
- package/test/CollectionSynchronizer.test.ts +1 -1
- package/test/DocCollection.test.ts +3 -2
- package/test/DocHandle.test.ts +32 -26
- package/test/DocSynchronizer.test.ts +3 -2
- package/test/Repo.test.ts +76 -27
- package/test/StorageSubsystem.test.ts +10 -7
- package/test/helpers/DummyNetworkAdapter.ts +2 -2
- 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,
|
|
82
|
-
Called
|
|
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](/
|
|
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
|
|
package/dist/DocCollection.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EventEmitter from "eventemitter3";
|
|
2
2
|
import { DocHandle } from "./DocHandle.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
28
|
+
automergeUrl: AutomergeUrl): DocHandle<T>;
|
|
29
29
|
delete(
|
|
30
30
|
/** The documentId of the handle to delete */
|
|
31
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/DocCollection.js
CHANGED
|
@@ -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
|
-
|
|
54
|
-
const
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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[
|
|
83
|
-
this.emit("delete-document", {
|
|
88
|
+
delete this.#handleCache[id];
|
|
89
|
+
this.emit("delete-document", {
|
|
90
|
+
encodedDocumentId: id,
|
|
91
|
+
});
|
|
84
92
|
}
|
|
85
93
|
}
|
package/dist/DocHandle.d.ts
CHANGED
|
@@ -1,21 +1,55 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge";
|
|
2
2
|
import EventEmitter from "eventemitter3";
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/DocHandle.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
*
|
|
31
|
+
* ┌─────────────────────┬─────────TIMEOUT────►┌────────┐
|
|
32
|
+
* ┌───┴─────┐ ┌───┴────────┐ │ failed │
|
|
33
|
+
* ┌───────┐ ┌──FIND──┤ loading ├─REQUEST──►│ requesting ├─UPDATE──┐ └────────┘
|
|
31
34
|
* │ idle ├──┤ └───┬─────┘ └────────────┘ │
|
|
32
|
-
* └───────┘ │ │
|
|
33
|
-
* │ └───────LOAD───────────────────────────────►│
|
|
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
|
-
|
|
78
|
-
|
|
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("
|
|
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, //
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
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, {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
package/dist/DocUrl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocUrl.d.ts","sourceRoot":"","sources":["../src/DocUrl.ts"],"names":[],"mappings":"
|
|
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"}
|