@automerge/automerge-repo 2.0.0-collectionsync-alpha.1 → 2.0.0

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 (203) 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 +33 -41
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +105 -66
  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 +24 -5
  15. package/dist/Repo.d.ts.map +1 -1
  16. package/dist/Repo.js +355 -169
  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.map +1 -1
  34. package/dist/helpers/tests/storage-adapter-tests.js +6 -9
  35. package/dist/helpers/throttle.d.ts.map +1 -1
  36. package/dist/helpers/withTimeout.d.ts.map +1 -1
  37. package/dist/index.d.ts +35 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +37 -6
  40. package/dist/network/NetworkSubsystem.d.ts +0 -1
  41. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  42. package/dist/network/NetworkSubsystem.js +0 -3
  43. package/dist/network/messages.d.ts +1 -7
  44. package/dist/network/messages.d.ts.map +1 -1
  45. package/dist/network/messages.js +1 -2
  46. package/dist/storage/StorageAdapter.d.ts +0 -9
  47. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  48. package/dist/storage/StorageAdapter.js +0 -33
  49. package/dist/storage/StorageSubsystem.d.ts +6 -2
  50. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  51. package/dist/storage/StorageSubsystem.js +131 -37
  52. package/dist/storage/keyHash.d.ts +1 -1
  53. package/dist/storage/keyHash.d.ts.map +1 -1
  54. package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -4
  55. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  56. package/dist/synchronizer/CollectionSynchronizer.js +32 -26
  57. package/dist/synchronizer/DocSynchronizer.d.ts +8 -8
  58. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  59. package/dist/synchronizer/DocSynchronizer.js +205 -79
  60. package/dist/types.d.ts +4 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/fuzz/fuzz.ts +3 -3
  63. package/package.json +4 -5
  64. package/src/AutomergeUrl.ts +101 -26
  65. package/src/DocHandle.ts +158 -77
  66. package/src/FindProgress.ts +48 -0
  67. package/src/RemoteHeadsSubscriptions.ts +11 -9
  68. package/src/Repo.ts +465 -180
  69. package/src/helpers/abortable.ts +62 -0
  70. package/src/helpers/bufferFromHex.ts +14 -0
  71. package/src/helpers/headsAreSame.ts +2 -2
  72. package/src/helpers/tests/network-adapter-tests.ts +14 -13
  73. package/src/helpers/tests/storage-adapter-tests.ts +13 -24
  74. package/src/index.ts +57 -38
  75. package/src/network/NetworkSubsystem.ts +0 -4
  76. package/src/network/messages.ts +2 -11
  77. package/src/storage/StorageAdapter.ts +0 -42
  78. package/src/storage/StorageSubsystem.ts +155 -45
  79. package/src/storage/keyHash.ts +1 -1
  80. package/src/synchronizer/CollectionSynchronizer.ts +42 -29
  81. package/src/synchronizer/DocSynchronizer.ts +263 -89
  82. package/src/types.ts +4 -1
  83. package/test/AutomergeUrl.test.ts +130 -0
  84. package/test/CollectionSynchronizer.test.ts +6 -8
  85. package/test/DocHandle.test.ts +161 -77
  86. package/test/DocSynchronizer.test.ts +11 -9
  87. package/test/RemoteHeadsSubscriptions.test.ts +1 -1
  88. package/test/Repo.test.ts +406 -341
  89. package/test/StorageSubsystem.test.ts +95 -20
  90. package/test/remoteHeads.test.ts +28 -13
  91. package/dist/CollectionHandle.d.ts +0 -14
  92. package/dist/CollectionHandle.d.ts.map +0 -1
  93. package/dist/CollectionHandle.js +0 -37
  94. package/dist/DocUrl.d.ts +0 -47
  95. package/dist/DocUrl.d.ts.map +0 -1
  96. package/dist/DocUrl.js +0 -72
  97. package/dist/EphemeralData.d.ts +0 -20
  98. package/dist/EphemeralData.d.ts.map +0 -1
  99. package/dist/EphemeralData.js +0 -1
  100. package/dist/ferigan.d.ts +0 -51
  101. package/dist/ferigan.d.ts.map +0 -1
  102. package/dist/ferigan.js +0 -98
  103. package/dist/src/DocHandle.d.ts +0 -182
  104. package/dist/src/DocHandle.d.ts.map +0 -1
  105. package/dist/src/DocHandle.js +0 -405
  106. package/dist/src/DocUrl.d.ts +0 -49
  107. package/dist/src/DocUrl.d.ts.map +0 -1
  108. package/dist/src/DocUrl.js +0 -72
  109. package/dist/src/EphemeralData.d.ts +0 -19
  110. package/dist/src/EphemeralData.d.ts.map +0 -1
  111. package/dist/src/EphemeralData.js +0 -1
  112. package/dist/src/Repo.d.ts +0 -74
  113. package/dist/src/Repo.d.ts.map +0 -1
  114. package/dist/src/Repo.js +0 -208
  115. package/dist/src/helpers/arraysAreEqual.d.ts +0 -2
  116. package/dist/src/helpers/arraysAreEqual.d.ts.map +0 -1
  117. package/dist/src/helpers/arraysAreEqual.js +0 -2
  118. package/dist/src/helpers/cbor.d.ts +0 -4
  119. package/dist/src/helpers/cbor.d.ts.map +0 -1
  120. package/dist/src/helpers/cbor.js +0 -8
  121. package/dist/src/helpers/eventPromise.d.ts +0 -11
  122. package/dist/src/helpers/eventPromise.d.ts.map +0 -1
  123. package/dist/src/helpers/eventPromise.js +0 -7
  124. package/dist/src/helpers/headsAreSame.d.ts +0 -2
  125. package/dist/src/helpers/headsAreSame.d.ts.map +0 -1
  126. package/dist/src/helpers/headsAreSame.js +0 -4
  127. package/dist/src/helpers/mergeArrays.d.ts +0 -2
  128. package/dist/src/helpers/mergeArrays.d.ts.map +0 -1
  129. package/dist/src/helpers/mergeArrays.js +0 -15
  130. package/dist/src/helpers/pause.d.ts +0 -6
  131. package/dist/src/helpers/pause.d.ts.map +0 -1
  132. package/dist/src/helpers/pause.js +0 -10
  133. package/dist/src/helpers/tests/network-adapter-tests.d.ts +0 -21
  134. package/dist/src/helpers/tests/network-adapter-tests.d.ts.map +0 -1
  135. package/dist/src/helpers/tests/network-adapter-tests.js +0 -122
  136. package/dist/src/helpers/withTimeout.d.ts +0 -12
  137. package/dist/src/helpers/withTimeout.d.ts.map +0 -1
  138. package/dist/src/helpers/withTimeout.js +0 -24
  139. package/dist/src/index.d.ts +0 -53
  140. package/dist/src/index.d.ts.map +0 -1
  141. package/dist/src/index.js +0 -40
  142. package/dist/src/network/NetworkAdapter.d.ts +0 -26
  143. package/dist/src/network/NetworkAdapter.d.ts.map +0 -1
  144. package/dist/src/network/NetworkAdapter.js +0 -4
  145. package/dist/src/network/NetworkSubsystem.d.ts +0 -23
  146. package/dist/src/network/NetworkSubsystem.d.ts.map +0 -1
  147. package/dist/src/network/NetworkSubsystem.js +0 -120
  148. package/dist/src/network/messages.d.ts +0 -85
  149. package/dist/src/network/messages.d.ts.map +0 -1
  150. package/dist/src/network/messages.js +0 -23
  151. package/dist/src/storage/StorageAdapter.d.ts +0 -14
  152. package/dist/src/storage/StorageAdapter.d.ts.map +0 -1
  153. package/dist/src/storage/StorageAdapter.js +0 -1
  154. package/dist/src/storage/StorageSubsystem.d.ts +0 -12
  155. package/dist/src/storage/StorageSubsystem.d.ts.map +0 -1
  156. package/dist/src/storage/StorageSubsystem.js +0 -145
  157. package/dist/src/synchronizer/CollectionSynchronizer.d.ts +0 -25
  158. package/dist/src/synchronizer/CollectionSynchronizer.d.ts.map +0 -1
  159. package/dist/src/synchronizer/CollectionSynchronizer.js +0 -106
  160. package/dist/src/synchronizer/DocSynchronizer.d.ts +0 -29
  161. package/dist/src/synchronizer/DocSynchronizer.d.ts.map +0 -1
  162. package/dist/src/synchronizer/DocSynchronizer.js +0 -263
  163. package/dist/src/synchronizer/Synchronizer.d.ts +0 -9
  164. package/dist/src/synchronizer/Synchronizer.d.ts.map +0 -1
  165. package/dist/src/synchronizer/Synchronizer.js +0 -2
  166. package/dist/src/types.d.ts +0 -16
  167. package/dist/src/types.d.ts.map +0 -1
  168. package/dist/src/types.js +0 -1
  169. package/dist/test/CollectionSynchronizer.test.d.ts +0 -2
  170. package/dist/test/CollectionSynchronizer.test.d.ts.map +0 -1
  171. package/dist/test/CollectionSynchronizer.test.js +0 -57
  172. package/dist/test/DocHandle.test.d.ts +0 -2
  173. package/dist/test/DocHandle.test.d.ts.map +0 -1
  174. package/dist/test/DocHandle.test.js +0 -238
  175. package/dist/test/DocSynchronizer.test.d.ts +0 -2
  176. package/dist/test/DocSynchronizer.test.d.ts.map +0 -1
  177. package/dist/test/DocSynchronizer.test.js +0 -111
  178. package/dist/test/Network.test.d.ts +0 -2
  179. package/dist/test/Network.test.d.ts.map +0 -1
  180. package/dist/test/Network.test.js +0 -11
  181. package/dist/test/Repo.test.d.ts +0 -2
  182. package/dist/test/Repo.test.d.ts.map +0 -1
  183. package/dist/test/Repo.test.js +0 -568
  184. package/dist/test/StorageSubsystem.test.d.ts +0 -2
  185. package/dist/test/StorageSubsystem.test.d.ts.map +0 -1
  186. package/dist/test/StorageSubsystem.test.js +0 -56
  187. package/dist/test/helpers/DummyNetworkAdapter.d.ts +0 -9
  188. package/dist/test/helpers/DummyNetworkAdapter.d.ts.map +0 -1
  189. package/dist/test/helpers/DummyNetworkAdapter.js +0 -15
  190. package/dist/test/helpers/DummyStorageAdapter.d.ts +0 -16
  191. package/dist/test/helpers/DummyStorageAdapter.d.ts.map +0 -1
  192. package/dist/test/helpers/DummyStorageAdapter.js +0 -33
  193. package/dist/test/helpers/generate-large-object.d.ts +0 -5
  194. package/dist/test/helpers/generate-large-object.d.ts.map +0 -1
  195. package/dist/test/helpers/generate-large-object.js +0 -9
  196. package/dist/test/helpers/getRandomItem.d.ts +0 -2
  197. package/dist/test/helpers/getRandomItem.d.ts.map +0 -1
  198. package/dist/test/helpers/getRandomItem.js +0 -4
  199. package/dist/test/types.d.ts +0 -4
  200. package/dist/test/types.d.ts.map +0 -1
  201. package/dist/test/types.js +0 -1
  202. package/src/CollectionHandle.ts +0 -54
  203. package/src/ferigan.ts +0 -184
package/src/DocHandle.ts CHANGED
@@ -1,12 +1,16 @@
1
- import * as A from "@automerge/automerge/slim/next"
1
+ import { next as A } from "@automerge/automerge/slim"
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,12 +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
  }),
89
+ onUnavailable: assign(() => {
90
+ return { doc: A.init() }
91
+ }),
75
92
  onUnload: assign(() => {
76
93
  return { doc: A.init() }
77
94
  }),
78
- onUnavailable: () => {
79
- this.emit("unavailable", { handle: this })
80
- },
81
95
  },
82
96
  }).createMachine({
83
97
  /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
@@ -176,7 +190,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
176
190
  #checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
177
191
  const beforeHeads = A.getHeads(before)
178
192
  const afterHeads = A.getHeads(after)
179
- const docChanged = !headsAreSame(afterHeads, beforeHeads)
193
+ const docChanged = !headsAreSame(
194
+ encodeHeads(afterHeads),
195
+ encodeHeads(beforeHeads)
196
+ )
180
197
  if (docChanged) {
181
198
  this.emit("heads-changed", { handle: this, doc: after })
182
199
 
@@ -202,7 +219,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
202
219
  /** Our documentId in Automerge URL form.
203
220
  */
204
221
  get url(): AutomergeUrl {
205
- return stringifyAutomergeUrl({ documentId: this.documentId })
222
+ return stringifyAutomergeUrl({
223
+ documentId: this.documentId,
224
+ heads: this.#fixedHeads,
225
+ })
206
226
  }
207
227
 
208
228
  /**
@@ -259,42 +279,28 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
259
279
  }
260
280
 
261
281
  /**
262
- * @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
263
286
  *
264
- * This is the recommended way to access a handle's document. Note that this waits for the handle
265
- * to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
266
287
  */
267
- async doc(
268
- /** states to wait for, such as "LOADING". mostly for internal use. */
269
- awaitStates: HandleState[] = ["ready", "unavailable"]
270
- ) {
271
- try {
272
- // wait for the document to enter one of the desired states
273
- await this.#statePromise(awaitStates)
274
- } catch (error) {
275
- // if we timed out, return undefined
276
- 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))
277
292
  }
278
- // Return the document
279
- return !this.isUnavailable() ? this.#doc : undefined
293
+ return this.#doc
280
294
  }
281
295
 
282
296
  /**
283
- * Synchronously returns the current state of the Automerge document this handle manages, or
284
- * undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
285
- * if you want to make sure loading is complete first.
286
- *
287
- * Not to be confused with the SyncState of the document, which describes the state of the
288
- * synchronization process.
289
- *
290
- * Note that `undefined` is not a valid Automerge document, so the return from this function is
291
- * unambigous.
292
297
  *
293
- * @returns the current document, or undefined if the document is not ready.
294
- */
298
+ * @deprecated */
295
299
  docSync() {
296
- if (!this.isReady()) return undefined
297
- 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()
298
304
  }
299
305
 
300
306
  /**
@@ -302,11 +308,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
302
308
  * This precisely defines the state of a document.
303
309
  * @returns the current document's heads, or undefined if the document is not ready
304
310
  */
305
- heads(): A.Heads | undefined {
306
- if (!this.isReady()) {
307
- return undefined
311
+ heads(): UrlHeads {
312
+ if (!this.isReady()) throw new Error("DocHandle is not ready")
313
+ if (this.#fixedHeads) {
314
+ return this.#fixedHeads
308
315
  }
309
- return A.getHeads(this.#doc)
316
+ return encodeHeads(A.getHeads(this.#doc))
310
317
  }
311
318
 
312
319
  begin() {
@@ -314,9 +321,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
314
321
  }
315
322
 
316
323
  /**
317
- * Creates a fixed "view" of an automerge document at the given point in time represented
318
- * by the `heads` passed in. The return value is the same type as docSync() and will return
319
- * undefined if the object hasn't finished loading.
324
+ * Returns an array of all past "heads" for the document in topological order.
320
325
  *
321
326
  * @remarks
322
327
  * A point-in-time in an automerge document is an *array* of heads since there may be
@@ -325,20 +330,22 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
325
330
  * history views would be quite large under concurrency (every thing in each branch against each other).
326
331
  * There might be a clever way to think about this, but we haven't found it yet, so for now at least
327
332
  * we present a single traversable view which excludes concurrency.
328
- * @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].
329
334
  */
330
- history(): A.Heads[] | undefined {
335
+ history(): UrlHeads[] | undefined {
331
336
  if (!this.isReady()) {
332
337
  return undefined
333
338
  }
334
339
  // This just returns all the heads as individual strings.
335
340
 
336
- 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[]
337
344
  }
338
345
 
339
346
  /**
340
347
  * Creates a fixed "view" of an automerge document at the given point in time represented
341
- * 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
342
349
  * undefined if the object hasn't finished loading.
343
350
  *
344
351
  * @remarks
@@ -346,13 +353,37 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
346
353
  * of Automerge doesn't check types at runtime, so if you go back to an old set of heads
347
354
  * that doesn't match the heads here, Typescript will not save you.
348
355
  *
349
- * @returns An Automerge.Doc<T> at the point in time.
356
+ * @argument heads - The heads to view the document at. See history().
357
+ * @returns DocHandle<T> at the time of `heads`
350
358
  */
351
- view(heads: A.Heads): A.Doc<T> | undefined {
359
+ view(heads: UrlHeads): DocHandle<T> {
352
360
  if (!this.isReady()) {
353
- return undefined
361
+ throw new Error(
362
+ `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
363
+ )
364
+ }
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
354
373
  }
355
- return A.view(this.#doc, heads)
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
356
387
  }
357
388
 
358
389
  /**
@@ -360,19 +391,46 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
360
391
  * if applied.
361
392
  *
362
393
  * @remarks
363
- * We allow specifying both a from/to heads or just a single comparison point, in which case
364
- * 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)
365
398
  *
366
- * @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
367
401
  */
368
- diff(first: A.Heads, second?: A.Heads): A.Patch[] | undefined {
402
+ diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
369
403
  if (!this.isReady()) {
370
- return undefined
404
+ throw new Error(
405
+ `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
406
+ )
371
407
  }
372
- // We allow only one set of heads to be specified, in which case we use the doc's heads
373
- const from = second ? first : this.heads() || [] // because we guard above this should always have useful data
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
+ )
428
+ }
429
+
430
+ // Otherwise treat as heads
431
+ const from = second ? first : ((this.heads() || []) as UrlHeads)
374
432
  const to = second ? second : first
375
- return A.diff(this.#doc, from, to)
433
+ return A.diff(doc, decodeHeads(from), decodeHeads(to))
376
434
  }
377
435
 
378
436
  /**
@@ -390,11 +448,15 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
390
448
  if (!this.isReady()) {
391
449
  return undefined
392
450
  }
451
+
393
452
  if (!change) {
394
453
  change = this.heads()![0]
395
454
  }
396
455
  // we return undefined instead of null by convention in this API
397
- return A.inspectChange(this.#doc, change) || undefined
456
+ return (
457
+ A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
458
+ undefined
459
+ )
398
460
  }
399
461
 
400
462
  /**
@@ -417,16 +479,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
417
479
  }
418
480
 
419
481
  /**
420
- * 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.
421
483
  * @hidden
422
484
  */
423
- setRemoteHeads(storageId: StorageId, heads: A.Heads) {
485
+ setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
424
486
  this.#remoteHeads[storageId] = heads
425
487
  this.emit("remote-heads", { storageId, heads })
426
488
  }
427
489
 
428
490
  /** Returns the heads of the storageId. */
429
- getRemoteHeads(storageId: StorageId): A.Heads | undefined {
491
+ getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
430
492
  return this.#remoteHeads[storageId]
431
493
  }
432
494
 
@@ -451,6 +513,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
451
513
  `DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
452
514
  )
453
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
+
454
523
  this.#machine.send({
455
524
  type: UPDATE,
456
525
  payload: { callback: doc => A.change(doc, options, callback) },
@@ -462,22 +531,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
462
531
  * @returns A set of heads representing the concurrent change that was made.
463
532
  */
464
533
  changeAt(
465
- heads: A.Heads,
534
+ heads: UrlHeads,
466
535
  callback: A.ChangeFn<T>,
467
536
  options: A.ChangeOptions<T> = {}
468
- ): string[] | undefined {
537
+ ): UrlHeads[] | undefined {
469
538
  if (!this.isReady()) {
470
539
  throw new Error(
471
540
  `DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
472
541
  )
473
542
  }
474
- 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
475
549
  this.#machine.send({
476
550
  type: UPDATE,
477
551
  payload: {
478
552
  callback: doc => {
479
- const result = A.changeAt(doc, heads, options, callback)
480
- resultHeads = result.newHeads || undefined
553
+ const result = A.changeAt(doc, decodeHeads(heads), options, callback)
554
+ resultHeads = result.newHeads
555
+ ? encodeHeads(result.newHeads)
556
+ : undefined
481
557
  return result.newDoc
482
558
  },
483
559
  },
@@ -502,10 +578,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
502
578
  if (!this.isReady() || !otherHandle.isReady()) {
503
579
  throw new Error("Both handles must be ready to merge")
504
580
  }
505
- const mergingDoc = otherHandle.docSync()
506
- if (!mergingDoc) {
507
- 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
+ )
508
585
  }
586
+ const mergingDoc = otherHandle.doc()
509
587
 
510
588
  this.update(doc => {
511
589
  return A.merge(doc, mergingDoc)
@@ -513,14 +591,15 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
513
591
  }
514
592
 
515
593
  /**
516
- * Used in testing to mark this document as unavailable.
594
+ * Updates the internal state machine to mark the document unavailable.
517
595
  * @hidden
518
596
  */
519
597
  unavailable() {
520
598
  this.#machine.send({ type: DOC_UNAVAILABLE })
521
599
  }
522
600
 
523
- /** 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.
524
603
  * @hidden
525
604
  * */
526
605
  request() {
@@ -552,7 +631,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
552
631
  broadcast(message: unknown) {
553
632
  this.emit("ephemeral-message-outbound", {
554
633
  handle: this,
555
- data: encode(message),
634
+ data: new Uint8Array(encode(message)),
556
635
  })
557
636
  }
558
637
 
@@ -577,6 +656,9 @@ export type DocHandleOptions<T> =
577
656
  | {
578
657
  isNew?: false
579
658
 
659
+ // An optional point in time to lock the document to.
660
+ heads?: UrlHeads
661
+
580
662
  /** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
581
663
  timeoutDelay?: number
582
664
  }
@@ -588,7 +670,6 @@ export interface DocHandleEvents<T> {
588
670
  "heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
589
671
  change: (payload: DocHandleChangePayload<T>) => void
590
672
  delete: (payload: DocHandleDeletePayload<T>) => void
591
- unavailable: (payload: DocHandleUnavailablePayload<T>) => void
592
673
  "ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
593
674
  "ephemeral-message-outbound": (
594
675
  payload: DocHandleOutboundEphemeralMessagePayload<T>
@@ -640,7 +721,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
640
721
  /** Emitted when we have new remote heads for this document */
641
722
  export interface DocHandleRemoteHeadsPayload {
642
723
  storageId: StorageId
643
- heads: A.Heads
724
+ heads: UrlHeads
644
725
  }
645
726
 
646
727
  // STATE MACHINE TYPES & CONSTANTS
@@ -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
  }