@automerge/automerge-repo 1.1.0-alpha.1 → 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 (58) hide show
  1. package/README.md +12 -7
  2. package/dist/AutomergeUrl.js +2 -2
  3. package/dist/RemoteHeadsSubscriptions.d.ts +1 -0
  4. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
  5. package/dist/RemoteHeadsSubscriptions.js +76 -16
  6. package/dist/Repo.d.ts +23 -10
  7. package/dist/Repo.d.ts.map +1 -1
  8. package/dist/Repo.js +103 -54
  9. package/dist/helpers/debounce.js +1 -1
  10. package/dist/helpers/pause.d.ts.map +1 -1
  11. package/dist/helpers/pause.js +2 -0
  12. package/dist/helpers/throttle.js +1 -1
  13. package/dist/helpers/withTimeout.d.ts.map +1 -1
  14. package/dist/helpers/withTimeout.js +2 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/network/NetworkAdapter.d.ts +14 -7
  19. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  20. package/dist/network/NetworkAdapter.js +3 -3
  21. package/dist/network/NetworkSubsystem.d.ts +4 -8
  22. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  23. package/dist/network/NetworkSubsystem.js +12 -13
  24. package/dist/network/messages.d.ts +48 -38
  25. package/dist/network/messages.d.ts.map +1 -1
  26. package/dist/network/messages.js +7 -9
  27. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  28. package/dist/storage/StorageSubsystem.js +7 -2
  29. package/dist/storage/keyHash.d.ts.map +1 -1
  30. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  31. package/dist/synchronizer/CollectionSynchronizer.js +5 -3
  32. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  33. package/dist/synchronizer/DocSynchronizer.js +20 -8
  34. package/dist/synchronizer/Synchronizer.d.ts +12 -3
  35. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  36. package/package.json +6 -6
  37. package/src/AutomergeUrl.ts +2 -2
  38. package/src/RemoteHeadsSubscriptions.ts +85 -16
  39. package/src/Repo.ts +131 -68
  40. package/src/helpers/debounce.ts +1 -1
  41. package/src/helpers/pause.ts +4 -0
  42. package/src/helpers/throttle.ts +1 -1
  43. package/src/helpers/withTimeout.ts +2 -0
  44. package/src/index.ts +2 -1
  45. package/src/network/NetworkAdapter.ts +18 -12
  46. package/src/network/NetworkSubsystem.ts +23 -24
  47. package/src/network/messages.ts +77 -68
  48. package/src/storage/StorageSubsystem.ts +7 -2
  49. package/src/storage/keyHash.ts +2 -0
  50. package/src/synchronizer/CollectionSynchronizer.ts +7 -4
  51. package/src/synchronizer/DocSynchronizer.ts +27 -15
  52. package/src/synchronizer/Synchronizer.ts +13 -3
  53. package/test/RemoteHeadsSubscriptions.test.ts +34 -24
  54. package/test/Repo.test.ts +57 -2
  55. package/test/StorageSubsystem.test.ts +1 -1
  56. package/test/helpers/waitForMessages.ts +22 -0
  57. package/test/remoteHeads.test.ts +197 -72
  58. package/.eslintrc +0 -28
@@ -1,8 +1,21 @@
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
 
8
+ /**
9
+ * Describes a peer intent to the system
10
+ * storageId: the key for syncState to decide what the other peer already has
11
+ * isEphemeral: to decide if we bother recording this peer's sync state
12
+ *
13
+ */
14
+ export interface PeerMetadata {
15
+ storageId?: StorageId
16
+ isEphemeral?: boolean
17
+ }
18
+
6
19
  /** An interface representing some way to connect to other peers
7
20
  *
8
21
  * @remarks
@@ -11,21 +24,15 @@ import { StorageId } from "../storage/types.js"
11
24
  * until the adapter emits a `ready` event before it starts trying to use it
12
25
  */
13
26
  export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
14
- peerId?: PeerId // hmmm, maybe not
15
- storageId?: StorageId
16
- isEphemeral = true
27
+ peerId?: PeerId
28
+ peerMetadata?: PeerMetadata
17
29
 
18
30
  /** Called by the {@link Repo} to start the connection process
19
31
  *
20
32
  * @argument peerId - the peerId of this repo
21
- * @argument storageId - the storage id of the peer
22
- * @argument isEphemeral - weather or not the other end should persist our sync state
33
+ * @argument peerMetadata - how this adapter should present itself to other peers
23
34
  */
24
- abstract connect(
25
- peerId: PeerId,
26
- storageId: StorageId | undefined,
27
- isEphemeral: boolean
28
- ): void
35
+ abstract connect(peerId: PeerId, peerMetadata?: PeerMetadata): void
29
36
 
30
37
  /** Called by the {@link Repo} to send a message to a peer
31
38
  *
@@ -62,8 +69,7 @@ export interface OpenPayload {
62
69
 
63
70
  export interface PeerCandidatePayload {
64
71
  peerId: PeerId
65
- storageId?: StorageId
66
- isEphemeral: boolean
72
+ peerMetadata: PeerMetadata
67
73
  }
68
74
 
69
75
  export interface PeerDisconnectedPayload {
@@ -1,15 +1,18 @@
1
1
  import debug from "debug"
2
2
  import { EventEmitter } from "eventemitter3"
3
3
  import { PeerId, SessionId } from "../types.js"
4
- import { NetworkAdapter, PeerDisconnectedPayload } from "./NetworkAdapter.js"
4
+ import type {
5
+ NetworkAdapter,
6
+ PeerDisconnectedPayload,
7
+ PeerMetadata,
8
+ } from "./NetworkAdapter.js"
5
9
  import {
6
10
  EphemeralMessage,
7
11
  MessageContents,
8
12
  RepoMessage,
9
13
  isEphemeralMessage,
10
- isValidRepoMessage,
14
+ isRepoMessage,
11
15
  } from "./messages.js"
12
- import { StorageId } from "../storage/types.js"
13
16
 
14
17
  type EphemeralMessageSource = `${PeerId}:${SessionId}`
15
18
 
@@ -29,8 +32,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
29
32
  constructor(
30
33
  adapters: NetworkAdapter[],
31
34
  public peerId = randomPeerId(),
32
- private storageId: Promise<StorageId | undefined>, // todo: we shouldn't pass a promise here
33
- private isEphemeral: boolean
35
+ private peerMetadata: Promise<PeerMetadata>
34
36
  ) {
35
37
  super()
36
38
  this.#log = debug(`automerge-repo:network:${this.peerId}`)
@@ -52,21 +54,17 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
52
54
  }
53
55
  })
54
56
 
55
- networkAdapter.on(
56
- "peer-candidate",
57
- ({ peerId, storageId, isEphemeral }) => {
58
- this.#log(`peer candidate: ${peerId} `)
57
+ networkAdapter.on("peer-candidate", ({ peerId, peerMetadata }) => {
58
+ this.#log(`peer candidate: ${peerId} `)
59
+ // TODO: This is where authentication would happen
59
60
 
60
- // TODO: This is where authentication would happen
61
-
62
- if (!this.#adaptersByPeer[peerId]) {
63
- // TODO: handle losing a server here
64
- this.#adaptersByPeer[peerId] = networkAdapter
65
- }
66
-
67
- this.emit("peer", { peerId, storageId, isEphemeral })
61
+ if (!this.#adaptersByPeer[peerId]) {
62
+ // TODO: handle losing a server here
63
+ this.#adaptersByPeer[peerId] = networkAdapter
68
64
  }
69
- )
65
+
66
+ this.emit("peer", { peerId, peerMetadata })
67
+ })
70
68
 
71
69
  networkAdapter.on("peer-disconnected", ({ peerId }) => {
72
70
  this.#log(`peer disconnected: ${peerId} `)
@@ -75,7 +73,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
75
73
  })
76
74
 
77
75
  networkAdapter.on("message", msg => {
78
- if (!isValidRepoMessage(msg)) {
76
+ if (!isRepoMessage(msg)) {
79
77
  this.#log(`invalid message: ${JSON.stringify(msg)}`)
80
78
  return
81
79
  }
@@ -107,8 +105,10 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
107
105
  })
108
106
  })
109
107
 
110
- this.storageId.then(storageId => {
111
- networkAdapter.connect(this.peerId, storageId, this.isEphemeral)
108
+ this.peerMetadata.then(peerMetadata => {
109
+ networkAdapter.connect(this.peerId, peerMetadata)
110
+ }).catch(err => {
111
+ this.#log("error connecting to network", err)
112
112
  })
113
113
  }
114
114
 
@@ -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
 
@@ -182,6 +182,5 @@ export interface NetworkSubsystemEvents {
182
182
 
183
183
  export interface PeerPayload {
184
184
  peerId: PeerId
185
- storageId?: StorageId
186
- isEphemeral: boolean
185
+ peerMetadata: PeerMetadata
187
186
  }
@@ -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,48 +82,44 @@ 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 */
95
+ /**
96
+ * Sent by a {@link Repo} to add or remove storage IDs from a remote peer's subscription.
97
+ */
98
+ export type RemoteSubscriptionControlMessage = {
99
+ type: "remote-subscription-change"
97
100
  senderId: PeerId
98
-
99
- /** The peer ID of the recipient of this message */
100
101
  targetId: PeerId
101
102
 
102
- /** The payload of the auth message (up to the specific auth provider) */
103
- payload: TPayload
104
- }
103
+ /** The storage IDs to add to the subscription */
104
+ add?: StorageId[]
105
105
 
106
- export type RemoteSubscriptionControlMessage = {
107
- type: "remote-subscription-change",
108
- senderId: PeerId,
109
- targetId: PeerId,
110
- add?: StorageId[],
111
- remove?: StorageId[],
106
+ /** The storage IDs to remove from the subscription */
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
- type: "remote-heads-changed",
116
- senderId: PeerId,
117
- targetId: PeerId,
118
- documentId: DocumentId,
119
- newHeads: {[key: StorageId]: {heads: string[], timestamp: number}},
114
+ type: "remote-heads-changed"
115
+ senderId: PeerId
116
+ targetId: PeerId
117
+
118
+ /** The document ID of the document that has changed */
119
+ documentId: DocumentId
120
+
121
+ /** The document's new heads */
122
+ newHeads: { [key: StorageId]: { heads: string[]; timestamp: number } }
120
123
  }
121
124
 
122
125
  /** These are message types that a {@link NetworkAdapter} surfaces to a {@link Repo}. */
@@ -128,15 +131,17 @@ export type RepoMessage =
128
131
  | RemoteSubscriptionControlMessage
129
132
  | RemoteHeadsChanged
130
133
 
131
- export type DocMessage = SyncMessage | EphemeralMessage | RequestMessage | DocumentUnavailableMessage
132
-
133
- /** These are all the message types that a {@link NetworkAdapter} might see. */
134
- export type Message = RepoMessage | AuthMessage
134
+ /** These are message types that are handled by the {@link CollectionSynchronizer}.*/
135
+ export type DocMessage =
136
+ | SyncMessage
137
+ | EphemeralMessage
138
+ | RequestMessage
139
+ | DocumentUnavailableMessage
135
140
 
136
141
  /**
137
142
  * The contents of a message, without the sender ID or other properties added by the {@link NetworkSubsystem})
138
143
  */
139
- export type MessageContents<T extends Message = Message> =
144
+ export type MessageContents<T extends Message = RepoMessage> =
140
145
  T extends EphemeralMessage
141
146
  ? Omit<T, "senderId" | "count" | "sessionId">
142
147
  : Omit<T, "senderId">
@@ -148,18 +153,21 @@ export interface SyncStateMessage {
148
153
  syncState: SyncState
149
154
  }
150
155
 
156
+ /** Notify the repo that a peer started syncing with a doc */
157
+ export interface OpenDocMessage {
158
+ peerId: PeerId
159
+ documentId: DocumentId
160
+ }
161
+
151
162
  // TYPE GUARDS
152
163
 
153
- export const isValidRepoMessage = (message: Message): message is RepoMessage =>
154
- typeof message === "object" &&
155
- typeof message.type === "string" &&
156
- typeof message.senderId === "string" &&
157
- (isSyncMessage(message) ||
158
- isEphemeralMessage(message) ||
159
- isRequestMessage(message) ||
160
- isDocumentUnavailableMessage(message) ||
161
- isRemoteSubscriptionControlMessage(message) ||
162
- 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)
163
171
 
164
172
  // prettier-ignore
165
173
  export const isDocumentUnavailableMessage = (msg: Message): msg is DocumentUnavailableMessage =>
@@ -174,6 +182,7 @@ export const isSyncMessage = (msg: Message): msg is SyncMessage =>
174
182
  export const isEphemeralMessage = (msg: Message): msg is EphemeralMessage =>
175
183
  msg.type === "ephemeral"
176
184
 
185
+ // prettier-ignore
177
186
  export const isRemoteSubscriptionControlMessage = (msg: Message): msg is RemoteSubscriptionControlMessage =>
178
187
  msg.type === "remote-subscription-change"
179
188
 
@@ -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) {
@@ -279,6 +279,11 @@ export class StorageSubsystem {
279
279
  incrementalSize += chunk.size
280
280
  }
281
281
  }
282
- return incrementalSize >= snapshotSize
282
+ // if the file is currently small, don't worry, just compact
283
+ // this might seem a bit arbitrary (1k is arbitrary) but is designed to ensure compaction
284
+ // for documents with only a single large change on top of an empty (or nearly empty) document
285
+ // for example: imported NPM modules, images, etc.
286
+ // if we have even more incrementals (so far) than the snapshot, compact
287
+ return snapshotSize < 1024 || incrementalSize >= snapshotSize
283
288
  }
284
289
  }
@@ -7,11 +7,13 @@ export function keyHash(binary: Uint8Array) {
7
7
  const hash = sha256.hash(binary)
8
8
  return bufferToHexString(hash)
9
9
  }
10
+
10
11
  export function headsHash(heads: A.Heads): string {
11
12
  const encoder = new TextEncoder()
12
13
  const headsbinary = mergeArrays(heads.map((h: string) => encoder.encode(h)))
13
14
  return keyHash(headsbinary)
14
15
  }
16
+
15
17
  function bufferToHexString(data: Uint8Array) {
16
18
  return Array.from(data, byte => byte.toString(16).padStart(2, "0")).join("")
17
19
  }
@@ -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"
@@ -42,18 +42,20 @@ export class CollectionSynchronizer extends Synchronizer {
42
42
  return
43
43
  }
44
44
 
45
- const persistanceInfo = this.repo.persistanceInfoByPeerId[peerId]
46
- if (!persistanceInfo || persistanceInfo.isEphemeral) {
45
+ const { storageId, isEphemeral } =
46
+ this.repo.peerMetadataByPeerId[peerId] || {}
47
+ if (!storageId || isEphemeral) {
47
48
  return
48
49
  }
49
50
 
50
51
  return this.repo.storageSubsystem.loadSyncState(
51
52
  handle.documentId,
52
- persistanceInfo.storageId
53
+ storageId
53
54
  )
54
55
  },
55
56
  })
56
57
  docSynchronizer.on("message", event => this.emit("message", event))
58
+ docSynchronizer.on("open-doc", event => this.emit("open-doc", event))
57
59
  docSynchronizer.on("sync-state", event => this.emit("sync-state", event))
58
60
  return docSynchronizer
59
61
  }
@@ -115,6 +117,7 @@ export class CollectionSynchronizer extends Synchronizer {
115
117
  }
116
118
 
117
119
  // TODO: implement this
120
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
118
121
  removeDocument(documentId: DocumentId) {
119
122
  throw new Error("not implemented")
120
123
  }
@@ -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) {
@@ -256,11 +264,15 @@ export class DocSynchronizer extends Synchronizer {
256
264
  )
257
265
  this.#setSyncState(peerId, reparsedSyncState)
258
266
 
259
- docPromise.then(doc => {
260
- if (doc) {
261
- this.#sendSyncMessage(peerId, doc)
262
- }
263
- })
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
+ })
264
276
  })
265
277
  })
266
278
  }
@@ -322,10 +334,10 @@ export class DocSynchronizer extends Synchronizer {
322
334
  }
323
335
 
324
336
  this.#processAllPendingSyncMessages()
325
- this.#processSyncMessage(message, new Date())
337
+ this.#processSyncMessage(message)
326
338
  }
327
339
 
328
- #processSyncMessage(message: SyncMessage | RequestMessage, received: Date) {
340
+ #processSyncMessage(message: SyncMessage | RequestMessage) {
329
341
  if (isRequestMessage(message)) {
330
342
  this.#peerDocumentStatuses[message.senderId] = "wants"
331
343
  }
@@ -384,7 +396,7 @@ export class DocSynchronizer extends Synchronizer {
384
396
 
385
397
  #processAllPendingSyncMessages() {
386
398
  for (const message of this.#pendingSyncMessages) {
387
- this.#processSyncMessage(message.message, message.received)
399
+ this.#processSyncMessage(message.message)
388
400
  }
389
401
 
390
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
  }