@automerge/automerge-repo 2.0.0-alpha.7 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +8 -8
  2. package/dist/AutomergeUrl.d.ts +17 -5
  3. package/dist/AutomergeUrl.d.ts.map +1 -1
  4. package/dist/AutomergeUrl.js +71 -24
  5. package/dist/DocHandle.d.ts +68 -45
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +166 -69
  8. package/dist/FindProgress.d.ts +30 -0
  9. package/dist/FindProgress.d.ts.map +1 -0
  10. package/dist/FindProgress.js +1 -0
  11. package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
  12. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
  13. package/dist/RemoteHeadsSubscriptions.js +4 -1
  14. package/dist/Repo.d.ts +46 -6
  15. package/dist/Repo.d.ts.map +1 -1
  16. package/dist/Repo.js +252 -67
  17. package/dist/helpers/abortable.d.ts +36 -0
  18. package/dist/helpers/abortable.d.ts.map +1 -0
  19. package/dist/helpers/abortable.js +47 -0
  20. package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
  21. package/dist/helpers/bufferFromHex.d.ts +3 -0
  22. package/dist/helpers/bufferFromHex.d.ts.map +1 -0
  23. package/dist/helpers/bufferFromHex.js +13 -0
  24. package/dist/helpers/debounce.d.ts.map +1 -1
  25. package/dist/helpers/eventPromise.d.ts.map +1 -1
  26. package/dist/helpers/headsAreSame.d.ts +2 -2
  27. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  28. package/dist/helpers/mergeArrays.d.ts +1 -1
  29. package/dist/helpers/mergeArrays.d.ts.map +1 -1
  30. package/dist/helpers/pause.d.ts.map +1 -1
  31. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  32. package/dist/helpers/tests/network-adapter-tests.js +13 -13
  33. package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
  34. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  35. package/dist/helpers/tests/storage-adapter-tests.js +25 -48
  36. package/dist/helpers/throttle.d.ts.map +1 -1
  37. package/dist/helpers/withTimeout.d.ts.map +1 -1
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/network/messages.d.ts.map +1 -1
  42. package/dist/storage/StorageSubsystem.d.ts +15 -1
  43. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  44. package/dist/storage/StorageSubsystem.js +50 -14
  45. package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -3
  46. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  47. package/dist/synchronizer/CollectionSynchronizer.js +34 -15
  48. package/dist/synchronizer/DocSynchronizer.d.ts +3 -2
  49. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  50. package/dist/synchronizer/DocSynchronizer.js +51 -27
  51. package/dist/synchronizer/Synchronizer.d.ts +11 -0
  52. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  53. package/dist/types.d.ts +4 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/fuzz/fuzz.ts +3 -3
  56. package/package.json +3 -4
  57. package/src/AutomergeUrl.ts +101 -26
  58. package/src/DocHandle.ts +235 -82
  59. package/src/FindProgress.ts +48 -0
  60. package/src/RemoteHeadsSubscriptions.ts +11 -9
  61. package/src/Repo.ts +368 -74
  62. package/src/helpers/abortable.ts +62 -0
  63. package/src/helpers/bufferFromHex.ts +14 -0
  64. package/src/helpers/headsAreSame.ts +2 -2
  65. package/src/helpers/tests/network-adapter-tests.ts +14 -13
  66. package/src/helpers/tests/storage-adapter-tests.ts +44 -86
  67. package/src/index.ts +7 -0
  68. package/src/storage/StorageSubsystem.ts +66 -16
  69. package/src/synchronizer/CollectionSynchronizer.ts +37 -16
  70. package/src/synchronizer/DocSynchronizer.ts +59 -32
  71. package/src/synchronizer/Synchronizer.ts +14 -0
  72. package/src/types.ts +4 -1
  73. package/test/AutomergeUrl.test.ts +130 -0
  74. package/test/CollectionSynchronizer.test.ts +4 -4
  75. package/test/DocHandle.test.ts +181 -38
  76. package/test/DocSynchronizer.test.ts +10 -3
  77. package/test/Repo.test.ts +376 -203
  78. package/test/StorageSubsystem.test.ts +80 -1
  79. package/test/remoteHeads.test.ts +27 -12
package/src/DocHandle.ts CHANGED
@@ -2,11 +2,15 @@ import * as A from "@automerge/automerge/slim/next"
2
2
  import debug from "debug"
3
3
  import { EventEmitter } from "eventemitter3"
4
4
  import { assertEvent, assign, createActor, setup, waitFor } from "xstate"
5
- import { stringifyAutomergeUrl } from "./AutomergeUrl.js"
5
+ import {
6
+ decodeHeads,
7
+ encodeHeads,
8
+ stringifyAutomergeUrl,
9
+ } from "./AutomergeUrl.js"
6
10
  import { encode } from "./helpers/cbor.js"
7
11
  import { headsAreSame } from "./helpers/headsAreSame.js"
8
12
  import { withTimeout } from "./helpers/withTimeout.js"
9
- import type { AutomergeUrl, DocumentId, PeerId } from "./types.js"
13
+ import type { AutomergeUrl, DocumentId, PeerId, UrlHeads } from "./types.js"
10
14
  import { StorageId } from "./storage/types.js"
11
15
 
12
16
  /**
@@ -28,6 +32,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
28
32
  /** The XState actor running our state machine. */
29
33
  #machine
30
34
 
35
+ /** If set, this handle will only show the document at these heads */
36
+ #fixedHeads?: UrlHeads
37
+
31
38
  /** The last known state of our document. */
32
39
  #prevDocState: T = A.init<T>()
33
40
 
@@ -36,7 +43,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
36
43
  #timeoutDelay = 60_000
37
44
 
38
45
  /** A dictionary mapping each peer to the last heads we know they have. */
39
- #remoteHeads: Record<StorageId, A.Heads> = {}
46
+ #remoteHeads: Record<StorageId, UrlHeads> = {}
47
+
48
+ /** Cache for view handles, keyed by the stringified heads */
49
+ #viewCache: Map<string, DocHandle<T>> = new Map()
40
50
 
41
51
  /** @hidden */
42
52
  constructor(
@@ -49,6 +59,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
49
59
  this.#timeoutDelay = options.timeoutDelay
50
60
  }
51
61
 
62
+ if ("heads" in options) {
63
+ this.#fixedHeads = options.heads
64
+ }
65
+
52
66
  const doc = A.init<T>()
53
67
 
54
68
  this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
@@ -72,9 +86,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
72
86
  this.emit("delete", { handle: this })
73
87
  return { doc: A.init() }
74
88
  }),
75
- onUnavailable: () => {
76
- this.emit("unavailable", { handle: this })
77
- },
89
+ onUnavailable: assign(() => {
90
+ return { doc: A.init() }
91
+ }),
92
+ onUnload: assign(() => {
93
+ return { doc: A.init() }
94
+ }),
78
95
  },
79
96
  }).createMachine({
80
97
  /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
@@ -86,6 +103,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
86
103
  context: { documentId, doc },
87
104
  on: {
88
105
  UPDATE: { actions: "onUpdate" },
106
+ UNLOAD: ".unloaded",
89
107
  DELETE: ".deleted",
90
108
  },
91
109
  states: {
@@ -113,6 +131,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
113
131
  on: { DOC_READY: "ready" },
114
132
  },
115
133
  ready: {},
134
+ unloaded: {
135
+ entry: "onUnload",
136
+ on: {
137
+ RELOAD: "loading",
138
+ },
139
+ },
116
140
  deleted: { entry: "onDelete", type: "final" },
117
141
  },
118
142
  })
@@ -131,7 +155,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
131
155
 
132
156
  // Start the machine, and send a create or find event to get things going
133
157
  this.#machine.start()
134
- this.#machine.send({ type: BEGIN })
158
+ this.begin()
135
159
  }
136
160
 
137
161
  // PRIVATE
@@ -166,7 +190,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
166
190
  #checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
167
191
  const beforeHeads = A.getHeads(before)
168
192
  const afterHeads = A.getHeads(after)
169
- const docChanged = !headsAreSame(afterHeads, beforeHeads)
193
+ const docChanged = !headsAreSame(
194
+ encodeHeads(afterHeads),
195
+ encodeHeads(beforeHeads)
196
+ )
170
197
  if (docChanged) {
171
198
  this.emit("heads-changed", { handle: this, doc: after })
172
199
 
@@ -192,7 +219,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
192
219
  /** Our documentId in Automerge URL form.
193
220
  */
194
221
  get url(): AutomergeUrl {
195
- return stringifyAutomergeUrl({ documentId: this.documentId })
222
+ return stringifyAutomergeUrl({
223
+ documentId: this.documentId,
224
+ heads: this.#fixedHeads,
225
+ })
196
226
  }
197
227
 
198
228
  /**
@@ -203,6 +233,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
203
233
  */
204
234
  isReady = () => this.inState(["ready"])
205
235
 
236
+ /**
237
+ * @returns true if the document has been unloaded.
238
+ *
239
+ * Unloaded documents are freed from memory but not removed from local storage. It's not currently
240
+ * possible at runtime to reload an unloaded document.
241
+ */
242
+ isUnloaded = () => this.inState(["unloaded"])
243
+
206
244
  /**
207
245
  * @returns true if the document has been marked as deleted.
208
246
  *
@@ -241,42 +279,28 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
241
279
  }
242
280
 
243
281
  /**
244
- * @returns the current state of this handle's Automerge document.
282
+ * Returns the current state of the Automerge document this handle manages.
283
+ *
284
+ * @returns the current document
285
+ * @throws on deleted and unavailable documents
245
286
  *
246
- * This is the recommended way to access a handle's document. Note that this waits for the handle
247
- * to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
248
287
  */
249
- async doc(
250
- /** states to wait for, such as "LOADING". mostly for internal use. */
251
- awaitStates: HandleState[] = ["ready", "unavailable"]
252
- ) {
253
- try {
254
- // wait for the document to enter one of the desired states
255
- await this.#statePromise(awaitStates)
256
- } catch (error) {
257
- // if we timed out, return undefined
258
- return undefined
288
+ doc() {
289
+ if (!this.isReady()) throw new Error("DocHandle is not ready")
290
+ if (this.#fixedHeads) {
291
+ return A.view(this.#doc, decodeHeads(this.#fixedHeads))
259
292
  }
260
- // Return the document
261
- return !this.isUnavailable() ? this.#doc : undefined
293
+ return this.#doc
262
294
  }
263
295
 
264
296
  /**
265
- * Synchronously returns the current state of the Automerge document this handle manages, or
266
- * undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
267
- * if you want to make sure loading is complete first.
268
- *
269
- * Not to be confused with the SyncState of the document, which describes the state of the
270
- * synchronization process.
271
297
  *
272
- * Note that `undefined` is not a valid Automerge document, so the return from this function is
273
- * unambigous.
274
- *
275
- * @returns the current document, or undefined if the document is not ready.
276
- */
298
+ * @deprecated */
277
299
  docSync() {
278
- if (!this.isReady()) return undefined
279
- else return this.#doc
300
+ console.warn(
301
+ "docSync is deprecated. Use doc() instead. This function will be removed as part of the 2.0 release."
302
+ )
303
+ return this.doc()
280
304
  }
281
305
 
282
306
  /**
@@ -284,17 +308,20 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
284
308
  * This precisely defines the state of a document.
285
309
  * @returns the current document's heads, or undefined if the document is not ready
286
310
  */
287
- heads(): A.Heads | undefined {
288
- if (!this.isReady()) {
289
- return undefined
311
+ heads(): UrlHeads {
312
+ if (!this.isReady()) throw new Error("DocHandle is not ready")
313
+ if (this.#fixedHeads) {
314
+ return this.#fixedHeads
290
315
  }
291
- return A.getHeads(this.#doc)
316
+ return encodeHeads(A.getHeads(this.#doc))
317
+ }
318
+
319
+ begin() {
320
+ this.#machine.send({ type: BEGIN })
292
321
  }
293
322
 
294
323
  /**
295
- * Creates a fixed "view" of an automerge document at the given point in time represented
296
- * by the `heads` passed in. The return value is the same type as docSync() and will return
297
- * undefined if the object hasn't finished loading.
324
+ * Returns an array of all past "heads" for the document in topological order.
298
325
  *
299
326
  * @remarks
300
327
  * A point-in-time in an automerge document is an *array* of heads since there may be
@@ -303,49 +330,133 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
303
330
  * history views would be quite large under concurrency (every thing in each branch against each other).
304
331
  * There might be a clever way to think about this, but we haven't found it yet, so for now at least
305
332
  * we present a single traversable view which excludes concurrency.
306
- * @returns The individual heads for every change in the document.
333
+ * @returns UrlHeads[] - The individual heads for every change in the document. Each item is a tagged string[1].
307
334
  */
308
- history(): A.Heads[] | undefined {
335
+ history(): UrlHeads[] | undefined {
309
336
  if (!this.isReady()) {
310
337
  return undefined
311
338
  }
312
339
  // This just returns all the heads as individual strings.
313
340
 
314
- return A.topoHistoryTraversal(this.#doc).map(h => [h]) as A.Heads[]
341
+ return A.topoHistoryTraversal(this.#doc).map(h =>
342
+ encodeHeads([h])
343
+ ) as UrlHeads[]
315
344
  }
316
345
 
317
346
  /**
318
347
  * Creates a fixed "view" of an automerge document at the given point in time represented
319
- * by the `heads` passed in. The return value is the same type as docSync() and will return
348
+ * by the `heads` passed in. The return value is the same type as doc() and will return
320
349
  * undefined if the object hasn't finished loading.
321
- * @returns
350
+ *
351
+ * @remarks
352
+ * Note that our Typescript types do not consider change over time and the current version
353
+ * of Automerge doesn't check types at runtime, so if you go back to an old set of heads
354
+ * that doesn't match the heads here, Typescript will not save you.
355
+ *
356
+ * @argument heads - The heads to view the document at. See history().
357
+ * @returns DocHandle<T> at the time of `heads`
322
358
  */
323
- view(heads: A.Heads): A.Doc<T> | undefined {
359
+ view(heads: UrlHeads): DocHandle<T> {
324
360
  if (!this.isReady()) {
325
- return undefined
361
+ throw new Error(
362
+ `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
363
+ )
326
364
  }
327
- return A.view(this.#doc, heads)
365
+
366
+ // Create a cache key from the heads
367
+ const cacheKey = JSON.stringify(heads)
368
+
369
+ // Check if we have a cached handle for these heads
370
+ const cachedHandle = this.#viewCache.get(cacheKey)
371
+ if (cachedHandle) {
372
+ return cachedHandle
373
+ }
374
+
375
+ // Create a new handle with the same documentId but fixed heads
376
+ const handle = new DocHandle<T>(this.documentId, {
377
+ heads,
378
+ timeoutDelay: this.#timeoutDelay,
379
+ })
380
+ handle.update(() => A.clone(this.#doc))
381
+ handle.doneLoading()
382
+
383
+ // Store in cache
384
+ this.#viewCache.set(cacheKey, handle)
385
+
386
+ return handle
328
387
  }
329
388
 
330
389
  /**
331
- * Creates a fixed "view" of an automerge document at the given point in time represented
332
- * by the `heads` passed in. The return value is the same type as docSync() and will return
333
- * undefined if the object hasn't finished loading.
390
+ * Returns a set of Patch operations that will move a materialized document from one state to another
391
+ * if applied.
334
392
  *
335
393
  * @remarks
336
- * We allow specifying both a from/to heads or just a single comparison point, in which case
337
- * the base will be the current document heads.
394
+ * We allow specifying either:
395
+ * - Two sets of heads to compare directly
396
+ * - A single set of heads to compare against our current heads
397
+ * - Another DocHandle to compare against (which must share history with this document)
338
398
  *
339
- * @returns Automerge patches that go from one document state to the other. Use view() to get the full state.
399
+ * @throws Error if the documents don't share history or if either document is not ready
400
+ * @returns Automerge patches that go from one document state to the other
340
401
  */
341
- diff(first: A.Heads, second?: A.Heads): A.Patch[] | undefined {
402
+ diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
342
403
  if (!this.isReady()) {
343
- return undefined
404
+ throw new Error(
405
+ `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
406
+ )
407
+ }
408
+
409
+ const doc = this.#doc
410
+ if (!doc) throw new Error("Document not available")
411
+
412
+ // If first argument is a DocHandle
413
+ if (first instanceof DocHandle) {
414
+ if (!first.isReady()) {
415
+ throw new Error("Cannot diff against a handle that isn't ready")
416
+ }
417
+ const otherHeads = first.heads()
418
+ if (!otherHeads) throw new Error("Other document's heads not available")
419
+
420
+ // Create a temporary merged doc to verify shared history and compute diff
421
+ const mergedDoc = A.merge(A.clone(doc), first.doc()!)
422
+ // Use the merged doc to compute the diff
423
+ return A.diff(
424
+ mergedDoc,
425
+ decodeHeads(this.heads()!),
426
+ decodeHeads(otherHeads)
427
+ )
344
428
  }
345
- // We allow only one set of heads to be specified, in which case we use the doc's heads
346
- const from = second ? first : this.heads() || [] // because we guard above this should always have useful data
429
+
430
+ // Otherwise treat as heads
431
+ const from = second ? first : ((this.heads() || []) as UrlHeads)
347
432
  const to = second ? second : first
348
- return A.diff(this.#doc, from, to)
433
+ return A.diff(doc, decodeHeads(from), decodeHeads(to))
434
+ }
435
+
436
+ /**
437
+ * `metadata(head?)` allows you to look at the metadata for a change
438
+ * this can be used to build history graphs to find commit messages and edit times.
439
+ * this interface.
440
+ *
441
+ * @remarks
442
+ * I'm really not convinced this is the right way to surface this information so
443
+ * I'm leaving this API "hidden".
444
+ *
445
+ * @hidden
446
+ */
447
+ metadata(change?: string): A.DecodedChange | undefined {
448
+ if (!this.isReady()) {
449
+ return undefined
450
+ }
451
+
452
+ if (!change) {
453
+ change = this.heads()![0]
454
+ }
455
+ // we return undefined instead of null by convention in this API
456
+ return (
457
+ A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
458
+ undefined
459
+ )
349
460
  }
350
461
 
351
462
  /**
@@ -368,16 +479,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
368
479
  }
369
480
 
370
481
  /**
371
- * Called by the repo either when a doc handle changes or we receive new remote heads.
482
+ * Called by the repo when a doc handle changes or we receive new remote heads.
372
483
  * @hidden
373
484
  */
374
- setRemoteHeads(storageId: StorageId, heads: A.Heads) {
485
+ setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
375
486
  this.#remoteHeads[storageId] = heads
376
487
  this.emit("remote-heads", { storageId, heads })
377
488
  }
378
489
 
379
490
  /** Returns the heads of the storageId. */
380
- getRemoteHeads(storageId: StorageId): A.Heads | undefined {
491
+ getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
381
492
  return this.#remoteHeads[storageId]
382
493
  }
383
494
 
@@ -402,6 +513,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
402
513
  `DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
403
514
  )
404
515
  }
516
+
517
+ if (this.#fixedHeads) {
518
+ throw new Error(
519
+ `DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
520
+ )
521
+ }
522
+
405
523
  this.#machine.send({
406
524
  type: UPDATE,
407
525
  payload: { callback: doc => A.change(doc, options, callback) },
@@ -413,22 +531,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
413
531
  * @returns A set of heads representing the concurrent change that was made.
414
532
  */
415
533
  changeAt(
416
- heads: A.Heads,
534
+ heads: UrlHeads,
417
535
  callback: A.ChangeFn<T>,
418
536
  options: A.ChangeOptions<T> = {}
419
- ): string[] | undefined {
537
+ ): UrlHeads[] | undefined {
420
538
  if (!this.isReady()) {
421
539
  throw new Error(
422
540
  `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
423
541
  )
424
542
  }
425
- let resultHeads: string[] | undefined = undefined
543
+ if (this.#fixedHeads) {
544
+ throw new Error(
545
+ `DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
546
+ )
547
+ }
548
+ let resultHeads: UrlHeads | undefined = undefined
426
549
  this.#machine.send({
427
550
  type: UPDATE,
428
551
  payload: {
429
552
  callback: doc => {
430
- const result = A.changeAt(doc, heads, options, callback)
431
- resultHeads = result.newHeads || undefined
553
+ const result = A.changeAt(doc, decodeHeads(heads), options, callback)
554
+ resultHeads = result.newHeads
555
+ ? encodeHeads(result.newHeads)
556
+ : undefined
432
557
  return result.newDoc
433
558
  },
434
559
  },
@@ -453,10 +578,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
453
578
  if (!this.isReady() || !otherHandle.isReady()) {
454
579
  throw new Error("Both handles must be ready to merge")
455
580
  }
456
- const mergingDoc = otherHandle.docSync()
457
- if (!mergingDoc) {
458
- throw new Error("The document to be merged in is falsy, aborting.")
581
+ if (this.#fixedHeads) {
582
+ throw new Error(
583
+ `DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
584
+ )
459
585
  }
586
+ const mergingDoc = otherHandle.doc()
460
587
 
461
588
  this.update(doc => {
462
589
  return A.merge(doc, mergingDoc)
@@ -464,20 +591,31 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
464
591
  }
465
592
 
466
593
  /**
467
- * Used in testing to mark this document as unavailable.
594
+ * Updates the internal state machine to mark the document unavailable.
468
595
  * @hidden
469
596
  */
470
597
  unavailable() {
471
598
  this.#machine.send({ type: DOC_UNAVAILABLE })
472
599
  }
473
600
 
474
- /** Called by the repo when the document is not found in storage.
601
+ /**
602
+ * Called by the repo either when the document is not found in storage.
475
603
  * @hidden
476
604
  * */
477
605
  request() {
478
606
  if (this.#state === "loading") this.#machine.send({ type: REQUEST })
479
607
  }
480
608
 
609
+ /** Called by the repo to free memory used by the document. */
610
+ unload() {
611
+ this.#machine.send({ type: UNLOAD })
612
+ }
613
+
614
+ /** Called by the repo to reuse an unloaded handle. */
615
+ reload() {
616
+ this.#machine.send({ type: RELOAD })
617
+ }
618
+
481
619
  /** Called by the repo when the document is deleted. */
482
620
  delete() {
483
621
  this.#machine.send({ type: DELETE })
@@ -493,7 +631,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
493
631
  broadcast(message: unknown) {
494
632
  this.emit("ephemeral-message-outbound", {
495
633
  handle: this,
496
- data: encode(message),
634
+ data: new Uint8Array(encode(message)),
497
635
  })
498
636
  }
499
637
 
@@ -518,6 +656,9 @@ export type DocHandleOptions<T> =
518
656
  | {
519
657
  isNew?: false
520
658
 
659
+ // An optional point in time to lock the document to.
660
+ heads?: UrlHeads
661
+
521
662
  /** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
522
663
  timeoutDelay?: number
523
664
  }
@@ -529,7 +670,6 @@ export interface DocHandleEvents<T> {
529
670
  "heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
530
671
  change: (payload: DocHandleChangePayload<T>) => void
531
672
  delete: (payload: DocHandleDeletePayload<T>) => void
532
- unavailable: (payload: DocHandleUnavailablePayload<T>) => void
533
673
  "ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
534
674
  "ephemeral-message-outbound": (
535
675
  payload: DocHandleOutboundEphemeralMessagePayload<T>
@@ -581,7 +721,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
581
721
  /** Emitted when we have new remote heads for this document */
582
722
  export interface DocHandleRemoteHeadsPayload {
583
723
  storageId: StorageId
584
- heads: A.Heads
724
+ heads: UrlHeads
585
725
  }
586
726
 
587
727
  // STATE MACHINE TYPES & CONSTANTS
@@ -600,6 +740,8 @@ export const HandleState = {
600
740
  REQUESTING: "requesting",
601
741
  /** The document is available */
602
742
  READY: "ready",
743
+ /** The document has been unloaded from the handle, to free memory usage */
744
+ UNLOADED: "unloaded",
603
745
  /** The document has been deleted from the repo */
604
746
  DELETED: "deleted",
605
747
  /** The document was not available in storage or from any connected peers */
@@ -607,8 +749,15 @@ export const HandleState = {
607
749
  } as const
608
750
  export type HandleState = (typeof HandleState)[keyof typeof HandleState]
609
751
 
610
- export const { IDLE, LOADING, REQUESTING, READY, DELETED, UNAVAILABLE } =
611
- HandleState
752
+ export const {
753
+ IDLE,
754
+ LOADING,
755
+ REQUESTING,
756
+ READY,
757
+ UNLOADED,
758
+ DELETED,
759
+ UNAVAILABLE,
760
+ } = HandleState
612
761
 
613
762
  // context
614
763
 
@@ -628,14 +777,18 @@ type DocHandleEvent<T> =
628
777
  type: typeof UPDATE
629
778
  payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
630
779
  }
631
- | { type: typeof TIMEOUT }
780
+ | { type: typeof UNLOAD }
781
+ | { type: typeof RELOAD }
632
782
  | { type: typeof DELETE }
783
+ | { type: typeof TIMEOUT }
633
784
  | { type: typeof DOC_UNAVAILABLE }
634
785
 
635
786
  const BEGIN = "BEGIN"
636
787
  const REQUEST = "REQUEST"
637
788
  const DOC_READY = "DOC_READY"
638
789
  const UPDATE = "UPDATE"
790
+ const UNLOAD = "UNLOAD"
791
+ const RELOAD = "RELOAD"
639
792
  const DELETE = "DELETE"
640
793
  const TIMEOUT = "TIMEOUT"
641
794
  const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
@@ -0,0 +1,48 @@
1
+ import { DocHandle } from "./DocHandle.js"
2
+
3
+ export type FindProgressState =
4
+ | "loading"
5
+ | "ready"
6
+ | "failed"
7
+ | "aborted"
8
+ | "unavailable"
9
+
10
+ interface FindProgressBase<T> {
11
+ state: FindProgressState
12
+ handle: DocHandle<T>
13
+ }
14
+
15
+ interface FindProgressLoading<T> extends FindProgressBase<T> {
16
+ state: "loading"
17
+ progress: number
18
+ }
19
+
20
+ interface FindProgressReady<T> extends FindProgressBase<T> {
21
+ state: "ready"
22
+ }
23
+
24
+ interface FindProgressFailed<T> extends FindProgressBase<T> {
25
+ state: "failed"
26
+ error: Error
27
+ }
28
+
29
+ interface FindProgressUnavailable<T> extends FindProgressBase<T> {
30
+ state: "unavailable"
31
+ }
32
+
33
+ interface FindProgressAborted<T> extends FindProgressBase<T> {
34
+ state: "aborted"
35
+ }
36
+
37
+ export type FindProgress<T> =
38
+ | FindProgressLoading<T>
39
+ | FindProgressReady<T>
40
+ | FindProgressFailed<T>
41
+ | FindProgressUnavailable<T>
42
+ | FindProgressAborted<T>
43
+
44
+ export type FindProgressWithMethods<T> = FindProgress<T> & {
45
+ next: () => Promise<FindProgressWithMethods<T>>
46
+ // TODO: i don't like this allowableStates
47
+ untilReady: (allowableStates: string[]) => Promise<DocHandle<T>>
48
+ }
@@ -1,6 +1,5 @@
1
- import { next as A } from "@automerge/automerge/slim"
2
1
  import { EventEmitter } from "eventemitter3"
3
- import { DocumentId, PeerId } from "./types.js"
2
+ import { DocumentId, PeerId, UrlHeads } from "./types.js"
4
3
  import {
5
4
  RemoteHeadsChanged,
6
5
  RemoteSubscriptionControlMessage,
@@ -12,7 +11,7 @@ import debug from "debug"
12
11
  export type RemoteHeadsSubscriptionEventPayload = {
13
12
  documentId: DocumentId
14
13
  storageId: StorageId
15
- remoteHeads: A.Heads
14
+ remoteHeads: UrlHeads
16
15
  timestamp: number
17
16
  }
18
17
 
@@ -21,7 +20,7 @@ export type NotifyRemoteHeadsPayload = {
21
20
  targetId: PeerId
22
21
  documentId: DocumentId
23
22
  storageId: StorageId
24
- heads: A.Heads
23
+ heads: UrlHeads
25
24
  timestamp: number
26
25
  }
27
26
 
@@ -216,7 +215,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
216
215
  handleImmediateRemoteHeadsChanged(
217
216
  documentId: DocumentId,
218
217
  storageId: StorageId,
219
- heads: A.Heads
218
+ heads: UrlHeads
220
219
  ) {
221
220
  this.#log("handleLocalHeadsChanged", documentId, storageId, heads)
222
221
  const remote = this.#knownHeads.get(documentId)
@@ -334,7 +333,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
334
333
  #changedHeads(msg: RemoteHeadsChanged): {
335
334
  documentId: DocumentId
336
335
  storageId: StorageId
337
- remoteHeads: A.Heads
336
+ remoteHeads: UrlHeads
338
337
  timestamp: number
339
338
  }[] {
340
339
  const changedHeads = []
@@ -356,11 +355,14 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
356
355
  if (docRemote && docRemote.timestamp >= timestamp) {
357
356
  continue
358
357
  } else {
359
- remote.set(storageId as StorageId, { timestamp, heads })
358
+ remote.set(storageId as StorageId, {
359
+ timestamp,
360
+ heads: heads as UrlHeads,
361
+ })
360
362
  changedHeads.push({
361
363
  documentId,
362
364
  storageId: storageId as StorageId,
363
- remoteHeads: heads,
365
+ remoteHeads: heads as UrlHeads,
364
366
  timestamp,
365
367
  })
366
368
  }
@@ -371,5 +373,5 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
371
373
 
372
374
  type LastHeads = {
373
375
  timestamp: number
374
- heads: A.Heads
376
+ heads: UrlHeads
375
377
  }