@automerge/automerge-repo 1.1.0-alpha.7 → 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 (50) hide show
  1. package/README.md +3 -1
  2. package/dist/AutomergeUrl.js +1 -1
  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/Repo.d.ts +18 -6
  7. package/dist/Repo.d.ts.map +1 -1
  8. package/dist/Repo.js +88 -72
  9. package/dist/helpers/pause.d.ts +0 -1
  10. package/dist/helpers/pause.d.ts.map +1 -1
  11. package/dist/helpers/pause.js +2 -8
  12. package/dist/helpers/withTimeout.d.ts.map +1 -1
  13. package/dist/helpers/withTimeout.js +2 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  18. package/dist/network/NetworkAdapter.js +1 -0
  19. package/dist/network/NetworkSubsystem.js +3 -3
  20. package/dist/network/messages.d.ts +43 -38
  21. package/dist/network/messages.d.ts.map +1 -1
  22. package/dist/network/messages.js +7 -9
  23. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  24. package/dist/synchronizer/CollectionSynchronizer.js +1 -0
  25. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  26. package/dist/synchronizer/DocSynchronizer.js +11 -7
  27. package/dist/synchronizer/Synchronizer.d.ts +11 -3
  28. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  29. package/package.json +3 -4
  30. package/src/AutomergeUrl.ts +1 -1
  31. package/src/DocHandle.ts +34 -12
  32. package/src/Repo.ts +113 -84
  33. package/src/helpers/pause.ts +3 -11
  34. package/src/helpers/withTimeout.ts +2 -0
  35. package/src/index.ts +1 -1
  36. package/src/network/NetworkAdapter.ts +4 -2
  37. package/src/network/NetworkSubsystem.ts +3 -3
  38. package/src/network/messages.ts +60 -63
  39. package/src/synchronizer/CollectionSynchronizer.ts +1 -0
  40. package/src/synchronizer/DocSynchronizer.ts +19 -15
  41. package/src/synchronizer/Synchronizer.ts +11 -3
  42. package/test/CollectionSynchronizer.test.ts +7 -5
  43. package/test/DocHandle.test.ts +11 -2
  44. package/test/RemoteHeadsSubscriptions.test.ts +49 -49
  45. package/test/Repo.test.ts +39 -1
  46. package/test/StorageSubsystem.test.ts +1 -1
  47. package/test/helpers/collectMessages.ts +19 -0
  48. package/test/remoteHeads.test.ts +142 -119
  49. package/.eslintrc +0 -28
  50. package/test/helpers/waitForMessages.ts +0 -22
package/src/Repo.ts CHANGED
@@ -7,17 +7,18 @@ import {
7
7
  parseAutomergeUrl,
8
8
  } from "./AutomergeUrl.js"
9
9
  import { DocHandle, DocHandleEncodedChangePayload } from "./DocHandle.js"
10
+ import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js"
11
+ import { headsAreSame } from "./helpers/headsAreSame.js"
10
12
  import { throttle } from "./helpers/throttle.js"
11
13
  import { NetworkAdapter, type PeerMetadata } from "./network/NetworkAdapter.js"
12
14
  import { NetworkSubsystem } from "./network/NetworkSubsystem.js"
15
+ import { RepoMessage } from "./network/messages.js"
13
16
  import { StorageAdapter } from "./storage/StorageAdapter.js"
14
17
  import { StorageSubsystem } from "./storage/StorageSubsystem.js"
18
+ import { StorageId } from "./storage/types.js"
15
19
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
20
+ import { SyncStatePayload } from "./synchronizer/Synchronizer.js"
16
21
  import type { AnyDocumentId, DocumentId, PeerId } from "./types.js"
17
- import { RepoMessage, SyncStateMessage } from "./network/messages.js"
18
- import { StorageId } from "./storage/types.js"
19
- import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js"
20
- import { headsAreSame } from "./helpers/headsAreSame.js"
21
22
 
22
23
  /** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
23
24
  /** The `Repo` is the main entry point of this library
@@ -52,6 +53,7 @@ export class Repo extends EventEmitter<RepoEvents> {
52
53
  peerMetadataByPeerId: Record<PeerId, PeerMetadata> = {}
53
54
 
54
55
  #remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
56
+ #remoteHeadsGossipingEnabled = false
55
57
 
56
58
  constructor({
57
59
  storage,
@@ -59,8 +61,10 @@ export class Repo extends EventEmitter<RepoEvents> {
59
61
  peerId,
60
62
  sharePolicy,
61
63
  isEphemeral = storage === undefined,
64
+ enableRemoteHeadsGossiping = false,
62
65
  }: RepoConfig) {
63
66
  super()
67
+ this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping
64
68
  this.#log = debug(`automerge-repo:repo`)
65
69
  this.sharePolicy = sharePolicy ?? this.sharePolicy
66
70
 
@@ -77,10 +81,7 @@ export class Repo extends EventEmitter<RepoEvents> {
77
81
  }: DocHandleEncodedChangePayload<any>) => {
78
82
  void storageSubsystem.saveDoc(handle.documentId, doc)
79
83
  }
80
- handle.on(
81
- "heads-changed",
82
- throttle(saveFn, this.saveDebounceRate)
83
- )
84
+ handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
84
85
 
85
86
  if (isNew) {
86
87
  // this is a new document, immediately save it
@@ -140,9 +141,11 @@ export class Repo extends EventEmitter<RepoEvents> {
140
141
  networkSubsystem.send(message)
141
142
  })
142
143
 
143
- this.#synchronizer.on("open-doc", ({ peerId, documentId }) => {
144
- this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId)
145
- })
144
+ if (this.#remoteHeadsGossipingEnabled) {
145
+ this.#synchronizer.on("open-doc", ({ peerId, documentId }) => {
146
+ this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId)
147
+ })
148
+ }
146
149
 
147
150
  // STORAGE
148
151
  // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
@@ -154,7 +157,7 @@ export class Repo extends EventEmitter<RepoEvents> {
154
157
 
155
158
  const myPeerMetadata: Promise<PeerMetadata> = new Promise(
156
159
  // eslint-disable-next-line no-async-promise-executor -- TODO: fix
157
- async (resolve) =>
160
+ async resolve =>
158
161
  resolve({
159
162
  storageId: await storageSubsystem?.id(),
160
163
  isEphemeral,
@@ -178,7 +181,7 @@ export class Repo extends EventEmitter<RepoEvents> {
178
181
 
179
182
  this.sharePolicy(peerId)
180
183
  .then(shouldShare => {
181
- if (shouldShare) {
184
+ if (shouldShare && this.#remoteHeadsGossipingEnabled) {
182
185
  this.#remoteHeadsSubscriptions.addGenerousPeer(peerId)
183
186
  }
184
187
  })
@@ -218,7 +221,7 @@ export class Repo extends EventEmitter<RepoEvents> {
218
221
  if (haveHeadsChanged) {
219
222
  handle.setRemoteHeads(storageId, message.syncState.theirHeads)
220
223
 
221
- if (storageId) {
224
+ if (storageId && this.#remoteHeadsGossipingEnabled) {
222
225
  this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
223
226
  message.documentId,
224
227
  storageId,
@@ -228,45 +231,51 @@ export class Repo extends EventEmitter<RepoEvents> {
228
231
  }
229
232
  })
230
233
 
231
- this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
232
- this.networkSubsystem.send({
233
- type: "remote-heads-changed",
234
- targetId: message.targetId,
235
- documentId: message.documentId,
236
- newHeads: {
237
- [message.storageId]: {
238
- heads: message.heads,
239
- timestamp: message.timestamp,
234
+ if (this.#remoteHeadsGossipingEnabled) {
235
+ this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
236
+ this.networkSubsystem.send({
237
+ type: "remote-heads-changed",
238
+ targetId: message.targetId,
239
+ documentId: message.documentId,
240
+ newHeads: {
241
+ [message.storageId]: {
242
+ heads: message.heads,
243
+ timestamp: message.timestamp,
244
+ },
240
245
  },
241
- },
246
+ })
242
247
  })
243
- })
244
248
 
245
- this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
246
- this.#log("change-remote-subs", message)
247
- for (const peer of message.peers) {
248
- this.networkSubsystem.send({
249
- type: "remote-subscription-change",
250
- targetId: peer,
251
- add: message.add,
252
- remove: message.remove,
253
- })
254
- }
255
- })
249
+ this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
250
+ this.#log("change-remote-subs", message)
251
+ for (const peer of message.peers) {
252
+ this.networkSubsystem.send({
253
+ type: "remote-subscription-change",
254
+ targetId: peer,
255
+ add: message.add,
256
+ remove: message.remove,
257
+ })
258
+ }
259
+ })
256
260
 
257
- this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
258
- const handle = this.#handleCache[message.documentId]
259
- handle.setRemoteHeads(message.storageId, message.remoteHeads)
260
- })
261
+ this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
262
+ const handle = this.#handleCache[message.documentId]
263
+ handle.setRemoteHeads(message.storageId, message.remoteHeads)
264
+ })
265
+ }
261
266
  }
262
267
 
263
268
  #receiveMessage(message: RepoMessage) {
264
269
  switch (message.type) {
265
270
  case "remote-subscription-change":
266
- this.#remoteHeadsSubscriptions.handleControlMessage(message)
271
+ if (this.#remoteHeadsGossipingEnabled) {
272
+ this.#remoteHeadsSubscriptions.handleControlMessage(message)
273
+ }
267
274
  break
268
275
  case "remote-heads-changed":
269
- this.#remoteHeadsSubscriptions.handleRemoteHeads(message)
276
+ if (this.#remoteHeadsGossipingEnabled) {
277
+ this.#remoteHeadsSubscriptions.handleRemoteHeads(message)
278
+ }
270
279
  break
271
280
  case "sync":
272
281
  case "request":
@@ -280,17 +289,17 @@ export class Repo extends EventEmitter<RepoEvents> {
280
289
 
281
290
  #throttledSaveSyncStateHandlers: Record<
282
291
  StorageId,
283
- (message: SyncStateMessage) => void
292
+ (payload: SyncStatePayload) => void
284
293
  > = {}
285
294
 
286
295
  /** saves sync state throttled per storage id, if a peer doesn't have a storage id it's sync state is not persisted */
287
- #saveSyncState(message: SyncStateMessage) {
296
+ #saveSyncState(payload: SyncStatePayload) {
288
297
  if (!this.storageSubsystem) {
289
298
  return
290
299
  }
291
300
 
292
301
  const { storageId, isEphemeral } =
293
- this.peerMetadataByPeerId[message.peerId] || {}
302
+ this.peerMetadataByPeerId[payload.peerId] || {}
294
303
 
295
304
  if (!storageId || isEphemeral) {
296
305
  return
@@ -299,33 +308,37 @@ export class Repo extends EventEmitter<RepoEvents> {
299
308
  let handler = this.#throttledSaveSyncStateHandlers[storageId]
300
309
  if (!handler) {
301
310
  handler = this.#throttledSaveSyncStateHandlers[storageId] = throttle(
302
- ({ documentId, syncState }: SyncStateMessage) => {
303
- this.storageSubsystem!.saveSyncState(documentId, storageId, syncState)
304
- .catch(err => {
305
- this.#log("error saving sync state", { err })
306
- })
311
+ ({ documentId, syncState }: SyncStatePayload) => {
312
+ void this.storageSubsystem!.saveSyncState(
313
+ documentId,
314
+ storageId,
315
+ syncState
316
+ )
307
317
  },
308
318
  this.saveDebounceRate
309
319
  )
310
320
  }
311
321
 
312
- handler(message)
322
+ handler(payload)
313
323
  }
314
324
 
315
325
  /** Returns an existing handle if we have it; creates one otherwise. */
316
- #getHandle<T>(
326
+ #getHandle<T>({
327
+ documentId,
328
+ isNew,
329
+ initialValue,
330
+ }: {
317
331
  /** The documentId of the handle to look up or create */
318
- documentId: DocumentId,
319
-
320
- /** If we know we're creating a new document, specify this so we can have access to it immediately */
332
+ documentId: DocumentId /** If we know we're creating a new document, specify this so we can have access to it immediately */
321
333
  isNew: boolean
322
- ) {
334
+ initialValue?: T
335
+ }) {
323
336
  // If we have the handle cached, return it
324
337
  if (this.#handleCache[documentId]) return this.#handleCache[documentId]
325
338
 
326
339
  // If not, create a new handle, cache it, and return it
327
340
  if (!documentId) throw new Error(`Invalid documentId ${documentId}`)
328
- const handle = new DocHandle<T>(documentId, { isNew })
341
+ const handle = new DocHandle<T>(documentId, { isNew, initialValue })
329
342
  this.#handleCache[documentId] = handle
330
343
  return handle
331
344
  }
@@ -345,32 +358,18 @@ export class Repo extends EventEmitter<RepoEvents> {
345
358
  }
346
359
 
347
360
  /**
348
- * Creates a new document and returns a handle to it. The initial value of the document is
349
- * an empty object `{}`. Its documentId is generated by the system. we emit a `document` event
350
- * to advertise interest in the document.
361
+ * Creates a new document and returns a handle to it. The initial value of the document is an
362
+ * empty object `{}` unless an initial value is provided. Its documentId is generated by the
363
+ * system. we emit a `document` event to advertise interest in the document.
351
364
  */
352
- create<T>(): DocHandle<T> {
353
- // TODO:
354
- // either
355
- // - pass an initial value and do something like this to ensure that you get a valid initial value
356
-
357
- // const myInitialValue = {
358
- // tasks: [],
359
- // filter: "all",
360
- //
361
- // const guaranteeInitialValue = (doc: any) => {
362
- // if (!doc.tasks) doc.tasks = []
363
- // if (!doc.filter) doc.filter = "all"
364
-
365
- // return { ...myInitialValue, ...doc }
366
- // }
367
-
368
- // or
369
- // - pass a "reify" function that takes a `<any>` and returns `<T>`
370
-
365
+ create<T>(initialValue?: T): DocHandle<T> {
371
366
  // Generate a new UUID and store it in the buffer
372
367
  const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
373
- const handle = this.#getHandle<T>(documentId, true) as DocHandle<T>
368
+ const handle = this.#getHandle<T>({
369
+ documentId,
370
+ isNew: true,
371
+ initialValue,
372
+ }) as DocHandle<T>
374
373
  this.emit("document", { handle, isNew: true })
375
374
  return handle
376
375
  }
@@ -436,7 +435,10 @@ export class Repo extends EventEmitter<RepoEvents> {
436
435
  return this.#handleCache[documentId]
437
436
  }
438
437
 
439
- const handle = this.#getHandle<T>(documentId, false) as DocHandle<T>
438
+ const handle = this.#getHandle<T>({
439
+ documentId,
440
+ isNew: false,
441
+ }) as DocHandle<T>
440
442
  this.emit("document", { handle, isNew: false })
441
443
  return handle
442
444
  }
@@ -447,13 +449,29 @@ export class Repo extends EventEmitter<RepoEvents> {
447
449
  ) {
448
450
  const documentId = interpretAsDocumentId(id)
449
451
 
450
- const handle = this.#getHandle(documentId, false)
452
+ const handle = this.#getHandle({ documentId, isNew: false })
451
453
  handle.delete()
452
454
 
453
455
  delete this.#handleCache[documentId]
454
456
  this.emit("delete-document", { documentId })
455
457
  }
456
458
 
459
+ /**
460
+ * Exports a document to a binary format.
461
+ * @param id - The url or documentId of the handle to export
462
+ *
463
+ * @returns Promise<Uint8Array | undefined> - A Promise containing the binary document,
464
+ * or undefined if the document is unavailable.
465
+ */
466
+ async export(id: AnyDocumentId): Promise<Uint8Array | undefined> {
467
+ const documentId = interpretAsDocumentId(id)
468
+
469
+ const handle = this.#getHandle({ documentId, isNew: false })
470
+ const doc = await handle.doc()
471
+ if (!doc) return undefined
472
+ return Automerge.save(doc)
473
+ }
474
+
457
475
  /**
458
476
  * Imports document binary into the repo.
459
477
  * @param binary - The binary to import
@@ -471,8 +489,14 @@ export class Repo extends EventEmitter<RepoEvents> {
471
489
  }
472
490
 
473
491
  subscribeToRemotes = (remotes: StorageId[]) => {
474
- this.#log("subscribeToRemotes", { remotes })
475
- this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes)
492
+ if (this.#remoteHeadsGossipingEnabled) {
493
+ this.#log("subscribeToRemotes", { remotes })
494
+ this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes)
495
+ } else {
496
+ this.#log(
497
+ "WARN: subscribeToRemotes called but remote heads gossiping is not enabled"
498
+ )
499
+ }
476
500
  }
477
501
 
478
502
  storageId = async (): Promise<StorageId | undefined> => {
@@ -503,6 +527,11 @@ export interface RepoConfig {
503
527
  * all peers). A server only syncs documents that a peer explicitly requests by ID.
504
528
  */
505
529
  sharePolicy?: SharePolicy
530
+
531
+ /**
532
+ * Whether to enable the experimental remote heads gossiping feature
533
+ */
534
+ enableRemoteHeadsGossiping?: boolean
506
535
  }
507
536
 
508
537
  /** A function that determines whether we should share a document with a peer
@@ -1,14 +1,6 @@
1
+ /* c8 ignore start */
2
+
1
3
  export const pause = (t = 0) =>
2
4
  new Promise<void>(resolve => setTimeout(() => resolve(), t))
3
5
 
4
- export function rejectOnTimeout<T>(
5
- promise: Promise<T>,
6
- millis: number
7
- ): Promise<T> {
8
- return Promise.race([
9
- promise,
10
- pause(millis).then(() => {
11
- throw new Error("timeout exceeded")
12
- }),
13
- ])
14
- }
6
+ /* c8 ignore end */
@@ -1,3 +1,4 @@
1
+ /* c8 ignore start */
1
2
  /**
2
3
  * If `promise` is resolved before `t` ms elapse, the timeout is cleared and the result of the
3
4
  * promise is returned. If the timeout ends first, a `TimeoutError` is thrown.
@@ -26,3 +27,4 @@ export class TimeoutError extends Error {
26
27
  this.name = "TimeoutError"
27
28
  }
28
29
  }
30
+ /* c8 ignore end */
package/src/index.ts CHANGED
@@ -34,7 +34,7 @@ export {
34
34
  } from "./AutomergeUrl.js"
35
35
  export { Repo } from "./Repo.js"
36
36
  export { NetworkAdapter } from "./network/NetworkAdapter.js"
37
- export { isValidRepoMessage } from "./network/messages.js"
37
+ export { isRepoMessage } from "./network/messages.js"
38
38
  export { StorageAdapter } from "./storage/StorageAdapter.js"
39
39
 
40
40
  /** @hidden **/
@@ -1,13 +1,15 @@
1
+ /* c8 ignore start */
2
+
1
3
  import { EventEmitter } from "eventemitter3"
2
4
  import { PeerId } from "../types.js"
3
5
  import { Message } from "./messages.js"
4
6
  import { StorageId } from "../storage/types.js"
5
7
 
6
- /**
8
+ /**
7
9
  * Describes a peer intent to the system
8
10
  * storageId: the key for syncState to decide what the other peer already has
9
11
  * isEphemeral: to decide if we bother recording this peer's sync state
10
- *
12
+ *
11
13
  */
12
14
  export interface PeerMetadata {
13
15
  storageId?: StorageId
@@ -11,7 +11,7 @@ import {
11
11
  MessageContents,
12
12
  RepoMessage,
13
13
  isEphemeralMessage,
14
- isValidRepoMessage,
14
+ isRepoMessage,
15
15
  } from "./messages.js"
16
16
 
17
17
  type EphemeralMessageSource = `${PeerId}:${SessionId}`
@@ -73,7 +73,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
73
73
  })
74
74
 
75
75
  networkAdapter.on("message", msg => {
76
- if (!isValidRepoMessage(msg)) {
76
+ if (!isRepoMessage(msg)) {
77
77
  this.#log(`invalid message: ${JSON.stringify(msg)}`)
78
78
  return
79
79
  }
@@ -146,7 +146,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
146
146
  }
147
147
 
148
148
  const outbound = prepareMessage(message)
149
- this.#log("sending message", outbound)
149
+ this.#log("sending message %o", outbound)
150
150
  peer.send(outbound as RepoMessage)
151
151
  }
152
152
 
@@ -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 =>
@@ -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
  }