@automerge/automerge-repo 2.0.0-alpha.2 → 2.0.0-alpha.20

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 (61) hide show
  1. package/dist/AutomergeUrl.d.ts +17 -5
  2. package/dist/AutomergeUrl.d.ts.map +1 -1
  3. package/dist/AutomergeUrl.js +71 -24
  4. package/dist/DocHandle.d.ts +80 -8
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +181 -10
  7. package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
  8. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
  9. package/dist/RemoteHeadsSubscriptions.js +4 -1
  10. package/dist/Repo.d.ts +35 -2
  11. package/dist/Repo.d.ts.map +1 -1
  12. package/dist/Repo.js +112 -70
  13. package/dist/entrypoints/fullfat.d.ts +1 -0
  14. package/dist/entrypoints/fullfat.d.ts.map +1 -1
  15. package/dist/entrypoints/fullfat.js +1 -2
  16. package/dist/helpers/bufferFromHex.d.ts +3 -0
  17. package/dist/helpers/bufferFromHex.d.ts.map +1 -0
  18. package/dist/helpers/bufferFromHex.js +13 -0
  19. package/dist/helpers/headsAreSame.d.ts +2 -2
  20. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  21. package/dist/helpers/mergeArrays.d.ts +1 -1
  22. package/dist/helpers/mergeArrays.d.ts.map +1 -1
  23. package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
  24. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  25. package/dist/helpers/tests/storage-adapter-tests.js +25 -48
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/storage/StorageSubsystem.d.ts +11 -1
  30. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  31. package/dist/storage/StorageSubsystem.js +20 -4
  32. package/dist/synchronizer/CollectionSynchronizer.d.ts +15 -2
  33. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  34. package/dist/synchronizer/CollectionSynchronizer.js +29 -8
  35. package/dist/synchronizer/DocSynchronizer.d.ts +7 -0
  36. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  37. package/dist/synchronizer/DocSynchronizer.js +14 -0
  38. package/dist/synchronizer/Synchronizer.d.ts +11 -0
  39. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  40. package/dist/types.d.ts +4 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +3 -3
  43. package/src/AutomergeUrl.ts +101 -26
  44. package/src/DocHandle.ts +245 -20
  45. package/src/RemoteHeadsSubscriptions.ts +11 -9
  46. package/src/Repo.ts +163 -68
  47. package/src/entrypoints/fullfat.ts +1 -2
  48. package/src/helpers/bufferFromHex.ts +14 -0
  49. package/src/helpers/headsAreSame.ts +2 -2
  50. package/src/helpers/tests/storage-adapter-tests.ts +44 -86
  51. package/src/index.ts +2 -0
  52. package/src/storage/StorageSubsystem.ts +29 -4
  53. package/src/synchronizer/CollectionSynchronizer.ts +42 -9
  54. package/src/synchronizer/DocSynchronizer.ts +15 -0
  55. package/src/synchronizer/Synchronizer.ts +14 -0
  56. package/src/types.ts +4 -1
  57. package/test/AutomergeUrl.test.ts +130 -0
  58. package/test/DocHandle.test.ts +209 -2
  59. package/test/DocSynchronizer.test.ts +10 -3
  60. package/test/Repo.test.ts +228 -3
  61. package/test/StorageSubsystem.test.ts +17 -0
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,7 @@ 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> = {}
40
47
 
41
48
  /** @hidden */
42
49
  constructor(
@@ -49,6 +56,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
49
56
  this.#timeoutDelay = options.timeoutDelay
50
57
  }
51
58
 
59
+ if ("heads" in options) {
60
+ this.#fixedHeads = options.heads
61
+ }
62
+
52
63
  const doc = A.init<T>()
53
64
 
54
65
  this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
@@ -72,6 +83,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
72
83
  this.emit("delete", { handle: this })
73
84
  return { doc: A.init() }
74
85
  }),
86
+ onUnload: assign(() => {
87
+ return { doc: A.init() }
88
+ }),
75
89
  onUnavailable: () => {
76
90
  this.emit("unavailable", { handle: this })
77
91
  },
@@ -86,6 +100,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
86
100
  context: { documentId, doc },
87
101
  on: {
88
102
  UPDATE: { actions: "onUpdate" },
103
+ UNLOAD: ".unloaded",
89
104
  DELETE: ".deleted",
90
105
  },
91
106
  states: {
@@ -113,6 +128,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
113
128
  on: { DOC_READY: "ready" },
114
129
  },
115
130
  ready: {},
131
+ unloaded: {
132
+ entry: "onUnload",
133
+ on: {
134
+ RELOAD: "loading",
135
+ },
136
+ },
116
137
  deleted: { entry: "onDelete", type: "final" },
117
138
  },
118
139
  })
@@ -131,7 +152,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
131
152
 
132
153
  // Start the machine, and send a create or find event to get things going
133
154
  this.#machine.start()
134
- this.#machine.send({ type: BEGIN })
155
+ this.begin()
135
156
  }
136
157
 
137
158
  // PRIVATE
@@ -166,7 +187,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
166
187
  #checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
167
188
  const beforeHeads = A.getHeads(before)
168
189
  const afterHeads = A.getHeads(after)
169
- const docChanged = !headsAreSame(afterHeads, beforeHeads)
190
+ const docChanged = !headsAreSame(
191
+ encodeHeads(afterHeads),
192
+ encodeHeads(beforeHeads)
193
+ )
170
194
  if (docChanged) {
171
195
  this.emit("heads-changed", { handle: this, doc: after })
172
196
 
@@ -192,7 +216,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
192
216
  /** Our documentId in Automerge URL form.
193
217
  */
194
218
  get url(): AutomergeUrl {
195
- return stringifyAutomergeUrl({ documentId: this.documentId })
219
+ return stringifyAutomergeUrl({
220
+ documentId: this.documentId,
221
+ heads: this.#fixedHeads,
222
+ })
196
223
  }
197
224
 
198
225
  /**
@@ -203,6 +230,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
203
230
  */
204
231
  isReady = () => this.inState(["ready"])
205
232
 
233
+ /**
234
+ * @returns true if the document has been unloaded.
235
+ *
236
+ * Unloaded documents are freed from memory but not removed from local storage. It's not currently
237
+ * possible at runtime to reload an unloaded document.
238
+ */
239
+ isUnloaded = () => this.inState(["unloaded"])
240
+
206
241
  /**
207
242
  * @returns true if the document has been marked as deleted.
208
243
  *
@@ -257,6 +292,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
257
292
  // if we timed out, return undefined
258
293
  return undefined
259
294
  }
295
+ // If we have fixed heads, return a view at those heads
296
+ if (this.#fixedHeads) {
297
+ const doc = this.#doc
298
+ if (!doc || this.isUnavailable()) return undefined
299
+ return A.view(doc, decodeHeads(this.#fixedHeads))
300
+ }
260
301
  // Return the document
261
302
  return !this.isUnavailable() ? this.#doc : undefined
262
303
  }
@@ -276,7 +317,11 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
276
317
  */
277
318
  docSync() {
278
319
  if (!this.isReady()) return undefined
279
- else return this.#doc
320
+ if (this.#fixedHeads) {
321
+ const doc = this.#doc
322
+ return doc ? A.view(doc, decodeHeads(this.#fixedHeads)) : undefined
323
+ }
324
+ return this.#doc
280
325
  }
281
326
 
282
327
  /**
@@ -284,11 +329,142 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
284
329
  * This precisely defines the state of a document.
285
330
  * @returns the current document's heads, or undefined if the document is not ready
286
331
  */
287
- heads(): A.Heads | undefined {
332
+ heads(): UrlHeads | undefined {
333
+ if (!this.isReady()) return undefined
334
+ if (this.#fixedHeads) {
335
+ return this.#fixedHeads
336
+ }
337
+ return encodeHeads(A.getHeads(this.#doc))
338
+ }
339
+
340
+ begin() {
341
+ this.#machine.send({ type: BEGIN })
342
+ }
343
+
344
+ /**
345
+ * Returns an array of all past "heads" for the document in topological order.
346
+ *
347
+ * @remarks
348
+ * A point-in-time in an automerge document is an *array* of heads since there may be
349
+ * concurrent edits. This API just returns a topologically sorted history of all edits
350
+ * so every previous entry will be (in some sense) before later ones, but the set of all possible
351
+ * history views would be quite large under concurrency (every thing in each branch against each other).
352
+ * There might be a clever way to think about this, but we haven't found it yet, so for now at least
353
+ * we present a single traversable view which excludes concurrency.
354
+ * @returns UrlHeads[] - The individual heads for every change in the document. Each item is a tagged string[1].
355
+ */
356
+ history(): UrlHeads[] | undefined {
288
357
  if (!this.isReady()) {
289
358
  return undefined
290
359
  }
291
- return A.getHeads(this.#doc)
360
+ // This just returns all the heads as individual strings.
361
+
362
+ return A.topoHistoryTraversal(this.#doc).map(h =>
363
+ encodeHeads([h])
364
+ ) as UrlHeads[]
365
+ }
366
+
367
+ /**
368
+ * Creates a new DocHandle with a fixed "view" at the given point in time represented
369
+ * by the `heads` passed in. The return value is the same type as docSync() and will return
370
+ * undefined if the object hasn't finished loading.
371
+ *
372
+ * @remarks
373
+ * Note that our Typescript types do not consider change over time and the current version
374
+ * of Automerge doesn't check types at runtime, so if you go back to an old set of heads
375
+ * that doesn't match the heads here, Typescript will not save you.
376
+ *
377
+ * @argument heads - The heads to view the document at. See history().
378
+ * @returns DocHandle<T> at the time of `heads`
379
+ */
380
+ view(heads: UrlHeads): DocHandle<T> {
381
+ if (!this.isReady()) {
382
+ throw new Error(
383
+ `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
384
+ )
385
+ }
386
+ // Create a new handle with the same documentId but fixed heads
387
+ const handle = new DocHandle<T>(this.documentId, {
388
+ heads,
389
+ timeoutDelay: this.#timeoutDelay,
390
+ })
391
+ handle.update(() => A.clone(this.#doc))
392
+ handle.doneLoading()
393
+
394
+ return handle
395
+ }
396
+
397
+ /**
398
+ * Returns a set of Patch operations that will move a materialized document from one state to another
399
+ * if applied.
400
+ *
401
+ * @remarks
402
+ * We allow specifying either:
403
+ * - Two sets of heads to compare directly
404
+ * - A single set of heads to compare against our current heads
405
+ * - Another DocHandle to compare against (which must share history with this document)
406
+ *
407
+ * @throws Error if the documents don't share history or if either document is not ready
408
+ * @returns Automerge patches that go from one document state to the other
409
+ */
410
+ diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
411
+ if (!this.isReady()) {
412
+ throw new Error(
413
+ `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
414
+ )
415
+ }
416
+
417
+ const doc = this.#doc
418
+ if (!doc) throw new Error("Document not available")
419
+
420
+ // If first argument is a DocHandle
421
+ if (first instanceof DocHandle) {
422
+ if (!first.isReady()) {
423
+ throw new Error("Cannot diff against a handle that isn't ready")
424
+ }
425
+ const otherHeads = first.heads()
426
+ if (!otherHeads) throw new Error("Other document's heads not available")
427
+
428
+ // Create a temporary merged doc to verify shared history and compute diff
429
+ const mergedDoc = A.merge(A.clone(doc), first.docSync()!)
430
+ // Use the merged doc to compute the diff
431
+ return A.diff(
432
+ mergedDoc,
433
+ decodeHeads(this.heads()!),
434
+ decodeHeads(otherHeads)
435
+ )
436
+ }
437
+
438
+ // Otherwise treat as heads
439
+ const from = second ? first : ((this.heads() || []) as UrlHeads)
440
+ const to = second ? second : first
441
+ return A.diff(doc, decodeHeads(from), decodeHeads(to))
442
+ }
443
+
444
+ /**
445
+ * `metadata(head?)` allows you to look at the metadata for a change
446
+ * this can be used to build history graphs to find commit messages and edit times.
447
+ * this interface.
448
+ *
449
+ * @remarks
450
+ * I'm really not convinced this is the right way to surface this information so
451
+ * I'm leaving this API "hidden".
452
+ *
453
+ * @hidden
454
+ */
455
+ metadata(change?: string): A.DecodedChange | undefined {
456
+ if (!this.isReady()) {
457
+ return undefined
458
+ }
459
+
460
+ if (!change) {
461
+ change = this.heads()![0]
462
+ }
463
+ // we return undefined instead of null by convention in this API
464
+ return (
465
+ A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
466
+ undefined
467
+ )
292
468
  }
293
469
 
294
470
  /**
@@ -314,13 +490,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
314
490
  * Called by the repo either when a doc handle changes or we receive new remote heads.
315
491
  * @hidden
316
492
  */
317
- setRemoteHeads(storageId: StorageId, heads: A.Heads) {
493
+ setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
318
494
  this.#remoteHeads[storageId] = heads
319
495
  this.emit("remote-heads", { storageId, heads })
320
496
  }
321
497
 
322
498
  /** Returns the heads of the storageId. */
323
- getRemoteHeads(storageId: StorageId): A.Heads | undefined {
499
+ getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
324
500
  return this.#remoteHeads[storageId]
325
501
  }
326
502
 
@@ -345,6 +521,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
345
521
  `DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
346
522
  )
347
523
  }
524
+
525
+ if (this.#fixedHeads) {
526
+ throw new Error(
527
+ `DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
528
+ )
529
+ }
530
+
348
531
  this.#machine.send({
349
532
  type: UPDATE,
350
533
  payload: { callback: doc => A.change(doc, options, callback) },
@@ -356,22 +539,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
356
539
  * @returns A set of heads representing the concurrent change that was made.
357
540
  */
358
541
  changeAt(
359
- heads: A.Heads,
542
+ heads: UrlHeads,
360
543
  callback: A.ChangeFn<T>,
361
544
  options: A.ChangeOptions<T> = {}
362
- ): string[] | undefined {
545
+ ): UrlHeads[] | undefined {
363
546
  if (!this.isReady()) {
364
547
  throw new Error(
365
548
  `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
366
549
  )
367
550
  }
368
- let resultHeads: string[] | undefined = undefined
551
+ if (this.#fixedHeads) {
552
+ throw new Error(
553
+ `DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
554
+ )
555
+ }
556
+ let resultHeads: UrlHeads | undefined = undefined
369
557
  this.#machine.send({
370
558
  type: UPDATE,
371
559
  payload: {
372
560
  callback: doc => {
373
- const result = A.changeAt(doc, heads, options, callback)
374
- resultHeads = result.newHeads || undefined
561
+ const result = A.changeAt(doc, decodeHeads(heads), options, callback)
562
+ resultHeads = result.newHeads
563
+ ? encodeHeads(result.newHeads)
564
+ : undefined
375
565
  return result.newDoc
376
566
  },
377
567
  },
@@ -396,6 +586,11 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
396
586
  if (!this.isReady() || !otherHandle.isReady()) {
397
587
  throw new Error("Both handles must be ready to merge")
398
588
  }
589
+ if (this.#fixedHeads) {
590
+ throw new Error(
591
+ `DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
592
+ )
593
+ }
399
594
  const mergingDoc = otherHandle.docSync()
400
595
  if (!mergingDoc) {
401
596
  throw new Error("The document to be merged in is falsy, aborting.")
@@ -421,6 +616,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
421
616
  if (this.#state === "loading") this.#machine.send({ type: REQUEST })
422
617
  }
423
618
 
619
+ /** Called by the repo to free memory used by the document. */
620
+ unload() {
621
+ this.#machine.send({ type: UNLOAD })
622
+ }
623
+
624
+ /** Called by the repo to reuse an unloaded handle. */
625
+ reload() {
626
+ this.#machine.send({ type: RELOAD })
627
+ }
628
+
424
629
  /** Called by the repo when the document is deleted. */
425
630
  delete() {
426
631
  this.#machine.send({ type: DELETE })
@@ -439,6 +644,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
439
644
  data: encode(message),
440
645
  })
441
646
  }
647
+
648
+ metrics(): { numOps: number; numChanges: number } {
649
+ return A.stats(this.#doc)
650
+ }
442
651
  }
443
652
 
444
653
  // TYPES
@@ -457,6 +666,9 @@ export type DocHandleOptions<T> =
457
666
  | {
458
667
  isNew?: false
459
668
 
669
+ // An optional point in time to lock the document to.
670
+ heads?: UrlHeads
671
+
460
672
  /** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
461
673
  timeoutDelay?: number
462
674
  }
@@ -520,7 +732,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
520
732
  /** Emitted when we have new remote heads for this document */
521
733
  export interface DocHandleRemoteHeadsPayload {
522
734
  storageId: StorageId
523
- heads: A.Heads
735
+ heads: UrlHeads
524
736
  }
525
737
 
526
738
  // STATE MACHINE TYPES & CONSTANTS
@@ -539,6 +751,8 @@ export const HandleState = {
539
751
  REQUESTING: "requesting",
540
752
  /** The document is available */
541
753
  READY: "ready",
754
+ /** The document has been unloaded from the handle, to free memory usage */
755
+ UNLOADED: "unloaded",
542
756
  /** The document has been deleted from the repo */
543
757
  DELETED: "deleted",
544
758
  /** The document was not available in storage or from any connected peers */
@@ -546,8 +760,15 @@ export const HandleState = {
546
760
  } as const
547
761
  export type HandleState = (typeof HandleState)[keyof typeof HandleState]
548
762
 
549
- export const { IDLE, LOADING, REQUESTING, READY, DELETED, UNAVAILABLE } =
550
- HandleState
763
+ export const {
764
+ IDLE,
765
+ LOADING,
766
+ REQUESTING,
767
+ READY,
768
+ UNLOADED,
769
+ DELETED,
770
+ UNAVAILABLE,
771
+ } = HandleState
551
772
 
552
773
  // context
553
774
 
@@ -567,14 +788,18 @@ type DocHandleEvent<T> =
567
788
  type: typeof UPDATE
568
789
  payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
569
790
  }
570
- | { type: typeof TIMEOUT }
791
+ | { type: typeof UNLOAD }
792
+ | { type: typeof RELOAD }
571
793
  | { type: typeof DELETE }
794
+ | { type: typeof TIMEOUT }
572
795
  | { type: typeof DOC_UNAVAILABLE }
573
796
 
574
797
  const BEGIN = "BEGIN"
575
798
  const REQUEST = "REQUEST"
576
799
  const DOC_READY = "DOC_READY"
577
800
  const UPDATE = "UPDATE"
801
+ const UNLOAD = "UNLOAD"
802
+ const RELOAD = "RELOAD"
578
803
  const DELETE = "DELETE"
579
804
  const TIMEOUT = "TIMEOUT"
580
805
  const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
@@ -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
  }