@automerge/automerge-repo 1.1.4 → 1.1.8
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 +3 -22
- package/dist/DocHandle.d.ts +124 -100
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +239 -231
- package/dist/Repo.d.ts +10 -3
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +22 -1
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
- package/dist/helpers/debounce.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +2 -2
- package/dist/helpers/tests/storage-adapter-tests.d.ts +7 -0
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -0
- package/dist/helpers/tests/storage-adapter-tests.js +128 -0
- package/dist/helpers/throttle.d.ts.map +1 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/synchronizer/DocSynchronizer.js +1 -1
- package/package.json +4 -4
- package/src/DocHandle.ts +325 -375
- package/src/Repo.ts +35 -8
- package/src/helpers/tests/network-adapter-tests.ts +4 -2
- package/src/helpers/tests/storage-adapter-tests.ts +193 -0
- package/src/index.ts +43 -0
- package/src/synchronizer/DocSynchronizer.ts +1 -1
- package/test/CollectionSynchronizer.test.ts +1 -3
- package/test/DocHandle.test.ts +19 -1
- package/test/DocSynchronizer.test.ts +1 -4
- package/test/DummyStorageAdapter.test.ts +11 -0
- package/test/Repo.test.ts +179 -53
- package/test/helpers/DummyNetworkAdapter.ts +20 -18
- package/test/helpers/DummyStorageAdapter.ts +5 -1
- package/test/remoteHeads.test.ts +1 -1
- package/tsconfig.json +1 -0
package/dist/DocHandle.js
CHANGED
|
@@ -1,44 +1,40 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge/next";
|
|
2
2
|
import debug from "debug";
|
|
3
3
|
import { EventEmitter } from "eventemitter3";
|
|
4
|
-
import { assign,
|
|
5
|
-
import { waitFor } from "xstate/lib/waitFor.js";
|
|
4
|
+
import { assertEvent, assign, createActor, setup, waitFor } from "xstate";
|
|
6
5
|
import { stringifyAutomergeUrl } from "./AutomergeUrl.js";
|
|
7
6
|
import { encode } from "./helpers/cbor.js";
|
|
8
7
|
import { headsAreSame } from "./helpers/headsAreSame.js";
|
|
9
8
|
import { withTimeout } from "./helpers/withTimeout.js";
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
9
|
+
/**
|
|
10
|
+
* A DocHandle is a wrapper around a single Automerge document that lets us listen for changes and
|
|
11
|
+
* notify the network and storage of new changes.
|
|
12
12
|
*
|
|
13
13
|
* @remarks
|
|
14
|
-
* A `DocHandle` represents a document which is being managed by a {@link Repo}.
|
|
15
|
-
* To obtain `DocHandle` use {@link Repo.find} or {@link Repo.create}.
|
|
14
|
+
* A `DocHandle` represents a document which is being managed by a {@link Repo}. You shouldn't ever
|
|
15
|
+
* instantiate this yourself. To obtain `DocHandle` use {@link Repo.find} or {@link Repo.create}.
|
|
16
16
|
*
|
|
17
17
|
* To modify the underlying document use either {@link DocHandle.change} or
|
|
18
|
-
* {@link DocHandle.changeAt}. These methods will notify the `Repo` that some
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
export class DocHandle
|
|
23
|
-
extends EventEmitter {
|
|
18
|
+
* {@link DocHandle.changeAt}. These methods will notify the `Repo` that some change has occured and
|
|
19
|
+
* the `Repo` will save any new changes to the attached {@link StorageAdapter} and send sync
|
|
20
|
+
* messages to connected peers.
|
|
21
|
+
*/
|
|
22
|
+
export class DocHandle extends EventEmitter {
|
|
24
23
|
documentId;
|
|
25
24
|
#log;
|
|
25
|
+
/** The XState actor running our state machine. */
|
|
26
26
|
#machine;
|
|
27
|
+
/** The last known state of our document. */
|
|
28
|
+
#prevDocState;
|
|
29
|
+
/** How long to wait before giving up on a document. (Note that a document will be marked
|
|
30
|
+
* unavailable much sooner if all known peers respond that they don't have it.) */
|
|
27
31
|
#timeoutDelay = 60_000;
|
|
32
|
+
/** A dictionary mapping each peer to the last heads we know they have. */
|
|
28
33
|
#remoteHeads = {};
|
|
29
|
-
/** The URL of this document
|
|
30
|
-
*
|
|
31
|
-
* @remarks
|
|
32
|
-
* This can be used to request the document from an instance of {@link Repo}
|
|
33
|
-
*/
|
|
34
|
-
get url() {
|
|
35
|
-
return stringifyAutomergeUrl({ documentId: this.documentId });
|
|
36
|
-
}
|
|
37
34
|
/** @hidden */
|
|
38
35
|
constructor(documentId, options = {}) {
|
|
39
36
|
super();
|
|
40
37
|
this.documentId = documentId;
|
|
41
|
-
this.documentId = documentId;
|
|
42
38
|
if ("timeoutDelay" in options && options.timeoutDelay) {
|
|
43
39
|
this.#timeoutDelay = options.timeoutDelay;
|
|
44
40
|
}
|
|
@@ -55,140 +51,85 @@ export class DocHandle//
|
|
|
55
51
|
doc = A.init();
|
|
56
52
|
}
|
|
57
53
|
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
const delay = this.#timeoutDelay;
|
|
55
|
+
const machine = setup({
|
|
56
|
+
types: {
|
|
57
|
+
context: {},
|
|
58
|
+
events: {},
|
|
59
|
+
},
|
|
60
|
+
actions: {
|
|
61
|
+
/** Update the doc using the given callback and put the modified doc in context */
|
|
62
|
+
onUpdate: assign(({ context, event }) => {
|
|
63
|
+
const oldDoc = context.doc;
|
|
64
|
+
assertEvent(event, UPDATE);
|
|
65
|
+
const { callback } = event.payload;
|
|
66
|
+
const doc = callback(oldDoc);
|
|
67
|
+
return { doc };
|
|
68
|
+
}),
|
|
69
|
+
onDelete: assign(() => {
|
|
70
|
+
this.emit("delete", { handle: this });
|
|
71
|
+
return { doc: undefined };
|
|
72
|
+
}),
|
|
73
|
+
onUnavailable: () => {
|
|
74
|
+
this.emit("unavailable", { handle: this });
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}).createMachine({
|
|
78
|
+
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
|
|
79
|
+
// You can use the XState extension for VS Code to visualize this machine.
|
|
80
|
+
// Or, you can see this static visualization (last updated April 2024): https://stately.ai/registry/editor/d7af9b58-c518-44f1-9c36-92a238b04a7a?machineId=91c387e7-0f01-42c9-a21d-293e9bf95bb7
|
|
81
|
+
initial: "idle",
|
|
82
|
+
context: { documentId, doc },
|
|
83
|
+
on: {
|
|
84
|
+
UPDATE: { actions: "onUpdate" },
|
|
85
|
+
DELETE: ".deleted",
|
|
86
|
+
},
|
|
75
87
|
states: {
|
|
76
88
|
idle: {
|
|
77
89
|
on: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// If we're accessing an existing document, we need to request it from storage
|
|
81
|
-
// and/or the network
|
|
82
|
-
FIND: { target: LOADING },
|
|
83
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
90
|
+
CREATE: "ready",
|
|
91
|
+
FIND: "loading",
|
|
84
92
|
},
|
|
85
93
|
},
|
|
86
94
|
loading: {
|
|
87
95
|
on: {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
REQUEST: { target: REQUESTING },
|
|
92
|
-
// AWAIT_NETWORK is called by the repo if the document is not found in storage but the network is not yet ready
|
|
93
|
-
AWAIT_NETWORK: { target: AWAITING_NETWORK },
|
|
94
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
96
|
+
REQUEST: "requesting",
|
|
97
|
+
DOC_READY: "ready",
|
|
98
|
+
AWAIT_NETWORK: "awaitingNetwork",
|
|
95
99
|
},
|
|
96
|
-
after: [
|
|
97
|
-
{
|
|
98
|
-
delay: this.#timeoutDelay,
|
|
99
|
-
target: UNAVAILABLE,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
100
|
+
after: { [delay]: "unavailable" },
|
|
102
101
|
},
|
|
103
102
|
awaitingNetwork: {
|
|
104
|
-
on: {
|
|
105
|
-
NETWORK_READY: { target: REQUESTING },
|
|
106
|
-
},
|
|
103
|
+
on: { NETWORK_READY: "requesting" },
|
|
107
104
|
},
|
|
108
105
|
requesting: {
|
|
109
106
|
on: {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
actions: "onUnavailable",
|
|
113
|
-
},
|
|
114
|
-
// UPDATE is called by the Repo when we receive changes from the network
|
|
115
|
-
UPDATE: { actions: "onUpdate" },
|
|
116
|
-
// REQUEST_COMPLETE is called from `onUpdate` when the doc has been fully loaded from the network
|
|
117
|
-
REQUEST_COMPLETE: { target: READY },
|
|
118
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
107
|
+
DOC_UNAVAILABLE: "unavailable",
|
|
108
|
+
DOC_READY: "ready",
|
|
119
109
|
},
|
|
120
|
-
after: [
|
|
121
|
-
{
|
|
122
|
-
delay: this.#timeoutDelay,
|
|
123
|
-
target: UNAVAILABLE,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
ready: {
|
|
128
|
-
on: {
|
|
129
|
-
// UPDATE is called by the Repo when we receive changes from the network
|
|
130
|
-
UPDATE: { actions: "onUpdate", target: READY },
|
|
131
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
deleted: {
|
|
135
|
-
type: "final",
|
|
110
|
+
after: { [delay]: "unavailable" },
|
|
136
111
|
},
|
|
137
112
|
unavailable: {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// REQUEST_COMPLETE is called from `onUpdate` when the doc has been fully loaded from the network
|
|
141
|
-
REQUEST_COMPLETE: { target: READY },
|
|
142
|
-
DELETE: { actions: "onDelete", target: DELETED },
|
|
143
|
-
},
|
|
113
|
+
entry: "onUnavailable",
|
|
114
|
+
on: { DOC_READY: "ready" },
|
|
144
115
|
},
|
|
116
|
+
ready: {},
|
|
117
|
+
deleted: { entry: "onDelete", type: "final" },
|
|
145
118
|
},
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const { doc } = context;
|
|
161
|
-
this.emit("unavailable", { handle: this });
|
|
162
|
-
return { doc };
|
|
163
|
-
}),
|
|
164
|
-
},
|
|
165
|
-
}))
|
|
166
|
-
.onTransition(({ value: state, history, context }, event) => {
|
|
167
|
-
const oldDoc = history?.context?.doc;
|
|
168
|
-
const newDoc = context.doc;
|
|
169
|
-
this.#log(`${history?.value}: ${event.type} → ${state}`, newDoc);
|
|
170
|
-
const docChanged = newDoc &&
|
|
171
|
-
oldDoc &&
|
|
172
|
-
!headsAreSame(A.getHeads(newDoc), A.getHeads(oldDoc));
|
|
173
|
-
if (docChanged) {
|
|
174
|
-
this.emit("heads-changed", { handle: this, doc: newDoc });
|
|
175
|
-
const patches = A.diff(newDoc, A.getHeads(oldDoc), A.getHeads(newDoc));
|
|
176
|
-
if (patches.length > 0) {
|
|
177
|
-
const source = "change"; // TODO: pass along the source (load/change/network)
|
|
178
|
-
this.emit("change", {
|
|
179
|
-
handle: this,
|
|
180
|
-
doc: newDoc,
|
|
181
|
-
patches,
|
|
182
|
-
patchInfo: { before: oldDoc, after: newDoc, source },
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
if (!this.isReady()) {
|
|
186
|
-
this.#machine.send(REQUEST_COMPLETE);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
})
|
|
190
|
-
.start();
|
|
191
|
-
this.#machine.send(isNew ? CREATE : FIND);
|
|
119
|
+
});
|
|
120
|
+
// Instantiate the state machine
|
|
121
|
+
this.#machine = createActor(machine);
|
|
122
|
+
// Listen for state transitions
|
|
123
|
+
this.#machine.subscribe(state => {
|
|
124
|
+
const before = this.#prevDocState;
|
|
125
|
+
const after = state.context.doc;
|
|
126
|
+
this.#log(`→ ${state.value} %o`, after);
|
|
127
|
+
// if the document has changed, emit a change event
|
|
128
|
+
this.#checkForChanges(before, after);
|
|
129
|
+
});
|
|
130
|
+
// Start the machine, and send a create or find event to get things going
|
|
131
|
+
this.#machine.start();
|
|
132
|
+
this.#machine.send(isNew ? { type: CREATE } : { type: FIND });
|
|
192
133
|
}
|
|
193
134
|
// PRIVATE
|
|
194
135
|
/** Returns the current document, regardless of state */
|
|
@@ -201,108 +142,177 @@ export class DocHandle//
|
|
|
201
142
|
}
|
|
202
143
|
/** Returns a promise that resolves when the docHandle is in one of the given states */
|
|
203
144
|
#statePromise(awaitStates) {
|
|
204
|
-
const awaitStatesArray = Array.isArray(awaitStates)
|
|
205
|
-
|
|
145
|
+
const awaitStatesArray = Array.isArray(awaitStates)
|
|
146
|
+
? awaitStates
|
|
147
|
+
: [awaitStates];
|
|
148
|
+
return waitFor(this.#machine, s => awaitStatesArray.some(state => s.matches(state)),
|
|
206
149
|
// use a longer delay here so as not to race with other delays
|
|
207
150
|
{ timeout: this.#timeoutDelay * 2 });
|
|
208
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Called after state transitions. If the document has changed, emits a change event. If we just
|
|
154
|
+
* received the document for the first time, signal that our request has been completed.
|
|
155
|
+
*/
|
|
156
|
+
#checkForChanges(before, after) {
|
|
157
|
+
const docChanged = after && before && !headsAreSame(A.getHeads(after), A.getHeads(before));
|
|
158
|
+
if (docChanged) {
|
|
159
|
+
this.emit("heads-changed", { handle: this, doc: after });
|
|
160
|
+
const patches = A.diff(after, A.getHeads(before), A.getHeads(after));
|
|
161
|
+
if (patches.length > 0) {
|
|
162
|
+
this.emit("change", {
|
|
163
|
+
handle: this,
|
|
164
|
+
doc: after,
|
|
165
|
+
patches,
|
|
166
|
+
// TODO: pass along the source (load/change/network)
|
|
167
|
+
patchInfo: { before, after, source: "change" },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// If we didn't have the document yet, signal that we now do
|
|
171
|
+
if (!this.isReady())
|
|
172
|
+
this.#machine.send({ type: DOC_READY });
|
|
173
|
+
}
|
|
174
|
+
this.#prevDocState = after;
|
|
175
|
+
}
|
|
209
176
|
// PUBLIC
|
|
177
|
+
/** Our documentId in Automerge URL form.
|
|
178
|
+
*/
|
|
179
|
+
get url() {
|
|
180
|
+
return stringifyAutomergeUrl({ documentId: this.documentId });
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* @returns true if the document is ready for accessing or changes.
|
|
184
|
+
*
|
|
185
|
+
* Note that for documents already stored locally this occurs before synchronization with any
|
|
186
|
+
* peers. We do not currently have an equivalent `whenSynced()`.
|
|
187
|
+
*/
|
|
188
|
+
isReady = () => this.inState(["ready"]);
|
|
189
|
+
/**
|
|
190
|
+
* @returns true if the document has been marked as deleted.
|
|
191
|
+
*
|
|
192
|
+
* Deleted documents are removed from local storage and the sync process. It's not currently
|
|
193
|
+
* possible at runtime to undelete a document.
|
|
194
|
+
*/
|
|
195
|
+
isDeleted = () => this.inState(["deleted"]);
|
|
210
196
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
197
|
+
* @returns true if the document is currently unavailable.
|
|
198
|
+
*
|
|
199
|
+
* This will be the case if the document is not found in storage and no peers have shared it with us.
|
|
214
200
|
*/
|
|
215
|
-
|
|
201
|
+
isUnavailable = () => this.inState(["unavailable"]);
|
|
216
202
|
/**
|
|
217
|
-
*
|
|
218
|
-
* Deleted documents are removed from local storage and the sync process.
|
|
219
|
-
* It's not currently possible at runtime to undelete a document.
|
|
220
|
-
* @returns true if the document has been marked as deleted
|
|
203
|
+
* @returns true if the handle is in one of the given states.
|
|
221
204
|
*/
|
|
222
|
-
|
|
223
|
-
isUnavailable = () => this.inState([HandleState.UNAVAILABLE]);
|
|
224
|
-
inState = (states) => states.some(this.#machine?.getSnapshot().matches);
|
|
205
|
+
inState = (states) => states.some(s => this.#machine.getSnapshot().matches(s));
|
|
225
206
|
/** @hidden */
|
|
226
207
|
get state() {
|
|
227
|
-
return this.#machine
|
|
208
|
+
return this.#machine.getSnapshot().value;
|
|
228
209
|
}
|
|
229
210
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
211
|
+
* @returns a promise that resolves when the document is in one of the given states (if no states
|
|
212
|
+
* are passed, when the document is ready)
|
|
213
|
+
*
|
|
214
|
+
* Use this to block until the document handle has finished loading. The async equivalent to
|
|
215
|
+
* checking `inState()`.
|
|
234
216
|
*/
|
|
235
|
-
async whenReady(awaitStates = [
|
|
217
|
+
async whenReady(awaitStates = ["ready"]) {
|
|
236
218
|
await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay);
|
|
237
219
|
}
|
|
238
220
|
/**
|
|
239
|
-
*
|
|
240
|
-
* Note that this waits for the handle to be ready if necessary, and currently, if
|
|
241
|
-
* loading (or synchronization) fails, will never resolve.
|
|
221
|
+
* @returns the current state of this handle's Automerge document.
|
|
242
222
|
*
|
|
243
|
-
*
|
|
223
|
+
* This is the recommended way to access a handle's document. Note that this waits for the handle
|
|
224
|
+
* to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
|
|
244
225
|
*/
|
|
245
|
-
async doc(
|
|
226
|
+
async doc(
|
|
227
|
+
/** states to wait for, such as "LOADING". mostly for internal use. */
|
|
228
|
+
awaitStates = ["ready", "unavailable"]) {
|
|
246
229
|
try {
|
|
247
230
|
// wait for the document to enter one of the desired states
|
|
248
231
|
await this.#statePromise(awaitStates);
|
|
249
232
|
}
|
|
250
233
|
catch (error) {
|
|
251
|
-
// if we timed out
|
|
234
|
+
// if we timed out, return undefined
|
|
252
235
|
return undefined;
|
|
253
236
|
}
|
|
254
237
|
// Return the document
|
|
255
238
|
return !this.isUnavailable() ? this.#doc : undefined;
|
|
256
239
|
}
|
|
257
240
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
241
|
+
* Synchronously returns the current state of the Automerge document this handle manages, or
|
|
242
|
+
* undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
|
|
243
|
+
* if you want to make sure loading is complete first.
|
|
244
|
+
*
|
|
245
|
+
* Not to be confused with the SyncState of the document, which describes the state of the
|
|
246
|
+
* synchronization process.
|
|
261
247
|
*
|
|
262
|
-
*
|
|
248
|
+
* Note that `undefined` is not a valid Automerge document, so the return from this function is
|
|
249
|
+
* unambigous.
|
|
263
250
|
*
|
|
264
|
-
*
|
|
265
|
-
* @returns the current document, or undefined if the document is not ready
|
|
251
|
+
* @returns the current document, or undefined if the document is not ready.
|
|
266
252
|
*/
|
|
267
253
|
docSync() {
|
|
254
|
+
if (!this.isReady())
|
|
255
|
+
return undefined;
|
|
256
|
+
else
|
|
257
|
+
return this.#doc;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Returns the current "heads" of the document, akin to a git commit.
|
|
261
|
+
* This precisely defines the state of a document.
|
|
262
|
+
* @returns the current document's heads, or undefined if the document is not ready
|
|
263
|
+
*/
|
|
264
|
+
heads() {
|
|
268
265
|
if (!this.isReady()) {
|
|
269
266
|
return undefined;
|
|
270
267
|
}
|
|
271
|
-
return this.#doc;
|
|
268
|
+
return A.getHeads(this.#doc);
|
|
272
269
|
}
|
|
273
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* `update` is called by the repo when we receive changes from the network
|
|
272
|
+
* Called by the repo when we receive changes from the network.
|
|
274
273
|
* @hidden
|
|
275
|
-
|
|
274
|
+
*/
|
|
276
275
|
update(callback) {
|
|
277
|
-
this.#machine.send(UPDATE, {
|
|
278
|
-
payload: { callback },
|
|
279
|
-
});
|
|
276
|
+
this.#machine.send({ type: UPDATE, payload: { callback } });
|
|
280
277
|
}
|
|
281
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* Called by the repo either when a doc handle changes or we receive new remote heads.
|
|
282
280
|
* @hidden
|
|
283
281
|
*/
|
|
284
282
|
setRemoteHeads(storageId, heads) {
|
|
285
283
|
this.#remoteHeads[storageId] = heads;
|
|
286
284
|
this.emit("remote-heads", { storageId, heads });
|
|
287
285
|
}
|
|
288
|
-
/** Returns the heads of the storageId */
|
|
286
|
+
/** Returns the heads of the storageId. */
|
|
289
287
|
getRemoteHeads(storageId) {
|
|
290
288
|
return this.#remoteHeads[storageId];
|
|
291
289
|
}
|
|
292
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* All changes to an Automerge document should be made through this method.
|
|
292
|
+
* Inside the callback, the document should be treated as mutable: all edits will be recorded
|
|
293
|
+
* using a Proxy and translated into operations as part of a single recorded "change".
|
|
294
|
+
*
|
|
295
|
+
* Note that assignment via ES6 spread operators will result in *replacing* the object
|
|
296
|
+
* instead of mutating it which will prevent clean merges. This may be what you want, but
|
|
297
|
+
* `doc.foo = { ...doc.foo, bar: "baz" }` is not equivalent to `doc.foo.bar = "baz"`.
|
|
298
|
+
*
|
|
299
|
+
* Local changes will be stored (by the StorageSubsystem) and synchronized (by the
|
|
300
|
+
* DocSynchronizer) to any peers you are sharing it with.
|
|
301
|
+
*
|
|
302
|
+
* @param callback - A function that takes the current document and mutates it.
|
|
303
|
+
*
|
|
304
|
+
*/
|
|
293
305
|
change(callback, options = {}) {
|
|
294
306
|
if (!this.isReady()) {
|
|
295
307
|
throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
|
|
296
308
|
}
|
|
297
|
-
this.#machine.send(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return A.change(doc, options, callback);
|
|
301
|
-
},
|
|
302
|
-
},
|
|
309
|
+
this.#machine.send({
|
|
310
|
+
type: UPDATE,
|
|
311
|
+
payload: { callback: doc => A.change(doc, options, callback) },
|
|
303
312
|
});
|
|
304
313
|
}
|
|
305
|
-
/**
|
|
314
|
+
/**
|
|
315
|
+
* Makes a change as if the document were at `heads`.
|
|
306
316
|
*
|
|
307
317
|
* @returns A set of heads representing the concurrent change that was made.
|
|
308
318
|
*/
|
|
@@ -311,70 +321,75 @@ export class DocHandle//
|
|
|
311
321
|
throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
|
|
312
322
|
}
|
|
313
323
|
let resultHeads = undefined;
|
|
314
|
-
this.#machine.send(
|
|
324
|
+
this.#machine.send({
|
|
325
|
+
type: UPDATE,
|
|
315
326
|
payload: {
|
|
316
|
-
callback:
|
|
327
|
+
callback: doc => {
|
|
317
328
|
const result = A.changeAt(doc, heads, options, callback);
|
|
318
329
|
resultHeads = result.newHeads || undefined;
|
|
319
330
|
return result.newDoc;
|
|
320
331
|
},
|
|
321
332
|
},
|
|
322
333
|
});
|
|
334
|
+
// the callback above will always run before we get here, so this should always contain the new heads
|
|
323
335
|
return resultHeads;
|
|
324
336
|
}
|
|
325
|
-
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
337
|
+
/**
|
|
338
|
+
* Merges another document into this document. Any peers we are sharing changes with will be
|
|
339
|
+
* notified of the changes resulting from the merge.
|
|
328
340
|
*
|
|
329
|
-
* @
|
|
330
|
-
* This is a convenience method for
|
|
331
|
-
* `handle.change(doc => A.merge(doc, otherHandle.docSync()))`. Any peers
|
|
332
|
-
* whom we are sharing changes with will be notified of the changes resulting
|
|
333
|
-
* from the merge.
|
|
341
|
+
* @returns the merged document.
|
|
334
342
|
*
|
|
335
|
-
* @throws if either document is not ready or if `otherHandle` is unavailable
|
|
343
|
+
* @throws if either document is not ready or if `otherHandle` is unavailable.
|
|
336
344
|
*/
|
|
337
|
-
merge(
|
|
345
|
+
merge(
|
|
346
|
+
/** the handle of the document to merge into this one */
|
|
347
|
+
otherHandle) {
|
|
338
348
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
339
349
|
throw new Error("Both handles must be ready to merge");
|
|
340
350
|
}
|
|
341
351
|
const mergingDoc = otherHandle.docSync();
|
|
342
352
|
if (!mergingDoc) {
|
|
343
|
-
throw new Error("The document to be merged in is
|
|
353
|
+
throw new Error("The document to be merged in is falsy, aborting.");
|
|
344
354
|
}
|
|
345
355
|
this.update(doc => {
|
|
346
356
|
return A.merge(doc, mergingDoc);
|
|
347
357
|
});
|
|
348
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Used in testing to mark this document as unavailable.
|
|
361
|
+
* @hidden
|
|
362
|
+
*/
|
|
349
363
|
unavailable() {
|
|
350
|
-
this.#machine.send(
|
|
364
|
+
this.#machine.send({ type: DOC_UNAVAILABLE });
|
|
351
365
|
}
|
|
352
|
-
/**
|
|
366
|
+
/** Called by the repo when the document is not found in storage.
|
|
353
367
|
* @hidden
|
|
354
368
|
* */
|
|
355
369
|
request() {
|
|
356
|
-
if (this.#state ===
|
|
357
|
-
this.#machine.send(REQUEST);
|
|
370
|
+
if (this.#state === "loading")
|
|
371
|
+
this.#machine.send({ type: REQUEST });
|
|
358
372
|
}
|
|
359
373
|
/** @hidden */
|
|
360
374
|
awaitNetwork() {
|
|
361
|
-
if (this.#state ===
|
|
362
|
-
this.#machine.send(AWAIT_NETWORK);
|
|
375
|
+
if (this.#state === "loading")
|
|
376
|
+
this.#machine.send({ type: AWAIT_NETWORK });
|
|
363
377
|
}
|
|
364
378
|
/** @hidden */
|
|
365
379
|
networkReady() {
|
|
366
|
-
if (this.#state ===
|
|
367
|
-
this.#machine.send(NETWORK_READY);
|
|
380
|
+
if (this.#state === "awaitingNetwork")
|
|
381
|
+
this.#machine.send({ type: NETWORK_READY });
|
|
368
382
|
}
|
|
369
|
-
/**
|
|
383
|
+
/** Called by the repo when the document is deleted. */
|
|
370
384
|
delete() {
|
|
371
|
-
this.#machine.send(DELETE);
|
|
385
|
+
this.#machine.send({ type: DELETE });
|
|
372
386
|
}
|
|
373
|
-
/**
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
387
|
+
/**
|
|
388
|
+
* Sends an arbitrary ephemeral message out to all reachable peers who would receive sync messages
|
|
389
|
+
* from you. It has no guarantee of delivery, and is not persisted to the underlying automerge doc
|
|
390
|
+
* in any way. Messages will have a sending PeerId but this is *not* a useful user identifier (a
|
|
391
|
+
* user could have multiple tabs open and would appear as multiple PeerIds). Every message source
|
|
392
|
+
* must have a unique PeerId.
|
|
378
393
|
*/
|
|
379
394
|
broadcast(message) {
|
|
380
395
|
this.emit("ephemeral-message-outbound", {
|
|
@@ -383,12 +398,10 @@ export class DocHandle//
|
|
|
383
398
|
});
|
|
384
399
|
}
|
|
385
400
|
}
|
|
386
|
-
// STATE MACHINE TYPES
|
|
401
|
+
// STATE MACHINE TYPES & CONSTANTS
|
|
387
402
|
// state
|
|
388
403
|
/**
|
|
389
|
-
*
|
|
390
|
-
* @enum
|
|
391
|
-
*
|
|
404
|
+
* Possible internal states for a DocHandle
|
|
392
405
|
*/
|
|
393
406
|
export const HandleState = {
|
|
394
407
|
/** The handle has been created but not yet loaded or requested */
|
|
@@ -406,19 +419,14 @@ export const HandleState = {
|
|
|
406
419
|
/** The document was not available in storage or from any connected peers */
|
|
407
420
|
UNAVAILABLE: "unavailable",
|
|
408
421
|
};
|
|
409
|
-
// events
|
|
410
|
-
export const Event = {
|
|
411
|
-
CREATE: "CREATE",
|
|
412
|
-
FIND: "FIND",
|
|
413
|
-
REQUEST: "REQUEST",
|
|
414
|
-
REQUEST_COMPLETE: "REQUEST_COMPLETE",
|
|
415
|
-
AWAIT_NETWORK: "AWAIT_NETWORK",
|
|
416
|
-
NETWORK_READY: "NETWORK_READY",
|
|
417
|
-
UPDATE: "UPDATE",
|
|
418
|
-
TIMEOUT: "TIMEOUT",
|
|
419
|
-
DELETE: "DELETE",
|
|
420
|
-
MARK_UNAVAILABLE: "MARK_UNAVAILABLE",
|
|
421
|
-
};
|
|
422
|
-
// CONSTANTS
|
|
423
422
|
export const { IDLE, LOADING, AWAITING_NETWORK, REQUESTING, READY, DELETED, UNAVAILABLE, } = HandleState;
|
|
424
|
-
const
|
|
423
|
+
const CREATE = "CREATE";
|
|
424
|
+
const FIND = "FIND";
|
|
425
|
+
const REQUEST = "REQUEST";
|
|
426
|
+
const DOC_READY = "DOC_READY";
|
|
427
|
+
const AWAIT_NETWORK = "AWAIT_NETWORK";
|
|
428
|
+
const NETWORK_READY = "NETWORK_READY";
|
|
429
|
+
const UPDATE = "UPDATE";
|
|
430
|
+
const DELETE = "DELETE";
|
|
431
|
+
const TIMEOUT = "TIMEOUT";
|
|
432
|
+
const DOC_UNAVAILABLE = "DOC_UNAVAILABLE";
|