@automerge/automerge-repo 1.1.0-alpha.6 → 1.1.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 (59) hide show
  1. package/README.md +12 -7
  2. package/dist/AutomergeUrl.js +2 -2
  3. package/dist/DocHandle.d.ts +10 -4
  4. package/dist/DocHandle.d.ts.map +1 -1
  5. package/dist/DocHandle.js +17 -8
  6. package/dist/RemoteHeadsSubscriptions.js +3 -3
  7. package/dist/Repo.d.ts +23 -6
  8. package/dist/Repo.d.ts.map +1 -1
  9. package/dist/Repo.js +104 -71
  10. package/dist/helpers/debounce.js +1 -1
  11. package/dist/helpers/pause.d.ts +0 -1
  12. package/dist/helpers/pause.d.ts.map +1 -1
  13. package/dist/helpers/pause.js +2 -8
  14. package/dist/helpers/throttle.js +1 -1
  15. package/dist/helpers/withTimeout.d.ts.map +1 -1
  16. package/dist/helpers/withTimeout.js +2 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  21. package/dist/network/NetworkAdapter.js +2 -1
  22. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  23. package/dist/network/NetworkSubsystem.js +5 -3
  24. package/dist/network/messages.d.ts +43 -38
  25. package/dist/network/messages.d.ts.map +1 -1
  26. package/dist/network/messages.js +7 -9
  27. package/dist/storage/StorageSubsystem.js +1 -1
  28. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  29. package/dist/synchronizer/CollectionSynchronizer.js +1 -0
  30. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  31. package/dist/synchronizer/DocSynchronizer.js +13 -5
  32. package/dist/synchronizer/Synchronizer.d.ts +11 -3
  33. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  34. package/package.json +4 -4
  35. package/src/AutomergeUrl.ts +2 -2
  36. package/src/DocHandle.ts +34 -12
  37. package/src/RemoteHeadsSubscriptions.ts +3 -3
  38. package/src/Repo.ts +130 -81
  39. package/src/helpers/debounce.ts +1 -1
  40. package/src/helpers/pause.ts +3 -11
  41. package/src/helpers/throttle.ts +1 -1
  42. package/src/helpers/withTimeout.ts +2 -0
  43. package/src/index.ts +1 -1
  44. package/src/network/NetworkAdapter.ts +5 -3
  45. package/src/network/NetworkSubsystem.ts +5 -4
  46. package/src/network/messages.ts +60 -63
  47. package/src/storage/StorageSubsystem.ts +1 -1
  48. package/src/synchronizer/CollectionSynchronizer.ts +2 -1
  49. package/src/synchronizer/DocSynchronizer.ts +19 -11
  50. package/src/synchronizer/Synchronizer.ts +11 -3
  51. package/test/CollectionSynchronizer.test.ts +7 -5
  52. package/test/DocHandle.test.ts +11 -2
  53. package/test/RemoteHeadsSubscriptions.test.ts +53 -50
  54. package/test/Repo.test.ts +64 -2
  55. package/test/StorageSubsystem.test.ts +1 -1
  56. package/test/helpers/collectMessages.ts +19 -0
  57. package/test/remoteHeads.test.ts +141 -120
  58. package/.eslintrc +0 -28
  59. package/test/helpers/waitForMessages.ts +0 -22
@@ -1,17 +1,27 @@
1
1
  import { SyncState } from "@automerge/automerge"
2
- import { DocumentId, PeerId, SessionId } from "../types.js"
3
2
  import { StorageId } from "../storage/types.js"
3
+ import { DocumentId, PeerId, SessionId } from "../types.js"
4
+
5
+ export type Message = {
6
+ type: string
7
+
8
+ /** The peer ID of the sender of this message */
9
+ senderId: PeerId
10
+
11
+ /** The peer ID of the recipient of this message */
12
+ targetId: PeerId
13
+
14
+ data?: Uint8Array
15
+
16
+ documentId?: DocumentId
17
+ }
4
18
 
5
19
  /**
6
20
  * A sync message for a particular document
7
21
  */
8
22
  export type SyncMessage = {
9
23
  type: "sync"
10
-
11
- /** The peer ID of the sender of this message */
12
24
  senderId: PeerId
13
-
14
- /** The peer ID of the recipient of this message */
15
25
  targetId: PeerId
16
26
 
17
27
  /** The automerge sync message */
@@ -21,53 +31,50 @@ export type SyncMessage = {
21
31
  documentId: DocumentId
22
32
  }
23
33
 
24
- /** An ephemeral message
34
+ /**
35
+ * An ephemeral message.
25
36
  *
26
37
  * @remarks
27
- * Ephemeral messages are not persisted anywhere and have no particular
28
- * structure. `automerge-repo` will gossip them around, in order to avoid
29
- * eternal loops of ephemeral messages every message has a session ID, which
30
- * is a random number generated by the sender at startup time, and a sequence
31
- * number. The combination of these two things allows us to discard messages
32
- * we have already seen.
38
+ * Ephemeral messages are not persisted anywhere. The data property can be used by the application
39
+ * as needed. The repo gossips these around.
40
+ *
41
+ * In order to avoid infinite loops of ephemeral messages, every message has (a) a session ID, which
42
+ * is a random number generated by the sender at startup time; and (b) a sequence number. The
43
+ * combination of these two things allows us to discard messages we have already seen.
33
44
  * */
34
45
  export type EphemeralMessage = {
35
46
  type: "ephemeral"
36
-
37
- /** The peer ID of the sender of this message */
38
47
  senderId: PeerId
39
-
40
- /** The peer ID of the recipient of this message */
41
48
  targetId: PeerId
42
49
 
43
- /** A sequence number which must be incremented for each message sent by this peer */
50
+ /** A sequence number which must be incremented for each message sent by this peer. */
44
51
  count: number
45
52
 
46
- /** The ID of the session this message is part of. The sequence number for a given session always increases */
53
+ /** The ID of the session this message is part of. The sequence number for a given session always increases. */
47
54
  sessionId: SessionId
48
55
 
49
- /** The document ID this message pertains to */
56
+ /** The document ID this message pertains to. */
50
57
  documentId: DocumentId
51
58
 
52
- /** The actual data of the message */
59
+ /** The actual data of the message. */
53
60
  data: Uint8Array
54
61
  }
55
62
 
56
- /** Sent by a {@link Repo} to indicate that it does not have the document and none of it's connected peers do either */
63
+ /**
64
+ * Sent by a {@link Repo} to indicate that it does not have the document and none of its connected
65
+ * peers do either.
66
+ */
57
67
  export type DocumentUnavailableMessage = {
58
68
  type: "doc-unavailable"
59
-
60
- /** The peer ID of the sender of this message */
61
69
  senderId: PeerId
62
-
63
- /** The peer ID of the recipient of this message */
64
70
  targetId: PeerId
65
71
 
66
72
  /** The document which the peer claims it doesn't have */
67
73
  documentId: DocumentId
68
74
  }
69
75
 
70
- /** Sent by a {@link Repo} to request a document from a peer
76
+ /**
77
+ * Sent by a {@link Repo} to request a document from a peer.
71
78
  *
72
79
  * @remarks
73
80
  * This is identical to a {@link SyncMessage} except that it is sent by a {@link Repo}
@@ -75,47 +82,43 @@ export type DocumentUnavailableMessage = {
75
82
  * */
76
83
  export type RequestMessage = {
77
84
  type: "request"
78
-
79
- /** The peer ID of the sender of this message */
80
85
  senderId: PeerId
81
-
82
- /** The peer ID of the recipient of this message */
83
86
  targetId: PeerId
84
87
 
85
- /** The initial automerge sync message */
88
+ /** The automerge sync message */
86
89
  data: Uint8Array
87
90
 
88
- /** The document ID this message requests */
91
+ /** The document ID of the document this message is for */
89
92
  documentId: DocumentId
90
93
  }
91
94
 
92
- /** (anticipating work in progress) */
93
- export type AuthMessage<TPayload = any> = {
94
- type: "auth"
95
-
96
- /** The peer ID of the sender of this message */
97
- senderId: PeerId
98
-
99
- /** The peer ID of the recipient of this message */
100
- targetId: PeerId
101
-
102
- /** The payload of the auth message (up to the specific auth provider) */
103
- payload: TPayload
104
- }
105
-
95
+ /**
96
+ * Sent by a {@link Repo} to add or remove storage IDs from a remote peer's subscription.
97
+ */
106
98
  export type RemoteSubscriptionControlMessage = {
107
99
  type: "remote-subscription-change"
108
100
  senderId: PeerId
109
101
  targetId: PeerId
102
+
103
+ /** The storage IDs to add to the subscription */
110
104
  add?: StorageId[]
105
+
106
+ /** The storage IDs to remove from the subscription */
111
107
  remove?: StorageId[]
112
108
  }
113
109
 
110
+ /**
111
+ * Sent by a {@link Repo} to indicate that the heads of a document have changed on a remote peer.
112
+ */
114
113
  export type RemoteHeadsChanged = {
115
114
  type: "remote-heads-changed"
116
115
  senderId: PeerId
117
116
  targetId: PeerId
117
+
118
+ /** The document ID of the document that has changed */
118
119
  documentId: DocumentId
120
+
121
+ /** The document's new heads */
119
122
  newHeads: { [key: StorageId]: { heads: string[]; timestamp: number } }
120
123
  }
121
124
 
@@ -128,19 +131,17 @@ export type RepoMessage =
128
131
  | RemoteSubscriptionControlMessage
129
132
  | RemoteHeadsChanged
130
133
 
134
+ /** These are message types that are handled by the {@link CollectionSynchronizer}.*/
131
135
  export type DocMessage =
132
136
  | SyncMessage
133
137
  | EphemeralMessage
134
138
  | RequestMessage
135
139
  | DocumentUnavailableMessage
136
140
 
137
- /** These are all the message types that a {@link NetworkAdapter} might see. */
138
- export type Message = RepoMessage | AuthMessage
139
-
140
141
  /**
141
142
  * The contents of a message, without the sender ID or other properties added by the {@link NetworkSubsystem})
142
143
  */
143
- export type MessageContents<T extends Message = Message> =
144
+ export type MessageContents<T extends Message = RepoMessage> =
144
145
  T extends EphemeralMessage
145
146
  ? Omit<T, "senderId" | "count" | "sessionId">
146
147
  : Omit<T, "senderId">
@@ -160,16 +161,13 @@ export interface OpenDocMessage {
160
161
 
161
162
  // TYPE GUARDS
162
163
 
163
- export const isValidRepoMessage = (message: Message): message is RepoMessage =>
164
- typeof message === "object" &&
165
- typeof message.type === "string" &&
166
- typeof message.senderId === "string" &&
167
- (isSyncMessage(message) ||
168
- isEphemeralMessage(message) ||
169
- isRequestMessage(message) ||
170
- isDocumentUnavailableMessage(message) ||
171
- isRemoteSubscriptionControlMessage(message) ||
172
- isRemoteHeadsChanged(message))
164
+ export const isRepoMessage = (message: Message): message is RepoMessage =>
165
+ isSyncMessage(message) ||
166
+ isEphemeralMessage(message) ||
167
+ isRequestMessage(message) ||
168
+ isDocumentUnavailableMessage(message) ||
169
+ isRemoteSubscriptionControlMessage(message) ||
170
+ isRemoteHeadsChanged(message)
173
171
 
174
172
  // prettier-ignore
175
173
  export const isDocumentUnavailableMessage = (msg: Message): msg is DocumentUnavailableMessage =>
@@ -184,9 +182,8 @@ export const isSyncMessage = (msg: Message): msg is SyncMessage =>
184
182
  export const isEphemeralMessage = (msg: Message): msg is EphemeralMessage =>
185
183
  msg.type === "ephemeral"
186
184
 
187
- export const isRemoteSubscriptionControlMessage = (
188
- msg: Message
189
- ): msg is RemoteSubscriptionControlMessage =>
185
+ // prettier-ignore
186
+ export const isRemoteSubscriptionControlMessage = (msg: Message): msg is RemoteSubscriptionControlMessage =>
190
187
  msg.type === "remote-subscription-change"
191
188
 
192
189
  export const isRemoteHeadsChanged = (msg: Message): msg is RemoteHeadsChanged =>
@@ -33,7 +33,7 @@ export class StorageSubsystem {
33
33
  }
34
34
 
35
35
  async id(): Promise<StorageId> {
36
- let storedId = await this.#storageAdapter.load(["storage-adapter-id"])
36
+ const storedId = await this.#storageAdapter.load(["storage-adapter-id"])
37
37
 
38
38
  let id: StorageId
39
39
  if (storedId) {
@@ -2,7 +2,7 @@ import debug from "debug"
2
2
  import { DocHandle } from "../DocHandle.js"
3
3
  import { stringifyAutomergeUrl } from "../AutomergeUrl.js"
4
4
  import { Repo } from "../Repo.js"
5
- import { DocMessage, RepoMessage } from "../network/messages.js"
5
+ import { DocMessage } from "../network/messages.js"
6
6
  import { DocumentId, PeerId } from "../types.js"
7
7
  import { DocSynchronizer } from "./DocSynchronizer.js"
8
8
  import { Synchronizer } from "./Synchronizer.js"
@@ -117,6 +117,7 @@ export class CollectionSynchronizer extends Synchronizer {
117
117
  }
118
118
 
119
119
  // TODO: implement this
120
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
120
121
  removeDocument(documentId: DocumentId) {
121
122
  throw new Error("not implemented")
122
123
  }
@@ -137,9 +137,13 @@ export class DocSynchronizer extends Synchronizer {
137
137
 
138
138
  let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId]
139
139
  if (!pendingCallbacks) {
140
- this.#onLoadSyncState(peerId).then(syncState => {
141
- this.#initSyncState(peerId, syncState ?? A.initSyncState())
142
- })
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
+ })
143
147
  pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = []
144
148
  }
145
149
 
@@ -260,11 +264,15 @@ export class DocSynchronizer extends Synchronizer {
260
264
  )
261
265
  this.#setSyncState(peerId, reparsedSyncState)
262
266
 
263
- docPromise.then(doc => {
264
- if (doc) {
265
- this.#sendSyncMessage(peerId, doc)
266
- }
267
- })
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
+ })
268
276
  })
269
277
  })
270
278
  }
@@ -326,10 +334,10 @@ export class DocSynchronizer extends Synchronizer {
326
334
  }
327
335
 
328
336
  this.#processAllPendingSyncMessages()
329
- this.#processSyncMessage(message, new Date())
337
+ this.#processSyncMessage(message)
330
338
  }
331
339
 
332
- #processSyncMessage(message: SyncMessage | RequestMessage, received: Date) {
340
+ #processSyncMessage(message: SyncMessage | RequestMessage) {
333
341
  if (isRequestMessage(message)) {
334
342
  this.#peerDocumentStatuses[message.senderId] = "wants"
335
343
  }
@@ -388,7 +396,7 @@ export class DocSynchronizer extends Synchronizer {
388
396
 
389
397
  #processAllPendingSyncMessages() {
390
398
  for (const message of this.#pendingSyncMessages) {
391
- this.#processSyncMessage(message.message, message.received)
399
+ this.#processSyncMessage(message.message)
392
400
  }
393
401
 
394
402
  this.#pendingSyncMessages = []
@@ -3,15 +3,23 @@ import {
3
3
  MessageContents,
4
4
  OpenDocMessage,
5
5
  RepoMessage,
6
- SyncStateMessage,
7
6
  } from "../network/messages.js"
7
+ import { SyncState } from "@automerge/automerge"
8
+ import { PeerId, DocumentId } from "../types.js"
8
9
 
9
10
  export abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
10
11
  abstract receiveMessage(message: RepoMessage): void
11
12
  }
12
13
 
13
14
  export interface SynchronizerEvents {
14
- message: (arg: MessageContents) => void
15
- "sync-state": (arg: SyncStateMessage) => void
15
+ message: (payload: MessageContents) => void
16
+ "sync-state": (payload: SyncStatePayload) => void
16
17
  "open-doc": (arg: OpenDocMessage) => void
17
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
25
+ }
@@ -1,6 +1,6 @@
1
1
  import assert from "assert"
2
2
  import { beforeEach, describe, it } from "vitest"
3
- import { PeerId, Repo } from "../src/index.js"
3
+ import { PeerId, Repo, SyncMessage } from "../src/index.js"
4
4
  import { CollectionSynchronizer } from "../src/synchronizer/CollectionSynchronizer.js"
5
5
 
6
6
  describe("CollectionSynchronizer", () => {
@@ -24,8 +24,9 @@ describe("CollectionSynchronizer", () => {
24
24
  synchronizer.addPeer("peer1" as PeerId)
25
25
 
26
26
  synchronizer.once("message", event => {
27
- assert(event.targetId === "peer1")
28
- assert(event.documentId === handle.documentId)
27
+ const { targetId, documentId } = event as SyncMessage
28
+ assert(targetId === "peer1")
29
+ assert(documentId === handle.documentId)
29
30
  done()
30
31
  })
31
32
 
@@ -37,8 +38,9 @@ describe("CollectionSynchronizer", () => {
37
38
  const handle = repo.create()
38
39
  synchronizer.addDocument(handle.documentId)
39
40
  synchronizer.once("message", event => {
40
- assert(event.targetId === "peer1")
41
- assert(event.documentId === handle.documentId)
41
+ const { targetId, documentId } = event as SyncMessage
42
+ assert(targetId === "peer1")
43
+ assert(documentId === handle.documentId)
42
44
  done()
43
45
  })
44
46
  synchronizer.addPeer("peer1" as PeerId)
@@ -5,7 +5,7 @@ import { describe, it } from "vitest"
5
5
  import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
6
6
  import { eventPromise } from "../src/helpers/eventPromise.js"
7
7
  import { pause } from "../src/helpers/pause.js"
8
- import { DocHandle, DocHandleChangePayload, PeerId } from "../src/index.js"
8
+ import { DocHandle, DocHandleChangePayload } from "../src/index.js"
9
9
  import { TestDoc } from "./types.js"
10
10
 
11
11
  describe("DocHandle", () => {
@@ -20,6 +20,15 @@ describe("DocHandle", () => {
20
20
  assert.equal(handle.documentId, TEST_ID)
21
21
  })
22
22
 
23
+ it("should take an initial value", async () => {
24
+ const handle = new DocHandle(TEST_ID, {
25
+ isNew: true,
26
+ initialValue: { foo: "bar" },
27
+ })
28
+ const doc = await handle.doc()
29
+ assert.equal(doc.foo, "bar")
30
+ })
31
+
23
32
  it("should become ready when a document is loaded", async () => {
24
33
  const handle = new DocHandle<TestDoc>(TEST_ID)
25
34
  assert.equal(handle.isReady(), false)
@@ -44,7 +53,7 @@ describe("DocHandle", () => {
44
53
  assert.deepEqual(doc, handle.docSync())
45
54
  })
46
55
 
47
- it("should return undefined if we accessing the doc before ready", async () => {
56
+ it("should return undefined if we access the doc before ready", async () => {
48
57
  const handle = new DocHandle<TestDoc>(TEST_ID)
49
58
 
50
59
  assert.equal(handle.docSync(), undefined)
@@ -8,7 +8,7 @@ import {
8
8
  RemoteHeadsChanged,
9
9
  RemoteSubscriptionControlMessage,
10
10
  } from "../src/network/messages.js"
11
- import { waitForMessages } from "./helpers/waitForMessages.js"
11
+ import { collectMessages } from "./helpers/collectMessages.js"
12
12
 
13
13
  describe("RepoHeadsSubscriptions", () => {
14
14
  const storageA = "remote-a" as StorageId
@@ -86,15 +86,15 @@ describe("RepoHeadsSubscriptions", () => {
86
86
  it("should allow to subscribe and unsubscribe to storage ids", async () => {
87
87
  const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
88
88
 
89
- const remoteHeadsMessages = waitForMessages(
90
- remoteHeadsSubscriptions,
91
- "remote-heads-changed"
92
- )
89
+ const remoteHeadsMessages = collectMessages({
90
+ emitter: remoteHeadsSubscriptions,
91
+ event: "remote-heads-changed",
92
+ })
93
93
 
94
- const changeRemoteSubsAfterSubscribe = waitForMessages(
95
- remoteHeadsSubscriptions,
96
- "change-remote-subs"
97
- )
94
+ const changeRemoteSubsAfterSubscribe = collectMessages({
95
+ emitter: remoteHeadsSubscriptions,
96
+ event: "change-remote-subs",
97
+ })
98
98
 
99
99
  // subscribe to storageB and change storageB heads
100
100
  remoteHeadsSubscriptions.subscribeToRemotes([storageB])
@@ -114,10 +114,10 @@ describe("RepoHeadsSubscriptions", () => {
114
114
  assert.deepStrictEqual(messages[0].remove, undefined)
115
115
  assert.deepStrictEqual(messages[0].peers, [])
116
116
 
117
- const remoteHeadsMessagesAfterUnsub = waitForMessages(
118
- remoteHeadsSubscriptions,
119
- "change-remote-subs"
120
- )
117
+ const remoteHeadsMessagesAfterUnsub = collectMessages({
118
+ emitter: remoteHeadsSubscriptions,
119
+ event: "change-remote-subs",
120
+ })
121
121
 
122
122
  // unsubscribe from storageB
123
123
  remoteHeadsSubscriptions.unsubscribeFromRemotes([storageB])
@@ -133,15 +133,15 @@ describe("RepoHeadsSubscriptions", () => {
133
133
  it("should forward all changes to generous peers", async () => {
134
134
  const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
135
135
 
136
- const notifyRemoteHeadsMessagesPromise = waitForMessages(
137
- remoteHeadsSubscriptions,
138
- "notify-remote-heads"
139
- )
136
+ const notifyRemoteHeadsMessagesPromise = collectMessages({
137
+ emitter: remoteHeadsSubscriptions,
138
+ event: "notify-remote-heads",
139
+ })
140
140
 
141
- const changeRemoteSubsMessagesPromise = waitForMessages(
142
- remoteHeadsSubscriptions,
143
- "change-remote-subs"
144
- )
141
+ const changeRemoteSubsMessagesPromise = collectMessages({
142
+ emitter: remoteHeadsSubscriptions,
143
+ event: "change-remote-subs",
144
+ })
145
145
 
146
146
  remoteHeadsSubscriptions.addGenerousPeer(peerC)
147
147
  remoteHeadsSubscriptions.subscribeToRemotes([storageB])
@@ -170,10 +170,10 @@ describe("RepoHeadsSubscriptions", () => {
170
170
  assert.deepStrictEqual(messages[0].remove, undefined)
171
171
  assert.deepStrictEqual(messages[0].peers, [peerC])
172
172
 
173
- const changeRemoteSubsMessagesAfterUnsubPromise = waitForMessages(
174
- remoteHeadsSubscriptions,
175
- "change-remote-subs"
176
- )
173
+ const changeRemoteSubsMessagesAfterUnsubPromise = collectMessages({
174
+ emitter: remoteHeadsSubscriptions,
175
+ event: "change-remote-subs",
176
+ })
177
177
 
178
178
  // unsubsscribe from storage B
179
179
  remoteHeadsSubscriptions.unsubscribeFromRemotes([storageB])
@@ -189,10 +189,10 @@ describe("RepoHeadsSubscriptions", () => {
189
189
  it("should not notify generous peers of changed remote heads, if they send the heads originally", async () => {
190
190
  const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
191
191
 
192
- const messagesPromise = waitForMessages(
193
- remoteHeadsSubscriptions,
194
- "notify-remote-heads"
195
- )
192
+ const messagesPromise = collectMessages({
193
+ emitter: remoteHeadsSubscriptions,
194
+ event: "notify-remote-heads",
195
+ })
196
196
 
197
197
  remoteHeadsSubscriptions.addGenerousPeer(peerC)
198
198
  remoteHeadsSubscriptions.subscribeToRemotes([storageB])
@@ -219,10 +219,10 @@ describe("RepoHeadsSubscriptions", () => {
219
219
 
220
220
  // subscribe peer c to storage b
221
221
  remoteHeadsSubscriptions.handleControlMessage(subscribePeerCToStorageB)
222
- const messagesAfterSubscribePromise = waitForMessages(
223
- remoteHeadsSubscriptions,
224
- "notify-remote-heads"
225
- )
222
+ const messagesAfterSubscribePromise = collectMessages({
223
+ emitter: remoteHeadsSubscriptions,
224
+ event: "notify-remote-heads",
225
+ })
226
226
  remoteHeadsSubscriptions.subscribePeerToDoc(peerC, docA)
227
227
  remoteHeadsSubscriptions.subscribePeerToDoc(peerC, docC)
228
228
 
@@ -248,10 +248,10 @@ describe("RepoHeadsSubscriptions", () => {
248
248
 
249
249
  // unsubscribe peer C
250
250
  remoteHeadsSubscriptions.handleControlMessage(unsubscribePeerCFromStorageB)
251
- const messagesAfteUnsubscribePromise = waitForMessages(
252
- remoteHeadsSubscriptions,
253
- "notify-remote-heads"
254
- )
251
+ const messagesAfteUnsubscribePromise = collectMessages({
252
+ emitter: remoteHeadsSubscriptions,
253
+ event: "notify-remote-heads",
254
+ })
255
255
 
256
256
  // heads of docB for storageB change
257
257
  remoteHeadsSubscriptions.handleRemoteHeads(docBHeadsChangedForStorageB)
@@ -267,10 +267,10 @@ describe("RepoHeadsSubscriptions", () => {
267
267
 
268
268
  // subscribe peer c to storage b
269
269
  remoteHeadsSubscriptions.handleControlMessage(subscribePeerCToStorageB)
270
- const messagesAfterSubscribePromise = waitForMessages(
271
- remoteHeadsSubscriptions,
272
- "notify-remote-heads"
273
- )
270
+ const messagesAfterSubscribePromise = collectMessages({
271
+ emitter: remoteHeadsSubscriptions,
272
+ event: "notify-remote-heads",
273
+ })
274
274
 
275
275
  // change message for docA in storageB
276
276
  remoteHeadsSubscriptions.handleRemoteHeads(docAHeadsChangedForStorageB)
@@ -287,17 +287,20 @@ describe("RepoHeadsSubscriptions", () => {
287
287
  assert.strictEqual(messages.length, 0)
288
288
  })
289
289
 
290
- it("should ignore sync states with an older timestamp", async () => {
290
+ it("should only notify of sync states with a more recent timestamp", async () => {
291
291
  const remoteHeadsSubscription = new RemoteHeadsSubscriptions()
292
292
 
293
- const messagesPromise = waitForMessages(
294
- remoteHeadsSubscription,
295
- "remote-heads-changed"
296
- )
293
+ const messagesPromise = collectMessages({
294
+ emitter: remoteHeadsSubscription,
295
+ event: "remote-heads-changed",
296
+ })
297
297
 
298
298
  remoteHeadsSubscription.subscribeToRemotes([storageB])
299
299
  remoteHeadsSubscription.handleRemoteHeads(docBHeadsChangedForStorageB2)
300
300
 
301
+ // send same message
302
+ remoteHeadsSubscription.handleRemoteHeads(docBHeadsChangedForStorageB2)
303
+
301
304
  // send message with old heads
302
305
  remoteHeadsSubscription.handleRemoteHeads(docBHeadsChangedForStorageB)
303
306
 
@@ -311,10 +314,10 @@ describe("RepoHeadsSubscriptions", () => {
311
314
  it("should remove subs of disconnected peers", async () => {
312
315
  const remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
313
316
 
314
- const messagesPromise = waitForMessages(
315
- remoteHeadsSubscriptions,
316
- "change-remote-subs"
317
- )
317
+ const messagesPromise = collectMessages({
318
+ emitter: remoteHeadsSubscriptions,
319
+ event: "change-remote-subs",
320
+ })
318
321
 
319
322
  remoteHeadsSubscriptions.handleControlMessage({
320
323
  type: "remote-subscription-change",