@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.
- 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 -13
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +104 -53
- 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 +9 -9
- package/dist/helpers/headsAreSame.d.ts +2 -2
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/headsAreSame.js +1 -4
- 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 +4 -4
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +109 -31
- 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 +119 -77
- package/src/DocUrl.ts +90 -0
- package/src/Repo.ts +10 -11
- package/src/helpers/headsAreSame.ts +3 -5
- 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 +133 -36
- 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 +40 -35
- package/test/DocSynchronizer.test.ts +3 -2
- package/test/Repo.test.ts +134 -27
- package/test/StorageSubsystem.test.ts +13 -10
- 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,23 +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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
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: {
|
|
@@ -51,12 +54,18 @@ export class DocHandle//
|
|
|
51
54
|
},
|
|
52
55
|
loading: {
|
|
53
56
|
on: {
|
|
54
|
-
//
|
|
55
|
-
|
|
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
|
-
|
|
78
|
-
|
|
92
|
+
failed: {
|
|
93
|
+
type: "final",
|
|
94
|
+
},
|
|
95
|
+
deleted: {
|
|
96
|
+
type: "final",
|
|
97
|
+
},
|
|
79
98
|
},
|
|
80
99
|
}, {
|
|
81
100
|
actions: {
|
|
82
|
-
/**
|
|
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
|
-
|
|
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("
|
|
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, //
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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, {
|
|
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
|
-
|
|
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,
|
|
233
|
-
const { CREATE,
|
|
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
|
-
|
|
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"}
|