@automerge/automerge-repo 1.0.19 → 1.1.0-alpha.13

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 (68) hide show
  1. package/README.md +12 -7
  2. package/dist/AutomergeUrl.js +2 -2
  3. package/dist/DocHandle.d.ts +6 -5
  4. package/dist/DocHandle.d.ts.map +1 -1
  5. package/dist/DocHandle.js +7 -7
  6. package/dist/RemoteHeadsSubscriptions.d.ts +42 -0
  7. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -0
  8. package/dist/RemoteHeadsSubscriptions.js +284 -0
  9. package/dist/Repo.d.ts +29 -2
  10. package/dist/Repo.d.ts.map +1 -1
  11. package/dist/Repo.js +168 -9
  12. package/dist/helpers/debounce.js +1 -1
  13. package/dist/helpers/pause.d.ts.map +1 -1
  14. package/dist/helpers/pause.js +2 -0
  15. package/dist/helpers/throttle.js +1 -1
  16. package/dist/helpers/withTimeout.d.ts.map +1 -1
  17. package/dist/helpers/withTimeout.js +2 -0
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/network/NetworkAdapter.d.ts +15 -1
  22. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  23. package/dist/network/NetworkAdapter.js +3 -1
  24. package/dist/network/NetworkSubsystem.d.ts +4 -2
  25. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  26. package/dist/network/NetworkSubsystem.js +13 -7
  27. package/dist/network/messages.d.ts +68 -35
  28. package/dist/network/messages.d.ts.map +1 -1
  29. package/dist/network/messages.js +9 -7
  30. package/dist/storage/StorageSubsystem.d.ts +5 -3
  31. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  32. package/dist/storage/StorageSubsystem.js +23 -5
  33. package/dist/storage/keyHash.d.ts.map +1 -1
  34. package/dist/storage/types.d.ts +4 -0
  35. package/dist/storage/types.d.ts.map +1 -1
  36. package/dist/synchronizer/CollectionSynchronizer.d.ts +2 -2
  37. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  38. package/dist/synchronizer/CollectionSynchronizer.js +9 -3
  39. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  40. package/dist/synchronizer/DocSynchronizer.js +20 -17
  41. package/dist/synchronizer/Synchronizer.d.ts +12 -3
  42. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  43. package/package.json +6 -6
  44. package/src/AutomergeUrl.ts +2 -2
  45. package/src/DocHandle.ts +10 -9
  46. package/src/RemoteHeadsSubscriptions.ts +375 -0
  47. package/src/Repo.ts +241 -16
  48. package/src/helpers/debounce.ts +1 -1
  49. package/src/helpers/pause.ts +4 -0
  50. package/src/helpers/throttle.ts +1 -1
  51. package/src/helpers/withTimeout.ts +2 -0
  52. package/src/index.ts +3 -1
  53. package/src/network/NetworkAdapter.ts +19 -2
  54. package/src/network/NetworkSubsystem.ts +21 -9
  55. package/src/network/messages.ts +88 -50
  56. package/src/storage/StorageSubsystem.ts +30 -7
  57. package/src/storage/keyHash.ts +2 -0
  58. package/src/storage/types.ts +3 -0
  59. package/src/synchronizer/CollectionSynchronizer.ts +13 -5
  60. package/src/synchronizer/DocSynchronizer.ts +27 -27
  61. package/src/synchronizer/Synchronizer.ts +13 -3
  62. package/test/DocHandle.test.ts +0 -17
  63. package/test/RemoteHeadsSubscriptions.test.ts +353 -0
  64. package/test/Repo.test.ts +108 -17
  65. package/test/StorageSubsystem.test.ts +29 -7
  66. package/test/helpers/waitForMessages.ts +22 -0
  67. package/test/remoteHeads.test.ts +260 -0
  68. package/.eslintrc +0 -28
@@ -20,7 +20,6 @@ import {
20
20
  import { PeerId } from "../types.js"
21
21
  import { Synchronizer } from "./Synchronizer.js"
22
22
  import { throttle } from "../helpers/throttle.js"
23
- import { headsAreSame } from "../helpers/headsAreSame.js"
24
23
 
25
24
  type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants"
26
25
 
@@ -124,9 +123,7 @@ export class DocSynchronizer extends Synchronizer {
124
123
  }
125
124
 
126
125
  #withSyncState(peerId: PeerId, callback: (syncState: A.SyncState) => void) {
127
- if (!this.#peers.includes(peerId)) {
128
- this.#peers.push(peerId)
129
- }
126
+ this.#addPeer(peerId)
130
127
 
131
128
  if (!(peerId in this.#peerDocumentStatuses)) {
132
129
  this.#peerDocumentStatuses[peerId] = "unknown"
@@ -140,15 +137,26 @@ export class DocSynchronizer extends Synchronizer {
140
137
 
141
138
  let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
142
139
  if (!pendingCallbacks) {
143
- this.#onLoadSyncState(peerId).then(syncState => {
144
- this.#initSyncState(peerId, syncState ?? A.initSyncState())
145
- })
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
+ })
146
147
  pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = []
147
148
  }
148
149
 
149
150
  pendingCallbacks.push(callback)
150
151
  }
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
+
152
160
  #initSyncState(peerId: PeerId, syncState: A.SyncState) {
153
161
  const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
154
162
  if (pendingCallbacks) {
@@ -163,20 +171,8 @@ export class DocSynchronizer extends Synchronizer {
163
171
  }
164
172
 
165
173
  #setSyncState(peerId: PeerId, syncState: A.SyncState) {
166
- const previousSyncState = this.#syncStates[peerId]
167
-
168
174
  this.#syncStates[peerId] = syncState
169
175
 
170
- const haveTheirSyncedHeadsChanged =
171
- syncState.theirHeads &&
172
- (!previousSyncState ||
173
- !previousSyncState.theirHeads ||
174
- !headsAreSame(previousSyncState.theirHeads, syncState.theirHeads))
175
-
176
- if (haveTheirSyncedHeadsChanged) {
177
- this.#handle.setRemoteHeads(peerId, syncState.theirHeads)
178
- }
179
-
180
176
  this.emit("sync-state", {
181
177
  peerId,
182
178
  syncState,
@@ -268,11 +264,15 @@ export class DocSynchronizer extends Synchronizer {
268
264
  )
269
265
  this.#setSyncState(peerId, reparsedSyncState)
270
266
 
271
- docPromise.then(doc => {
272
- if (doc) {
273
- this.#sendSyncMessage(peerId, doc)
274
- }
275
- })
267
+ docPromise
268
+ .then(doc => {
269
+ if (doc) {
270
+ this.#sendSyncMessage(peerId, doc)
271
+ }
272
+ })
273
+ .catch(err => {
274
+ this.#log(`Error loading doc for ${peerId}: ${err}`)
275
+ })
276
276
  })
277
277
  })
278
278
  }
@@ -334,10 +334,10 @@ export class DocSynchronizer extends Synchronizer {
334
334
  }
335
335
 
336
336
  this.#processAllPendingSyncMessages()
337
- this.#processSyncMessage(message, new Date())
337
+ this.#processSyncMessage(message)
338
338
  }
339
339
 
340
- #processSyncMessage(message: SyncMessage | RequestMessage, received: Date) {
340
+ #processSyncMessage(message: SyncMessage | RequestMessage) {
341
341
  if (isRequestMessage(message)) {
342
342
  this.#peerDocumentStatuses[message.senderId] = "wants"
343
343
  }
@@ -396,7 +396,7 @@ export class DocSynchronizer extends Synchronizer {
396
396
 
397
397
  #processAllPendingSyncMessages() {
398
398
  for (const message of this.#pendingSyncMessages) {
399
- this.#processSyncMessage(message.message, message.received)
399
+ this.#processSyncMessage(message.message)
400
400
  }
401
401
 
402
402
  this.#pendingSyncMessages = []
@@ -1,15 +1,25 @@
1
1
  import { EventEmitter } from "eventemitter3"
2
2
  import {
3
3
  MessageContents,
4
+ OpenDocMessage,
4
5
  RepoMessage,
5
- SyncStateMessage,
6
6
  } from "../network/messages.js"
7
+ import { SyncState } from "@automerge/automerge"
8
+ import { PeerId, DocumentId } from "../types.js"
7
9
 
8
10
  export abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
9
11
  abstract receiveMessage(message: RepoMessage): void
10
12
  }
11
13
 
12
14
  export interface SynchronizerEvents {
13
- message: (arg: MessageContents) => void
14
- "sync-state": (arg: SyncStateMessage) => void
15
+ message: (payload: MessageContents) => void
16
+ "sync-state": (payload: SyncStatePayload) => void
17
+ "open-doc": (arg: OpenDocMessage) => void
18
+ }
19
+
20
+ /** Notify the repo that the sync state has changed */
21
+ export interface SyncStatePayload {
22
+ peerId: PeerId
23
+ documentId: DocumentId
24
+ syncState: SyncState
15
25
  }
@@ -303,23 +303,6 @@ describe("DocHandle", () => {
303
303
  assert(wasBar, "foo should have been bar as we changed at the old heads")
304
304
  })
305
305
 
306
- it("should allow to listen for remote head changes and manually read remote heads", async () => {
307
- const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
308
- const bob = "bob" as PeerId
309
-
310
- const remoteHeadsMessagePromise = eventPromise(handle, "remote-heads")
311
-
312
- handle.setRemoteHeads(bob, [])
313
-
314
- const remoteHeadsMessage = await remoteHeadsMessagePromise
315
-
316
- assert.strictEqual(remoteHeadsMessage.peerId, bob)
317
- assert.deepStrictEqual(remoteHeadsMessage.heads, [])
318
-
319
- // read remote heads manually
320
- assert.deepStrictEqual(handle.getRemoteHeads(bob), [])
321
- })
322
-
323
306
  describe("ephemeral messaging", () => {
324
307
  it("can broadcast a message for the network to send out", async () => {
325
308
  const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
@@ -0,0 +1,353 @@
1
+ import * as A from "@automerge/automerge"
2
+ import assert from "assert"
3
+ import { describe, it } from "vitest"
4
+ import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
5
+ import { RemoteHeadsSubscriptions } from "../src/RemoteHeadsSubscriptions.js"
6
+ import { PeerId, StorageId } from "../src/index.js"
7
+ import {
8
+ RemoteHeadsChanged,
9
+ RemoteSubscriptionControlMessage,
10
+ } from "../src/network/messages.js"
11
+ import { waitForMessages } from "./helpers/waitForMessages.js"
12
+
13
+ describe("RepoHeadsSubscriptions", () => {
14
+ const storageA = "remote-a" as StorageId
15
+ const storageB = "remote-b" as StorageId
16
+ const storageC = "remote-c" as StorageId
17
+ const storageD = "remote-d" as StorageId
18
+ const peerA = "peer-a" as PeerId
19
+ const peerB = "peer-b" as PeerId
20
+ const peerC = "peer-c" as PeerId
21
+ const peerD = "peer-d" as PeerId
22
+
23
+ const { documentId: docA } = parseAutomergeUrl(generateAutomergeUrl())
24
+ const { documentId: docB } = parseAutomergeUrl(generateAutomergeUrl())
25
+ const { documentId: docC } = parseAutomergeUrl(generateAutomergeUrl())
26
+
27
+ const docAHeadsChangedForStorageB: RemoteHeadsChanged = {
28
+ type: "remote-heads-changed",
29
+ senderId: peerD,
30
+ targetId: peerA,
31
+ documentId: docA,
32
+ newHeads: {
33
+ [storageB]: {
34
+ heads: [],
35
+ timestamp: Date.now(),
36
+ },
37
+ },
38
+ }
39
+
40
+ const docBHeadsChangedForStorageB: RemoteHeadsChanged = {
41
+ type: "remote-heads-changed",
42
+ senderId: peerD,
43
+ targetId: peerA,
44
+ documentId: docB,
45
+ newHeads: {
46
+ [storageB]: {
47
+ heads: [],
48
+ timestamp: Date.now(),
49
+ },
50
+ },
51
+ }
52
+
53
+ const docBHeads = A.getHeads(
54
+ A.change(A.init(), doc => {
55
+ ;(doc as any).foo = "123"
56
+ })
57
+ )
58
+
59
+ const docBHeadsChangedForStorageB2: RemoteHeadsChanged = {
60
+ type: "remote-heads-changed",
61
+ senderId: peerD,
62
+ targetId: peerA,
63
+ documentId: docB,
64
+ newHeads: {
65
+ [storageB]: {
66
+ heads: docBHeads,
67
+ timestamp: Date.now() + 1,
68
+ },
69
+ },
70
+ }
71
+
72
+ const subscribePeerCToStorageB: RemoteSubscriptionControlMessage = {
73
+ type: "remote-subscription-change",
74
+ senderId: peerC,
75
+ targetId: peerA,
76
+ add: [storageB],
77
+ }
78
+
79
+ const unsubscribePeerCFromStorageB: RemoteSubscriptionControlMessage = {
80
+ type: "remote-subscription-change",
81
+ senderId: peerC,
82
+ targetId: peerA,
83
+ remove: [storageB],
84
+ }
85
+
86
+ it("should allow to subscribe and unsubscribe to storage ids", async () => {
87
+ const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
88
+
89
+ const remoteHeadsMessages = waitForMessages(
90
+ remoteHeadsSubscriptions,
91
+ "remote-heads-changed"
92
+ )
93
+
94
+ const changeRemoteSubsAfterSubscribe = waitForMessages(
95
+ remoteHeadsSubscriptions,
96
+ "change-remote-subs"
97
+ )
98
+
99
+ // subscribe to storageB and change storageB heads
100
+ remoteHeadsSubscriptions.subscribeToRemotes([storageB])
101
+ remoteHeadsSubscriptions.handleRemoteHeads(docAHeadsChangedForStorageB)
102
+
103
+ // receive event for new heads of storageB
104
+ let messages = await remoteHeadsMessages
105
+ assert.strictEqual(messages.length, 1)
106
+ assert.strictEqual(messages[0].storageId, storageB)
107
+ assert.strictEqual(messages[0].documentId, docA)
108
+ assert.deepStrictEqual(messages[0].remoteHeads, [])
109
+
110
+ // receive event for add sub to storageB
111
+ messages = await changeRemoteSubsAfterSubscribe
112
+ assert.strictEqual(messages.length, 1)
113
+ assert.deepStrictEqual(messages[0].add, [storageB])
114
+ assert.deepStrictEqual(messages[0].remove, undefined)
115
+ assert.deepStrictEqual(messages[0].peers, [])
116
+
117
+ const remoteHeadsMessagesAfterUnsub = waitForMessages(
118
+ remoteHeadsSubscriptions,
119
+ "change-remote-subs"
120
+ )
121
+
122
+ // unsubscribe from storageB
123
+ remoteHeadsSubscriptions.unsubscribeFromRemotes([storageB])
124
+
125
+ // receive event for remove sub from storageB
126
+ messages = await remoteHeadsMessagesAfterUnsub
127
+ assert.strictEqual(messages.length, 1)
128
+ assert.deepStrictEqual(messages[0].add, undefined)
129
+ assert.deepStrictEqual(messages[0].remove, [storageB])
130
+ assert.deepStrictEqual(messages[0].peers, [])
131
+ })
132
+
133
+ it("should forward all changes to generous peers", async () => {
134
+ const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
135
+
136
+ const notifyRemoteHeadsMessagesPromise = waitForMessages(
137
+ remoteHeadsSubscriptions,
138
+ "notify-remote-heads"
139
+ )
140
+
141
+ const changeRemoteSubsMessagesPromise = waitForMessages(
142
+ remoteHeadsSubscriptions,
143
+ "change-remote-subs"
144
+ )
145
+
146
+ remoteHeadsSubscriptions.addGenerousPeer(peerC)
147
+ remoteHeadsSubscriptions.subscribeToRemotes([storageB])
148
+
149
+ // change message for docA in storageB
150
+ remoteHeadsSubscriptions.handleRemoteHeads(docAHeadsChangedForStorageB)
151
+
152
+ // change heads directly, are not forwarded
153
+ remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
154
+ docC,
155
+ storageB,
156
+ []
157
+ )
158
+
159
+ // should forward remote-heads events
160
+ let messages = await notifyRemoteHeadsMessagesPromise
161
+ assert.strictEqual(messages.length, 1)
162
+ assert.strictEqual(messages[0].documentId, docA)
163
+ assert.strictEqual(messages[0].storageId, storageB)
164
+ assert.deepStrictEqual(messages[0].heads, [])
165
+
166
+ // should forward subscriptions to generous peer
167
+ messages = await changeRemoteSubsMessagesPromise
168
+ assert.strictEqual(messages.length, 1)
169
+ assert.deepStrictEqual(messages[0].add, [storageB])
170
+ assert.deepStrictEqual(messages[0].remove, undefined)
171
+ assert.deepStrictEqual(messages[0].peers, [peerC])
172
+
173
+ const changeRemoteSubsMessagesAfterUnsubPromise = waitForMessages(
174
+ remoteHeadsSubscriptions,
175
+ "change-remote-subs"
176
+ )
177
+
178
+ // unsubsscribe from storage B
179
+ remoteHeadsSubscriptions.unsubscribeFromRemotes([storageB])
180
+
181
+ // should forward unsubscribe to generous peer
182
+ messages = await changeRemoteSubsMessagesAfterUnsubPromise
183
+ assert.strictEqual(messages.length, 1)
184
+ assert.deepStrictEqual(messages[0].add, undefined)
185
+ assert.deepStrictEqual(messages[0].remove, [storageB])
186
+ assert.deepStrictEqual(messages[0].peers, [peerC])
187
+ })
188
+
189
+ it("should not notify generous peers of changed remote heads, if they send the heads originally", async () => {
190
+ const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
191
+
192
+ const messagesPromise = waitForMessages(
193
+ remoteHeadsSubscriptions,
194
+ "notify-remote-heads"
195
+ )
196
+
197
+ remoteHeadsSubscriptions.addGenerousPeer(peerC)
198
+ remoteHeadsSubscriptions.subscribeToRemotes([storageB])
199
+ remoteHeadsSubscriptions.handleRemoteHeads({
200
+ type: "remote-heads-changed",
201
+ senderId: peerC,
202
+ targetId: peerA,
203
+ documentId: docA,
204
+ newHeads: {
205
+ [storageB]: {
206
+ heads: [],
207
+ timestamp: Date.now(),
208
+ },
209
+ },
210
+ })
211
+
212
+ const messages = await messagesPromise
213
+ assert.strictEqual(messages.length, 0)
214
+ })
215
+
216
+ it("should allow peers to subscribe and unsubscribe to storageIds", async () => {
217
+ const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
218
+ remoteHeadsSubscriptions.subscribeToRemotes([storageB])
219
+
220
+ // subscribe peer c to storage b
221
+ remoteHeadsSubscriptions.handleControlMessage(subscribePeerCToStorageB)
222
+ const messagesAfterSubscribePromise = waitForMessages(
223
+ remoteHeadsSubscriptions,
224
+ "notify-remote-heads"
225
+ )
226
+ remoteHeadsSubscriptions.subscribePeerToDoc(peerC, docA)
227
+ remoteHeadsSubscriptions.subscribePeerToDoc(peerC, docC)
228
+
229
+ // change message for docA in storageB
230
+ remoteHeadsSubscriptions.handleRemoteHeads(docAHeadsChangedForStorageB)
231
+
232
+ // change heads directly
233
+ remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
234
+ docC,
235
+ storageB,
236
+ []
237
+ )
238
+
239
+ // expect peer c to be notified both changes
240
+ let messages = await messagesAfterSubscribePromise
241
+ assert.strictEqual(messages.length, 2)
242
+ assert.strictEqual(messages[0].documentId, docA)
243
+ assert.strictEqual(messages[0].storageId, storageB)
244
+ assert.deepStrictEqual(messages[0].heads, [])
245
+ assert.strictEqual(messages[1].documentId, docC)
246
+ assert.strictEqual(messages[1].storageId, storageB)
247
+ assert.deepStrictEqual(messages[1].heads, [])
248
+
249
+ // unsubscribe peer C
250
+ remoteHeadsSubscriptions.handleControlMessage(unsubscribePeerCFromStorageB)
251
+ const messagesAfteUnsubscribePromise = waitForMessages(
252
+ remoteHeadsSubscriptions,
253
+ "notify-remote-heads"
254
+ )
255
+
256
+ // heads of docB for storageB change
257
+ remoteHeadsSubscriptions.handleRemoteHeads(docBHeadsChangedForStorageB)
258
+
259
+ // expect not to be be notified
260
+ messages = await messagesAfteUnsubscribePromise
261
+ assert.strictEqual(messages.length, 0)
262
+ })
263
+
264
+ it("should not send remote heads for docs that the peer is not subscribed to", async () => {
265
+ const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
266
+ remoteHeadsSubscriptions.subscribeToRemotes([storageB])
267
+
268
+ // subscribe peer c to storage b
269
+ remoteHeadsSubscriptions.handleControlMessage(subscribePeerCToStorageB)
270
+ const messagesAfterSubscribePromise = waitForMessages(
271
+ remoteHeadsSubscriptions,
272
+ "notify-remote-heads"
273
+ )
274
+
275
+ // change message for docA in storageB
276
+ remoteHeadsSubscriptions.handleRemoteHeads(docAHeadsChangedForStorageB)
277
+
278
+ // change heads directly
279
+ remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
280
+ docC,
281
+ storageB,
282
+ []
283
+ )
284
+
285
+ // expect peer c to be notified both changes
286
+ let messages = await messagesAfterSubscribePromise
287
+ assert.strictEqual(messages.length, 0)
288
+ })
289
+
290
+ it("should only notify of sync states with a more recent timestamp", async () => {
291
+ const remoteHeadsSubscription = new RemoteHeadsSubscriptions()
292
+
293
+ const messagesPromise = waitForMessages(
294
+ remoteHeadsSubscription,
295
+ "remote-heads-changed"
296
+ )
297
+
298
+ remoteHeadsSubscription.subscribeToRemotes([storageB])
299
+ remoteHeadsSubscription.handleRemoteHeads(docBHeadsChangedForStorageB2)
300
+
301
+ // send same message
302
+ remoteHeadsSubscription.handleRemoteHeads(docBHeadsChangedForStorageB2)
303
+
304
+ // send message with old heads
305
+ remoteHeadsSubscription.handleRemoteHeads(docBHeadsChangedForStorageB)
306
+
307
+ const messages = await messagesPromise
308
+ assert.strictEqual(messages.length, 1)
309
+ assert.strictEqual(messages[0].storageId, storageB)
310
+ assert.strictEqual(messages[0].documentId, docB)
311
+ assert.deepStrictEqual(messages[0].remoteHeads, docBHeads)
312
+ })
313
+
314
+ it("should remove subs of disconnected peers", async () => {
315
+ const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
316
+
317
+ const messagesPromise = waitForMessages(
318
+ remoteHeadsSubscriptions,
319
+ "change-remote-subs"
320
+ )
321
+
322
+ remoteHeadsSubscriptions.handleControlMessage({
323
+ type: "remote-subscription-change",
324
+ senderId: peerB,
325
+ targetId: peerA,
326
+ add: [storageA, storageC],
327
+ })
328
+
329
+ remoteHeadsSubscriptions.handleControlMessage({
330
+ type: "remote-subscription-change",
331
+ senderId: peerC,
332
+ targetId: peerA,
333
+ add: [storageA, storageD],
334
+ })
335
+
336
+ remoteHeadsSubscriptions.removePeer(peerB)
337
+
338
+ const messages = await messagesPromise
339
+ assert.deepStrictEqual(messages.length, 3)
340
+
341
+ assert.deepStrictEqual(messages[0].add, [storageA, storageC])
342
+ assert.deepStrictEqual(messages[0].remove, [])
343
+ assert.deepStrictEqual(messages[0].peers, [])
344
+
345
+ assert.deepStrictEqual(messages[1].add, [storageD])
346
+ assert.deepStrictEqual(messages[1].remove, [])
347
+ assert.deepStrictEqual(messages[1].peers, [])
348
+
349
+ assert.deepStrictEqual(messages[2].add, undefined)
350
+ assert.deepStrictEqual(messages[2].remove, [storageC])
351
+ assert.deepStrictEqual(messages[2].peers, [])
352
+ })
353
+ })