@automerge/automerge-repo 1.1.5 → 1.1.9

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/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, createMachine, interpret, } from "xstate";
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
- /** DocHandle is a wrapper around a single Automerge document that lets us
11
- * listen for changes and notify the network and storage of new changes.
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
- * change has occured and the `Repo` will save any new changes to the
20
- * attached {@link StorageAdapter} and send sync messages to connected peers.
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
- * Internally we use a state machine to orchestrate document loading and/or syncing, in order to
60
- * avoid requesting data we already have, or surfacing intermediate values to the consumer.
61
- *
62
- * ┌─────────────────────┬─────────TIMEOUT────►┌─────────────┐
63
- * ┌───┴─────┐ ┌───┴────────┐ │ unavailable │
64
- * ┌───────┐ ┌──FIND──┤ loading ├─REQUEST──►│ requesting ├─UPDATE──┐ └─────────────┘
65
- * │ idle ├──┤ └───┬─────┘ └────────────┘ │
66
- * └───────┘ │ │ └─►┌────────┐
67
- * │ └───────LOAD───────────────────────────────►│ ready │
68
- * └──CREATE───────────────────────────────────────────────►└────────┘
69
- */
70
- this.#machine = interpret(createMachine({
71
- predictableActionArguments: true,
72
- id: "docHandle",
73
- initial: IDLE,
74
- context: { documentId: this.documentId, doc },
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
- // If we're creating a new document, we don't need to load anything
79
- CREATE: { target: READY },
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
- // UPDATE is called by the Repo if the document is found in storage
89
- UPDATE: { actions: "onUpdate", target: READY },
90
- // REQUEST is called by the Repo if the document is not found in storage
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
- MARK_UNAVAILABLE: {
111
- target: UNAVAILABLE,
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
- on: {
139
- UPDATE: { actions: "onUpdate" },
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
- actions: {
148
- /** Put the updated doc on context */
149
- onUpdate: assign((context, { payload }) => {
150
- const { doc: oldDoc } = context;
151
- const { callback } = payload;
152
- const newDoc = callback(oldDoc);
153
- return { doc: newDoc };
154
- }),
155
- onDelete: assign(() => {
156
- this.emit("delete", { handle: this });
157
- return { doc: undefined };
158
- }),
159
- onUnavailable: assign(context => {
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 */
@@ -208,103 +149,170 @@ export class DocHandle//
208
149
  // use a longer delay here so as not to race with other delays
209
150
  { timeout: this.#timeoutDelay * 2 });
210
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
+ }
211
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"]);
212
196
  /**
213
- * Checks if the document is ready for accessing or changes.
214
- * Note that for documents already stored locally this occurs before synchronization
215
- * with any peers. We do not currently have an equivalent `whenSynced()`.
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.
216
200
  */
217
- isReady = () => this.inState([HandleState.READY]);
201
+ isUnavailable = () => this.inState(["unavailable"]);
218
202
  /**
219
- * Checks if this document has been marked as deleted.
220
- * Deleted documents are removed from local storage and the sync process.
221
- * It's not currently possible at runtime to undelete a document.
222
- * @returns true if the document has been marked as deleted
203
+ * @returns true if the handle is in one of the given states.
223
204
  */
224
- isDeleted = () => this.inState([HandleState.DELETED]);
225
- isUnavailable = () => this.inState([HandleState.UNAVAILABLE]);
226
- inState = (states) => states.some(this.#machine?.getSnapshot().matches);
205
+ inState = (states) => states.some(s => this.#machine.getSnapshot().matches(s));
227
206
  /** @hidden */
228
207
  get state() {
229
- return this.#machine?.getSnapshot().value;
208
+ return this.#machine.getSnapshot().value;
230
209
  }
231
210
  /**
232
- * Use this to block until the document handle has finished loading.
233
- * The async equivalent to checking `inState()`.
234
- * @param awaitStates = [READY]
235
- * @returns
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()`.
236
216
  */
237
- async whenReady(awaitStates = [READY]) {
217
+ async whenReady(awaitStates = ["ready"]) {
238
218
  await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay);
239
219
  }
240
220
  /**
241
- * Returns the current state of the Automerge document this handle manages.
242
- * Note that this waits for the handle to be ready if necessary, and currently, if
243
- * loading (or synchronization) fails, will never resolve.
221
+ * @returns the current state of this handle's Automerge document.
244
222
  *
245
- * @param {awaitStates=[READY]} optional states to wait for, such as "LOADING". mostly for internal use.
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.
246
225
  */
247
- async doc(awaitStates = [READY, UNAVAILABLE]) {
226
+ async doc(
227
+ /** states to wait for, such as "LOADING". mostly for internal use. */
228
+ awaitStates = ["ready", "unavailable"]) {
248
229
  try {
249
230
  // wait for the document to enter one of the desired states
250
231
  await this.#statePromise(awaitStates);
251
232
  }
252
233
  catch (error) {
253
- // if we timed out (or have determined the document is currently unavailable), return undefined
234
+ // if we timed out, return undefined
254
235
  return undefined;
255
236
  }
256
237
  // Return the document
257
238
  return !this.isUnavailable() ? this.#doc : undefined;
258
239
  }
259
240
  /**
260
- * Returns the current state of the Automerge document this handle manages, or undefined.
261
- * Useful in a synchronous context. Consider using `await handle.doc()` instead, check `isReady()`,
262
- * or use `whenReady()` if you want to make sure loading is complete first.
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.
263
247
  *
264
- * Do not confuse this with the SyncState of the document, which describes the state of the synchronization process.
248
+ * Note that `undefined` is not a valid Automerge document, so the return from this function is
249
+ * unambigous.
265
250
  *
266
- * Note that `undefined` is not a valid Automerge document so the return from this function is unambigous.
267
- * @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.
268
252
  */
269
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() {
270
265
  if (!this.isReady()) {
271
266
  return undefined;
272
267
  }
273
- return this.#doc;
268
+ return A.getHeads(this.#doc);
274
269
  }
275
- /** `update` is called by the repo when we receive changes from the network
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.
276
273
  * @hidden
277
- * */
274
+ */
278
275
  update(callback) {
279
- this.#machine.send(UPDATE, {
280
- payload: { callback },
281
- });
276
+ this.#machine.send({ type: UPDATE, payload: { callback } });
282
277
  }
283
- /** `setRemoteHeads` is called by the repo either when a doc handle changes or we receive new remote heads
278
+ /**
279
+ * Called by the repo either when a doc handle changes or we receive new remote heads.
284
280
  * @hidden
285
281
  */
286
282
  setRemoteHeads(storageId, heads) {
287
283
  this.#remoteHeads[storageId] = heads;
288
284
  this.emit("remote-heads", { storageId, heads });
289
285
  }
290
- /** Returns the heads of the storageId */
286
+ /** Returns the heads of the storageId. */
291
287
  getRemoteHeads(storageId) {
292
288
  return this.#remoteHeads[storageId];
293
289
  }
294
- /** `change` is called by the repo when the document is changed locally */
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
+ */
295
305
  change(callback, options = {}) {
296
306
  if (!this.isReady()) {
297
307
  throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
298
308
  }
299
- this.#machine.send(UPDATE, {
300
- payload: {
301
- callback: (doc) => {
302
- return A.change(doc, options, callback);
303
- },
304
- },
309
+ this.#machine.send({
310
+ type: UPDATE,
311
+ payload: { callback: doc => A.change(doc, options, callback) },
305
312
  });
306
313
  }
307
- /** Make a change as if the document were at `heads`
314
+ /**
315
+ * Makes a change as if the document were at `heads`.
308
316
  *
309
317
  * @returns A set of heads representing the concurrent change that was made.
310
318
  */
@@ -313,70 +321,75 @@ export class DocHandle//
313
321
  throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
314
322
  }
315
323
  let resultHeads = undefined;
316
- this.#machine.send(UPDATE, {
324
+ this.#machine.send({
325
+ type: UPDATE,
317
326
  payload: {
318
- callback: (doc) => {
327
+ callback: doc => {
319
328
  const result = A.changeAt(doc, heads, options, callback);
320
329
  resultHeads = result.newHeads || undefined;
321
330
  return result.newDoc;
322
331
  },
323
332
  },
324
333
  });
334
+ // the callback above will always run before we get here, so this should always contain the new heads
325
335
  return resultHeads;
326
336
  }
327
- /** Merge another document into this document
328
- *
329
- * @param otherHandle - the handle of the document to merge into this one
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.
330
340
  *
331
- * @remarks
332
- * This is a convenience method for
333
- * `handle.change(doc => A.merge(doc, otherHandle.docSync()))`. Any peers
334
- * whom we are sharing changes with will be notified of the changes resulting
335
- * from the merge.
341
+ * @returns the merged document.
336
342
  *
337
- * @throws if either document is not ready or if `otherHandle` is unavailable (`otherHandle.docSync() === undefined`)
343
+ * @throws if either document is not ready or if `otherHandle` is unavailable.
338
344
  */
339
- merge(otherHandle) {
345
+ merge(
346
+ /** the handle of the document to merge into this one */
347
+ otherHandle) {
340
348
  if (!this.isReady() || !otherHandle.isReady()) {
341
349
  throw new Error("Both handles must be ready to merge");
342
350
  }
343
351
  const mergingDoc = otherHandle.docSync();
344
352
  if (!mergingDoc) {
345
- throw new Error("The document to be merged in is null, aborting.");
353
+ throw new Error("The document to be merged in is falsy, aborting.");
346
354
  }
347
355
  this.update(doc => {
348
356
  return A.merge(doc, mergingDoc);
349
357
  });
350
358
  }
359
+ /**
360
+ * Used in testing to mark this document as unavailable.
361
+ * @hidden
362
+ */
351
363
  unavailable() {
352
- this.#machine.send(MARK_UNAVAILABLE);
364
+ this.#machine.send({ type: DOC_UNAVAILABLE });
353
365
  }
354
- /** `request` is called by the repo when the document is not found in storage
366
+ /** Called by the repo when the document is not found in storage.
355
367
  * @hidden
356
368
  * */
357
369
  request() {
358
- if (this.#state === LOADING)
359
- this.#machine.send(REQUEST);
370
+ if (this.#state === "loading")
371
+ this.#machine.send({ type: REQUEST });
360
372
  }
361
373
  /** @hidden */
362
374
  awaitNetwork() {
363
- if (this.#state === LOADING)
364
- this.#machine.send(AWAIT_NETWORK);
375
+ if (this.#state === "loading")
376
+ this.#machine.send({ type: AWAIT_NETWORK });
365
377
  }
366
378
  /** @hidden */
367
379
  networkReady() {
368
- if (this.#state === AWAITING_NETWORK)
369
- this.#machine.send(NETWORK_READY);
380
+ if (this.#state === "awaitingNetwork")
381
+ this.#machine.send({ type: NETWORK_READY });
370
382
  }
371
- /** `delete` is called by the repo when the document is deleted */
383
+ /** Called by the repo when the document is deleted. */
372
384
  delete() {
373
- this.#machine.send(DELETE);
385
+ this.#machine.send({ type: DELETE });
374
386
  }
375
- /** `broadcast` sends an arbitrary ephemeral message out to all reachable peers who would receive sync messages from you
376
- * it has no guarantee of delivery, and is not persisted to the underlying automerge doc in any way.
377
- * messages will have a sending PeerId but this is *not* a useful user identifier.
378
- * a user could have multiple tabs open and would appear as multiple PeerIds.
379
- * every message source must have a unique PeerId.
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.
380
393
  */
381
394
  broadcast(message) {
382
395
  this.emit("ephemeral-message-outbound", {
@@ -385,12 +398,10 @@ export class DocHandle//
385
398
  });
386
399
  }
387
400
  }
388
- // STATE MACHINE TYPES
401
+ // STATE MACHINE TYPES & CONSTANTS
389
402
  // state
390
403
  /**
391
- * The state of a document handle
392
- * @enum
393
- *
404
+ * Possible internal states for a DocHandle
394
405
  */
395
406
  export const HandleState = {
396
407
  /** The handle has been created but not yet loaded or requested */
@@ -408,19 +419,14 @@ export const HandleState = {
408
419
  /** The document was not available in storage or from any connected peers */
409
420
  UNAVAILABLE: "unavailable",
410
421
  };
411
- // events
412
- export const Event = {
413
- CREATE: "CREATE",
414
- FIND: "FIND",
415
- REQUEST: "REQUEST",
416
- REQUEST_COMPLETE: "REQUEST_COMPLETE",
417
- AWAIT_NETWORK: "AWAIT_NETWORK",
418
- NETWORK_READY: "NETWORK_READY",
419
- UPDATE: "UPDATE",
420
- TIMEOUT: "TIMEOUT",
421
- DELETE: "DELETE",
422
- MARK_UNAVAILABLE: "MARK_UNAVAILABLE",
423
- };
424
- // CONSTANTS
425
422
  export const { IDLE, LOADING, AWAITING_NETWORK, REQUESTING, READY, DELETED, UNAVAILABLE, } = HandleState;
426
- const { CREATE, FIND, REQUEST, UPDATE, TIMEOUT, DELETE, REQUEST_COMPLETE, MARK_UNAVAILABLE, AWAIT_NETWORK, NETWORK_READY, } = Event;
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";