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

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 (164) hide show
  1. package/dist/CollectionHandle.d.ts +14 -0
  2. package/dist/CollectionHandle.d.ts.map +1 -0
  3. package/dist/CollectionHandle.js +37 -0
  4. package/dist/DocHandle.d.ts +37 -6
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +64 -6
  7. package/dist/DocUrl.d.ts +47 -0
  8. package/dist/DocUrl.d.ts.map +1 -0
  9. package/dist/DocUrl.js +72 -0
  10. package/dist/EphemeralData.d.ts +20 -0
  11. package/dist/EphemeralData.d.ts.map +1 -0
  12. package/dist/EphemeralData.js +1 -0
  13. package/dist/Repo.d.ts +28 -7
  14. package/dist/Repo.d.ts.map +1 -1
  15. package/dist/Repo.js +142 -143
  16. package/dist/ferigan.d.ts +51 -0
  17. package/dist/ferigan.d.ts.map +1 -0
  18. package/dist/ferigan.js +98 -0
  19. package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
  20. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  21. package/dist/helpers/tests/storage-adapter-tests.js +19 -39
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/network/NetworkSubsystem.d.ts +1 -0
  26. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  27. package/dist/network/NetworkSubsystem.js +3 -0
  28. package/dist/network/messages.d.ts +7 -1
  29. package/dist/network/messages.d.ts.map +1 -1
  30. package/dist/network/messages.js +2 -1
  31. package/dist/src/DocHandle.d.ts +182 -0
  32. package/dist/src/DocHandle.d.ts.map +1 -0
  33. package/dist/src/DocHandle.js +405 -0
  34. package/dist/src/DocUrl.d.ts +49 -0
  35. package/dist/src/DocUrl.d.ts.map +1 -0
  36. package/dist/src/DocUrl.js +72 -0
  37. package/dist/src/EphemeralData.d.ts +19 -0
  38. package/dist/src/EphemeralData.d.ts.map +1 -0
  39. package/dist/src/EphemeralData.js +1 -0
  40. package/dist/src/Repo.d.ts +74 -0
  41. package/dist/src/Repo.d.ts.map +1 -0
  42. package/dist/src/Repo.js +208 -0
  43. package/dist/src/helpers/arraysAreEqual.d.ts +2 -0
  44. package/dist/src/helpers/arraysAreEqual.d.ts.map +1 -0
  45. package/dist/src/helpers/arraysAreEqual.js +2 -0
  46. package/dist/src/helpers/cbor.d.ts +4 -0
  47. package/dist/src/helpers/cbor.d.ts.map +1 -0
  48. package/dist/src/helpers/cbor.js +8 -0
  49. package/dist/src/helpers/eventPromise.d.ts +11 -0
  50. package/dist/src/helpers/eventPromise.d.ts.map +1 -0
  51. package/dist/src/helpers/eventPromise.js +7 -0
  52. package/dist/src/helpers/headsAreSame.d.ts +2 -0
  53. package/dist/src/helpers/headsAreSame.d.ts.map +1 -0
  54. package/dist/src/helpers/headsAreSame.js +4 -0
  55. package/dist/src/helpers/mergeArrays.d.ts +2 -0
  56. package/dist/src/helpers/mergeArrays.d.ts.map +1 -0
  57. package/dist/src/helpers/mergeArrays.js +15 -0
  58. package/dist/src/helpers/pause.d.ts +6 -0
  59. package/dist/src/helpers/pause.d.ts.map +1 -0
  60. package/dist/src/helpers/pause.js +10 -0
  61. package/dist/src/helpers/tests/network-adapter-tests.d.ts +21 -0
  62. package/dist/src/helpers/tests/network-adapter-tests.d.ts.map +1 -0
  63. package/dist/src/helpers/tests/network-adapter-tests.js +122 -0
  64. package/dist/src/helpers/withTimeout.d.ts +12 -0
  65. package/dist/src/helpers/withTimeout.d.ts.map +1 -0
  66. package/dist/src/helpers/withTimeout.js +24 -0
  67. package/dist/src/index.d.ts +53 -0
  68. package/dist/src/index.d.ts.map +1 -0
  69. package/dist/src/index.js +40 -0
  70. package/dist/src/network/NetworkAdapter.d.ts +26 -0
  71. package/dist/src/network/NetworkAdapter.d.ts.map +1 -0
  72. package/dist/src/network/NetworkAdapter.js +4 -0
  73. package/dist/src/network/NetworkSubsystem.d.ts +23 -0
  74. package/dist/src/network/NetworkSubsystem.d.ts.map +1 -0
  75. package/dist/src/network/NetworkSubsystem.js +120 -0
  76. package/dist/src/network/messages.d.ts +85 -0
  77. package/dist/src/network/messages.d.ts.map +1 -0
  78. package/dist/src/network/messages.js +23 -0
  79. package/dist/src/storage/StorageAdapter.d.ts +14 -0
  80. package/dist/src/storage/StorageAdapter.d.ts.map +1 -0
  81. package/dist/src/storage/StorageAdapter.js +1 -0
  82. package/dist/src/storage/StorageSubsystem.d.ts +12 -0
  83. package/dist/src/storage/StorageSubsystem.d.ts.map +1 -0
  84. package/dist/src/storage/StorageSubsystem.js +145 -0
  85. package/dist/src/synchronizer/CollectionSynchronizer.d.ts +25 -0
  86. package/dist/src/synchronizer/CollectionSynchronizer.d.ts.map +1 -0
  87. package/dist/src/synchronizer/CollectionSynchronizer.js +106 -0
  88. package/dist/src/synchronizer/DocSynchronizer.d.ts +29 -0
  89. package/dist/src/synchronizer/DocSynchronizer.d.ts.map +1 -0
  90. package/dist/src/synchronizer/DocSynchronizer.js +263 -0
  91. package/dist/src/synchronizer/Synchronizer.d.ts +9 -0
  92. package/dist/src/synchronizer/Synchronizer.d.ts.map +1 -0
  93. package/dist/src/synchronizer/Synchronizer.js +2 -0
  94. package/dist/src/types.d.ts +16 -0
  95. package/dist/src/types.d.ts.map +1 -0
  96. package/dist/src/types.js +1 -0
  97. package/dist/storage/StorageAdapter.d.ts +9 -0
  98. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  99. package/dist/storage/StorageAdapter.js +33 -0
  100. package/dist/storage/StorageSubsystem.d.ts +12 -2
  101. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  102. package/dist/storage/StorageSubsystem.js +42 -100
  103. package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -2
  104. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  105. package/dist/synchronizer/CollectionSynchronizer.js +28 -15
  106. package/dist/synchronizer/DocSynchronizer.d.ts +6 -5
  107. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  108. package/dist/synchronizer/DocSynchronizer.js +76 -178
  109. package/dist/synchronizer/Synchronizer.d.ts +11 -0
  110. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  111. package/dist/test/CollectionSynchronizer.test.d.ts +2 -0
  112. package/dist/test/CollectionSynchronizer.test.d.ts.map +1 -0
  113. package/dist/test/CollectionSynchronizer.test.js +57 -0
  114. package/dist/test/DocHandle.test.d.ts +2 -0
  115. package/dist/test/DocHandle.test.d.ts.map +1 -0
  116. package/dist/test/DocHandle.test.js +238 -0
  117. package/dist/test/DocSynchronizer.test.d.ts +2 -0
  118. package/dist/test/DocSynchronizer.test.d.ts.map +1 -0
  119. package/dist/test/DocSynchronizer.test.js +111 -0
  120. package/dist/test/Network.test.d.ts +2 -0
  121. package/dist/test/Network.test.d.ts.map +1 -0
  122. package/dist/test/Network.test.js +11 -0
  123. package/dist/test/Repo.test.d.ts +2 -0
  124. package/dist/test/Repo.test.d.ts.map +1 -0
  125. package/dist/test/Repo.test.js +568 -0
  126. package/dist/test/StorageSubsystem.test.d.ts +2 -0
  127. package/dist/test/StorageSubsystem.test.d.ts.map +1 -0
  128. package/dist/test/StorageSubsystem.test.js +56 -0
  129. package/dist/test/helpers/DummyNetworkAdapter.d.ts +9 -0
  130. package/dist/test/helpers/DummyNetworkAdapter.d.ts.map +1 -0
  131. package/dist/test/helpers/DummyNetworkAdapter.js +15 -0
  132. package/dist/test/helpers/DummyStorageAdapter.d.ts +16 -0
  133. package/dist/test/helpers/DummyStorageAdapter.d.ts.map +1 -0
  134. package/dist/test/helpers/DummyStorageAdapter.js +33 -0
  135. package/dist/test/helpers/generate-large-object.d.ts +5 -0
  136. package/dist/test/helpers/generate-large-object.d.ts.map +1 -0
  137. package/dist/test/helpers/generate-large-object.js +9 -0
  138. package/dist/test/helpers/getRandomItem.d.ts +2 -0
  139. package/dist/test/helpers/getRandomItem.d.ts.map +1 -0
  140. package/dist/test/helpers/getRandomItem.js +4 -0
  141. package/dist/test/types.d.ts +4 -0
  142. package/dist/test/types.d.ts.map +1 -0
  143. package/dist/test/types.js +1 -0
  144. package/package.json +3 -3
  145. package/src/CollectionHandle.ts +54 -0
  146. package/src/DocHandle.ts +80 -8
  147. package/src/Repo.ts +192 -183
  148. package/src/ferigan.ts +184 -0
  149. package/src/helpers/tests/storage-adapter-tests.ts +31 -62
  150. package/src/index.ts +2 -0
  151. package/src/network/NetworkSubsystem.ts +4 -0
  152. package/src/network/messages.ts +11 -2
  153. package/src/storage/StorageAdapter.ts +42 -0
  154. package/src/storage/StorageSubsystem.ts +59 -119
  155. package/src/synchronizer/CollectionSynchronizer.ts +34 -26
  156. package/src/synchronizer/DocSynchronizer.ts +84 -231
  157. package/src/synchronizer/Synchronizer.ts +14 -0
  158. package/test/CollectionSynchronizer.test.ts +4 -2
  159. package/test/DocHandle.test.ts +72 -13
  160. package/test/DocSynchronizer.test.ts +6 -1
  161. package/test/RemoteHeadsSubscriptions.test.ts +1 -1
  162. package/test/Repo.test.ts +225 -117
  163. package/test/StorageSubsystem.test.ts +20 -16
  164. package/test/remoteHeads.test.ts +1 -1
@@ -17,9 +17,10 @@ import {
17
17
  SyncMessage,
18
18
  isRequestMessage,
19
19
  } from "../network/messages.js"
20
- import { PeerId } from "../types.js"
20
+ import { AutomergeUrl, DocumentId, PeerId } from "../types.js"
21
21
  import { Synchronizer } from "./Synchronizer.js"
22
22
  import { throttle } from "../helpers/throttle.js"
23
+ import { parseAutomergeUrl } from "../AutomergeUrl.js"
23
24
 
24
25
  type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
25
26
 
@@ -30,7 +31,7 @@ type PendingMessage = {
30
31
 
31
32
  interface DocSynchronizerConfig {
32
33
  handle: DocHandle<unknown>
33
- onLoadSyncState?: (peerId: PeerId) => Promise<A.SyncState | undefined>
34
+ beelay: A.beelay.Beelay
34
35
  }
35
36
 
36
37
  /**
@@ -44,65 +45,56 @@ export class DocSynchronizer extends Synchronizer {
44
45
  /** Active peers */
45
46
  #peers: PeerId[] = []
46
47
 
47
- #pendingSyncStateCallbacks: Record<
48
- PeerId,
49
- ((syncState: A.SyncState) => void)[]
50
- > = {}
51
-
52
48
  #peerDocumentStatuses: Record<PeerId, PeerDocumentStatus> = {}
53
-
54
- /** Sync state for each peer we've communicated with (including inactive peers) */
55
- #syncStates: Record<PeerId, A.SyncState> = {}
56
-
57
- #pendingSyncMessages: Array<PendingMessage> = []
58
-
49
+ #lastSaveOffset: string | null = null
59
50
  #syncStarted = false
51
+ #beelay: A.beelay.Beelay
60
52
 
61
53
  #handle: DocHandle<unknown>
62
- #onLoadSyncState: (peerId: PeerId) => Promise<A.SyncState | undefined>
54
+ #docId: DocumentId
63
55
 
64
- constructor({ handle, onLoadSyncState }: DocSynchronizerConfig) {
56
+ constructor({ handle, beelay }: DocSynchronizerConfig) {
65
57
  super()
66
58
  this.#handle = handle
67
- this.#onLoadSyncState =
68
- onLoadSyncState ?? (() => Promise.resolve(undefined))
69
-
70
- const docId = handle.documentId.slice(0, 5)
71
- this.#log = debug(`automerge-repo:docsync:${docId}`)
59
+ this.#beelay = beelay
60
+ this.#docId = this.#handle.documentId
72
61
 
73
- handle.on(
74
- "change",
75
- throttle(() => this.#syncWithPeers(), this.syncDebounceRate)
76
- )
62
+ this.#log = debug(`automerge-repo:docsync:${this.#handle.documentId}`)
77
63
 
78
64
  handle.on("ephemeral-message-outbound", payload =>
79
65
  this.#broadcastToPeers(payload)
80
66
  )
81
67
 
82
- // Process pending sync messages immediately after the handle becomes ready.
83
- void (async () => {
84
- await handle.doc([READY, REQUESTING])
85
- this.#processAllPendingSyncMessages()
86
- })()
68
+ handle.on("change", changeInfo => {
69
+ const newLinks = changeInfo.patches
70
+ .map(patch => {
71
+ if (patch.action === "put") {
72
+ if (patch.value instanceof A.Link) {
73
+ return patch.value
74
+ }
75
+ }
76
+ return null
77
+ })
78
+ .filter(v => v != null)
79
+ for (const link of newLinks) {
80
+ const { documentId: target } = parseAutomergeUrl(
81
+ link.target as AutomergeUrl
82
+ )
83
+ this.#beelay.addLink({ from: this.#handle.documentId, to: target })
84
+ }
85
+ })
87
86
  }
88
87
 
89
88
  get peerStates() {
90
89
  return this.#peerDocumentStatuses
91
90
  }
92
91
 
93
- get documentId() {
94
- return this.#handle.documentId
92
+ get documentId(): DocumentId {
93
+ return this.#docId
95
94
  }
96
95
 
97
96
  /// PRIVATE
98
97
 
99
- async #syncWithPeers() {
100
- this.#log(`syncWithPeers`)
101
- const doc = await this.#handle.doc()
102
- if (doc === undefined) return
103
- this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
104
- }
105
-
106
98
  async #broadcastToPeers({
107
99
  data,
108
100
  }: DocHandleOutboundEphemeralMessagePayload<unknown>) {
@@ -116,110 +108,12 @@ export class DocSynchronizer extends Synchronizer {
116
108
  const message: MessageContents<EphemeralMessage> = {
117
109
  type: "ephemeral",
118
110
  targetId: peerId,
119
- documentId: this.#handle.documentId,
111
+ documentId: this.documentId,
120
112
  data,
121
113
  }
122
114
  this.emit("message", message)
123
115
  }
124
116
 
125
- #withSyncState(peerId: PeerId, callback: (syncState: A.SyncState) => void) {
126
- this.#addPeer(peerId)
127
-
128
- if (!(peerId in this.#peerDocumentStatuses)) {
129
- this.#peerDocumentStatuses[peerId] = "unknown"
130
- }
131
-
132
- const syncState = this.#syncStates[peerId]
133
- if (syncState) {
134
- callback(syncState)
135
- return
136
- }
137
-
138
- let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
139
- if (!pendingCallbacks) {
140
- this.#onLoadSyncState(peerId)
141
- .then(syncState => {
142
- this.#initSyncState(peerId, syncState ?? A.initSyncState())
143
- })
144
- .catch(err => {
145
- this.#log(`Error loading sync state for ${peerId}: ${err}`)
146
- })
147
- pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = []
148
- }
149
-
150
- pendingCallbacks.push(callback)
151
- }
152
-
153
- #addPeer(peerId: PeerId) {
154
- if (!this.#peers.includes(peerId)) {
155
- this.#peers.push(peerId)
156
- this.emit("open-doc", { documentId: this.documentId, peerId })
157
- }
158
- }
159
-
160
- #initSyncState(peerId: PeerId, syncState: A.SyncState) {
161
- const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
162
- if (pendingCallbacks) {
163
- for (const callback of pendingCallbacks) {
164
- callback(syncState)
165
- }
166
- }
167
-
168
- delete this.#pendingSyncStateCallbacks[peerId]
169
-
170
- this.#syncStates[peerId] = syncState
171
- }
172
-
173
- #setSyncState(peerId: PeerId, syncState: A.SyncState) {
174
- this.#syncStates[peerId] = syncState
175
-
176
- this.emit("sync-state", {
177
- peerId,
178
- syncState,
179
- documentId: this.#handle.documentId,
180
- })
181
- }
182
-
183
- #sendSyncMessage(peerId: PeerId, doc: A.Doc<unknown>) {
184
- this.#log(`sendSyncMessage ->${peerId}`)
185
-
186
- this.#withSyncState(peerId, syncState => {
187
- const [newSyncState, message] = A.generateSyncMessage(doc, syncState)
188
- if (message) {
189
- this.#setSyncState(peerId, newSyncState)
190
- const isNew = A.getHeads(doc).length === 0
191
-
192
- if (
193
- !this.#handle.isReady() &&
194
- isNew &&
195
- newSyncState.sharedHeads.length === 0 &&
196
- !Object.values(this.#peerDocumentStatuses).includes("has") &&
197
- this.#peerDocumentStatuses[peerId] === "unknown"
198
- ) {
199
- // we don't have the document (or access to it), so we request it
200
- this.emit("message", {
201
- type: "request",
202
- targetId: peerId,
203
- documentId: this.#handle.documentId,
204
- data: message,
205
- } as RequestMessage)
206
- } else {
207
- this.emit("message", {
208
- type: "sync",
209
- targetId: peerId,
210
- data: message,
211
- documentId: this.#handle.documentId,
212
- } as SyncMessage)
213
- }
214
-
215
- // if we have sent heads, then the peer now has or will have the document
216
- if (!isNew) {
217
- this.#peerDocumentStatuses[peerId] = "has"
218
- }
219
- }
220
- })
221
- }
222
-
223
117
  /// PUBLIC
224
118
 
225
119
  hasPeer(peerId: PeerId) {
@@ -227,66 +121,72 @@ export class DocSynchronizer extends Synchronizer {
227
121
  }
228
122
 
229
123
  beginSync(peerIds: PeerId[]) {
230
- const noPeersWithDocument = peerIds.every(
231
- peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
232
- )
233
-
234
- // At this point if we don't have anything in our storage, we need to use an empty doc to sync
235
- // with; but we don't want to surface that state to the front end
124
+ this.#log(`beginSync: ${peerIds.join(", ")}`)
236
125
 
237
126
  const docPromise = this.#handle
238
- .doc([READY, REQUESTING, UNAVAILABLE])
127
+ .whenReady([READY, REQUESTING, UNAVAILABLE])
239
128
  .then(doc => {
240
- // we register out peers first, then say that sync has started
241
129
  this.#syncStarted = true
242
130
  this.#checkDocUnavailable()
243
-
244
- const wasUnavailable = doc === undefined
245
- if (wasUnavailable && noPeersWithDocument) {
246
- return
247
- }
248
-
249
- // If the doc is unavailable we still need a blank document to generate
250
- // the sync message from
251
- return doc ?? A.init<unknown>()
252
131
  })
253
-
254
- this.#log(`beginSync: ${peerIds.join(", ")}`)
132
+ // TODO: handle this error
133
+ .catch(() => {})
255
134
 
256
135
  peerIds.forEach(peerId => {
257
- this.#withSyncState(peerId, syncState => {
258
- // HACK: if we have a sync state already, we round-trip it through the encoding system to make
259
- // sure state is preserved. This prevents an infinite loop caused by failed attempts to send
260
- // messages during disconnection.
261
- // TODO: cover that case with a test and remove this hack
262
- const reparsedSyncState = A.decodeSyncState(
263
- A.encodeSyncState(syncState)
264
- )
265
- this.#setSyncState(peerId, reparsedSyncState)
136
+ if (!this.#peers.includes(peerId)) {
137
+ this.#peers.push(peerId)
138
+ } else {
139
+ return
140
+ }
141
+ this.#peerDocumentStatuses[peerId] = "unknown"
266
142
 
267
- docPromise
268
- .then(doc => {
269
- if (doc) {
270
- this.#sendSyncMessage(peerId, doc)
143
+ docPromise.then(() => {
144
+ this.#syncStarted = true
145
+ this.#log(`beginning sync with ${peerId} for doc: ${this.documentId}`)
146
+ this.#beelay
147
+ .syncDoc(this.documentId, peerId)
148
+ .then(({ snapshot, found }) => {
149
+ this.#peerDocumentStatuses[peerId] = found ? "has" : "unavailable"
150
+ // this.#log("synced snapshot: ", snapshot)
151
+ if (found) {
152
+ this.#beelay.loadDocument(this.#docId).then(commitOrBundles => {
153
+ if (commitOrBundles != null) {
154
+ this.#handle?.update(d => {
155
+ let doc = d
156
+ for (const commitOrBundle of commitOrBundles) {
157
+ doc = A.loadIncremental(doc, commitOrBundle.contents)
158
+ }
159
+ return doc
160
+ })
161
+ this.#checkDocUnavailable()
162
+ }
163
+ })
164
+ } else {
165
+ this.#checkDocUnavailable()
271
166
  }
272
- })
273
- .catch(err => {
274
- this.#log(`Error loading doc for ${peerId}: ${err}`)
167
+ this.#beelay.listen(peerId, snapshot)
275
168
  })
276
169
  })
277
170
  })
278
171
  }
279
172
 
173
+ peerWantsDocument(peerId: PeerId) {
174
+ this.#peerDocumentStatuses[peerId] = "wants"
175
+ if (!this.#peers.includes(peerId)) {
176
+ this.beginSync([peerId as PeerId])
177
+ }
178
+ }
179
+
280
180
  endSync(peerId: PeerId) {
281
181
  this.#log(`removing peer ${peerId}`)
282
182
  this.#peers = this.#peers.filter(p => p !== peerId)
183
+ this.#beelay.cancelListens(peerId)
283
184
  }
284
185
 
285
186
  receiveMessage(message: RepoMessage) {
286
187
  switch (message.type) {
287
188
  case "sync":
288
189
  case "request":
289
- this.receiveSyncMessage(message)
290
190
  break
291
191
  case "ephemeral":
292
192
  this.receiveEphemeralMessage(message)
@@ -301,7 +201,7 @@ export class DocSynchronizer extends Synchronizer {
301
201
  }
302
202
 
303
203
  receiveEphemeralMessage(message: EphemeralMessage) {
304
- if (message.documentId !== this.#handle.documentId)
204
+ if (message.documentId !== this.documentId)
305
205
  throw new Error(`channelId doesn't match documentId`)
306
206
 
307
207
  const { senderId, data } = message
@@ -313,7 +213,6 @@ export class DocSynchronizer extends Synchronizer {
313
213
  senderId,
314
214
  message: contents,
315
215
  })
316
-
317
216
  this.#peers.forEach(peerId => {
318
217
  if (peerId === senderId) return
319
218
  this.emit("message", {
@@ -323,50 +222,7 @@ export class DocSynchronizer extends Synchronizer {
323
222
  })
324
223
  }
325
224
 
326
- receiveSyncMessage(message: SyncMessage | RequestMessage) {
327
- if (message.documentId !== this.#handle.documentId)
328
- throw new Error(`channelId doesn't match documentId`)
329
-
330
- // We need to block receiving the syncMessages until we've checked local storage
331
- if (!this.#handle.inState([READY, REQUESTING, UNAVAILABLE])) {
332
- this.#pendingSyncMessages.push({ message, received: new Date() })
333
- return
334
- }
335
-
336
- this.#processAllPendingSyncMessages()
337
- this.#processSyncMessage(message)
338
- }
339
-
340
- #processSyncMessage(message: SyncMessage | RequestMessage) {
341
- if (isRequestMessage(message)) {
342
- this.#peerDocumentStatuses[message.senderId] = "wants"
343
- }
344
-
345
- this.#checkDocUnavailable()
346
-
347
- // if the message has heads, then the peer has the document
348
- if (A.decodeSyncMessage(message.data).heads.length > 0) {
349
- this.#peerDocumentStatuses[message.senderId] = "has"
350
- }
351
-
352
- this.#withSyncState(message.senderId, syncState => {
353
- this.#handle.update(doc => {
354
- const [newDoc, newSyncState] = A.receiveSyncMessage(
355
- doc,
356
- syncState,
357
- message.data
358
- )
359
-
360
- this.#setSyncState(message.senderId, newSyncState)
361
-
362
- // respond to just this peer (as required)
363
- this.#sendSyncMessage(message.senderId, doc)
364
- return newDoc
365
- })
366
-
367
- this.#checkDocUnavailable()
368
- })
369
- }
225
+ receiveSyncMessage(message: SyncMessage | RequestMessage) {}
370
226
 
371
227
  #checkDocUnavailable() {
372
228
  // if we know none of the peers have the document, tell all our peers that we don't either
@@ -384,28 +240,25 @@ export class DocSynchronizer extends Synchronizer {
384
240
  .forEach(peerId => {
385
241
  const message: MessageContents<DocumentUnavailableMessage> = {
386
242
  type: "doc-unavailable",
387
- documentId: this.#handle.documentId,
243
+ documentId: this.documentId,
388
244
  targetId: peerId,
389
245
  }
390
246
  this.emit("message", message)
391
247
  })
392
248
 
393
- this.#handle.unavailable()
394
- }
395
- }
396
-
397
- #processAllPendingSyncMessages() {
398
- for (const message of this.#pendingSyncMessages) {
399
- this.#processSyncMessage(message.message)
249
+ if (this.#handle) {
250
+ this.#handle.unavailable()
251
+ }
400
252
  }
401
-
402
- this.#pendingSyncMessages = []
403
253
  }
404
254
 
405
- metrics(): { peers: PeerId[]; size: { numOps: number; numChanges: number } } {
255
+ metrics(): {
256
+ peers: PeerId[]
257
+ size: { numOps: number; numChanges: number } | undefined
258
+ } {
406
259
  return {
407
260
  peers: this.#peers,
408
- size: this.#handle.metrics(),
261
+ size: this.#handle?.metrics(),
409
262
  }
410
263
  }
411
264
  }
@@ -15,6 +15,7 @@ export interface SynchronizerEvents {
15
15
  message: (payload: MessageContents) => void
16
16
  "sync-state": (payload: SyncStatePayload) => void
17
17
  "open-doc": (arg: OpenDocMessage) => void
18
+ metrics: (arg: DocSyncMetrics) => void
18
19
  }
19
20
 
20
21
  /** Notify the repo that the sync state has changed */
@@ -23,3 +24,16 @@ export interface SyncStatePayload {
23
24
  documentId: DocumentId
24
25
  syncState: SyncState
25
26
  }
27
+
28
+ export type DocSyncMetrics =
29
+ | {
30
+ type: "receive-sync-message"
31
+ documentId: DocumentId
32
+ durationMillis: number
33
+ numOps: number
34
+ numChanges: number
35
+ }
36
+ | {
37
+ type: "doc-denied"
38
+ documentId: DocumentId
39
+ }
@@ -2,14 +2,16 @@ import assert from "assert"
2
2
  import { beforeEach, describe, it } from "vitest"
3
3
  import { PeerId, Repo, SyncMessage } from "../src/index.js"
4
4
  import { CollectionSynchronizer } from "../src/synchronizer/CollectionSynchronizer.js"
5
+ import { next as Automerge } from "@automerge/automerge"
5
6
 
6
- describe("CollectionSynchronizer", () => {
7
+ describe.skip("CollectionSynchronizer", () => {
7
8
  let repo: Repo
8
9
  let synchronizer: CollectionSynchronizer
10
+ let beelay: Automerge.beelay.Beelay
9
11
 
10
12
  beforeEach(() => {
11
13
  repo = new Repo()
12
- synchronizer = new CollectionSynchronizer(repo)
14
+ synchronizer = new CollectionSynchronizer(beelay, repo)
13
15
  })
14
16
 
15
17
  it("is not null", async () => {
@@ -7,6 +7,7 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
7
7
  import { pause } from "../src/helpers/pause.js"
8
8
  import { DocHandle, DocHandleChangePayload } from "../src/index.js"
9
9
  import { TestDoc } from "./types.js"
10
+ import { UNLOADED } from "../src/DocHandle.js"
10
11
 
11
12
  describe("DocHandle", () => {
12
13
  const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
@@ -116,19 +117,6 @@ describe("DocHandle", () => {
116
117
  assert.deepEqual(view, { foo: "one" })
117
118
  })
118
119
 
119
- it("should return a commit from the history", async () => {
120
- const handle = setup()
121
- handle.change(d => (d.foo = "zero"))
122
- handle.change(d => (d.foo = "one"))
123
- handle.change(d => (d.foo = "two"))
124
- handle.change(d => (d.foo = "three"))
125
- assert.equal(handle.isReady(), true)
126
-
127
- const history = handle.history()
128
- const view = handle.view(history[1])
129
- assert.deepEqual(view, { foo: "one" })
130
- })
131
-
132
120
  it("should return diffs", async () => {
133
121
  const handle = setup()
134
122
  handle.change(d => (d.foo = "zero"))
@@ -166,6 +154,34 @@ describe("DocHandle", () => {
166
154
  ])
167
155
  })
168
156
 
157
+ it("should allow direct access to decoded changes", async () => {
158
+ const handle = setup()
159
+ const time = Date.now()
160
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage" })
161
+ assert.equal(handle.isReady(), true)
162
+
163
+ const metadata = handle.metadata()
164
+ assert.deepEqual(metadata.message, "commitMessage")
165
+ // NOTE: I'm not testing time because of https://github.com/automerge/automerge/issues/965
166
+ // but it does round-trip successfully!
167
+ })
168
+
169
+ it("should allow direct access to a specific decoded change", async () => {
170
+ const handle = setup()
171
+ const time = Date.now()
172
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage" })
173
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage2" })
174
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage3" })
175
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage4" })
176
+ assert.equal(handle.isReady(), true)
177
+
178
+ const history = handle.history()
179
+ const metadata = handle.metadata(history[0][0])
180
+ assert.deepEqual(metadata.message, "commitMessage")
181
+ // NOTE: I'm not testing time because of https://github.com/automerge/automerge/issues/965
182
+ // but it does round-trip successfully!
183
+ })
184
+
169
185
  /**
170
186
  * Once there's a Repo#stop API this case should be covered in accompanying
171
187
  * tests and the following test removed.
@@ -407,6 +423,49 @@ describe("DocHandle", () => {
407
423
  assert.equal(handle.isDeleted(), true)
408
424
  })
409
425
 
426
+ it("should clear document reference when unloaded", async () => {
427
+ const handle = setup()
428
+
429
+ handle.change(doc => {
430
+ doc.foo = "bar"
431
+ })
432
+ const doc = await handle.doc()
433
+ assert.equal(doc?.foo, "bar")
434
+
435
+ handle.unload()
436
+ assert.equal(handle.isUnloaded(), true)
437
+
438
+ const clearedDoc = await handle.doc([UNLOADED])
439
+ assert.notEqual(clearedDoc?.foo, "bar")
440
+ })
441
+
442
+ it("should allow reloading after unloading", async () => {
443
+ const handle = setup()
444
+
445
+ handle.change(doc => {
446
+ doc.foo = "bar"
447
+ })
448
+ const doc = await handle.doc()
449
+ assert.equal(doc?.foo, "bar")
450
+
451
+ handle.unload()
452
+
453
+ // reload to transition from unloaded to loading
454
+ handle.reload()
455
+
456
+ // simulate requesting from the network
457
+ handle.request()
458
+
459
+ // simulate updating from the network
460
+ handle.update(doc => {
461
+ return A.change(doc, d => (d.foo = "bar"))
462
+ })
463
+
464
+ const reloadedDoc = await handle.doc()
465
+ assert.equal(handle.isReady(), true)
466
+ assert.equal(reloadedDoc?.foo, "bar")
467
+ })
468
+
410
469
  it("should allow changing at old heads", async () => {
411
470
  const handle = setup()
412
471
 
@@ -16,9 +16,10 @@ const alice = "alice" as PeerId
16
16
  const bob = "bob" as PeerId
17
17
  const charlie = "charlie" as PeerId
18
18
 
19
- describe("DocSynchronizer", () => {
19
+ describe.skip("DocSynchronizer", () => {
20
20
  let handle: DocHandle<TestDoc>
21
21
  let docSynchronizer: DocSynchronizer
22
+ let beelay: Automerge.beelay.Beelay
22
23
 
23
24
  const setup = () => {
24
25
  const docId = parseAutomergeUrl(generateAutomergeUrl()).documentId
@@ -26,6 +27,7 @@ describe("DocSynchronizer", () => {
26
27
  handle.doneLoading()
27
28
 
28
29
  docSynchronizer = new DocSynchronizer({
30
+ beelay,
29
31
  handle: handle as DocHandle<unknown>,
30
32
  })
31
33
 
@@ -106,6 +108,7 @@ describe("DocSynchronizer", () => {
106
108
 
107
109
  const handle = new DocHandle<TestDoc>(docId, { isNew: false })
108
110
  docSynchronizer = new DocSynchronizer({
111
+ beelay,
109
112
  handle: handle as DocHandle<unknown>,
110
113
  })
111
114
  docSynchronizer.beginSync([alice])
@@ -120,6 +123,7 @@ describe("DocSynchronizer", () => {
120
123
 
121
124
  const bobHandle = new DocHandle<TestDoc>(docId, { isNew: false })
122
125
  const bobDocSynchronizer = new DocSynchronizer({
126
+ beelay,
123
127
  handle: bobHandle as DocHandle<unknown>,
124
128
  })
125
129
  bobDocSynchronizer.beginSync([alice])
@@ -129,6 +133,7 @@ describe("DocSynchronizer", () => {
129
133
  const aliceHandle = new DocHandle<TestDoc>(docId, { isNew: false })
130
134
  const aliceDocSynchronizer = new DocSynchronizer({
131
135
  handle: aliceHandle as DocHandle<unknown>,
136
+ beelay,
132
137
  })
133
138
  aliceHandle.request()
134
139
 
@@ -10,7 +10,7 @@ import {
10
10
  } from "../src/network/messages.js"
11
11
  import { collectMessages } from "./helpers/collectMessages.js"
12
12
 
13
- describe("RepoHeadsSubscriptions", () => {
13
+ describe.skip("RepoHeadsSubscriptions", () => {
14
14
  const storageA = "remote-a" as StorageId
15
15
  const storageB = "remote-b" as StorageId
16
16
  const storageC = "remote-c" as StorageId