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

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 (47) hide show
  1. package/dist/DocHandle.d.ts +6 -5
  2. package/dist/DocHandle.d.ts.map +1 -1
  3. package/dist/DocHandle.js +7 -7
  4. package/dist/RemoteHeadsSubscriptions.d.ts +41 -0
  5. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -0
  6. package/dist/RemoteHeadsSubscriptions.js +224 -0
  7. package/dist/Repo.d.ts +11 -2
  8. package/dist/Repo.d.ts.map +1 -1
  9. package/dist/Repo.js +117 -8
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/network/NetworkAdapter.d.ts +15 -1
  13. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  14. package/dist/network/NetworkAdapter.js +1 -0
  15. package/dist/network/NetworkSubsystem.d.ts +4 -2
  16. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  17. package/dist/network/NetworkSubsystem.js +8 -4
  18. package/dist/network/messages.d.ts +24 -1
  19. package/dist/network/messages.d.ts.map +1 -1
  20. package/dist/network/messages.js +5 -1
  21. package/dist/storage/StorageSubsystem.d.ts +5 -3
  22. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  23. package/dist/storage/StorageSubsystem.js +23 -5
  24. package/dist/storage/types.d.ts +4 -0
  25. package/dist/storage/types.d.ts.map +1 -1
  26. package/dist/synchronizer/CollectionSynchronizer.d.ts +2 -2
  27. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  28. package/dist/synchronizer/CollectionSynchronizer.js +7 -3
  29. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  30. package/dist/synchronizer/DocSynchronizer.js +0 -9
  31. package/package.json +3 -3
  32. package/src/DocHandle.ts +10 -9
  33. package/src/RemoteHeadsSubscriptions.ts +306 -0
  34. package/src/Repo.ts +172 -12
  35. package/src/index.ts +2 -0
  36. package/src/network/NetworkAdapter.ts +19 -1
  37. package/src/network/NetworkSubsystem.ts +17 -6
  38. package/src/network/messages.ts +30 -1
  39. package/src/storage/StorageSubsystem.ts +30 -7
  40. package/src/storage/types.ts +3 -0
  41. package/src/synchronizer/CollectionSynchronizer.ts +11 -5
  42. package/src/synchronizer/DocSynchronizer.ts +0 -12
  43. package/test/DocHandle.test.ts +0 -17
  44. package/test/RemoteHeadsSubscriptions.test.ts +343 -0
  45. package/test/Repo.test.ts +51 -15
  46. package/test/StorageSubsystem.test.ts +28 -6
  47. package/test/remoteHeads.test.ts +135 -0
@@ -0,0 +1,306 @@
1
+ import { next as A } from "@automerge/automerge"
2
+ import { EventEmitter } from "eventemitter3"
3
+ import { DocumentId, PeerId } from "./types.js"
4
+ import {
5
+ RemoteHeadsChanged,
6
+ RemoteSubscriptionControlMessage,
7
+ } from "./network/messages.js"
8
+ import { StorageId } from "./index.js"
9
+ import debug from "debug"
10
+
11
+ // Notify a DocHandle that remote heads have changed
12
+ export type RemoteHeadsSubscriptionEventPayload = {
13
+ documentId: DocumentId
14
+ storageId: StorageId
15
+ remoteHeads: A.Heads
16
+ timestamp: number
17
+ }
18
+
19
+ // Send a message to the given peer notifying them of new heads
20
+ export type NotifyRemoteHeadsPayload = {
21
+ targetId: PeerId
22
+ documentId: DocumentId
23
+ storageId: StorageId
24
+ heads: A.Heads
25
+ timestamp: number
26
+ }
27
+
28
+ type RemoteHeadsSubscriptionEvents = {
29
+ "remote-heads-changed": (payload: RemoteHeadsSubscriptionEventPayload) => void
30
+ "change-remote-subs": (payload: {
31
+ peers: PeerId[]
32
+ add?: StorageId[]
33
+ remove?: StorageId[]
34
+ }) => void
35
+ "notify-remote-heads": (payload: NotifyRemoteHeadsPayload) => void
36
+ }
37
+
38
+ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscriptionEvents> {
39
+ // Storage IDs we have received remote heads from
40
+ #knownHeads: Map<DocumentId, Map<StorageId, LastHeads>> = new Map()
41
+ // Storage IDs we have subscribed to via Repo.subscribeToRemoteHeads
42
+ #ourSubscriptions: Set<StorageId> = new Set()
43
+ // Storage IDs other peers have subscribed to by sending us a control message
44
+ #theirSubscriptions: Map<StorageId, Set<PeerId>> = new Map()
45
+ // Peers we will always share remote heads with even if they are not subscribed
46
+ #generousPeers: Set<PeerId> = new Set()
47
+ #log = debug("automerge-repo:remote-heads-subscriptions")
48
+
49
+ subscribeToRemotes(remotes: StorageId[]) {
50
+ this.#log("subscribeToRemotes", remotes)
51
+ const remotesToAdd = []
52
+ for (const remote of remotes) {
53
+ if (!this.#ourSubscriptions.has(remote)) {
54
+ this.#ourSubscriptions.add(remote)
55
+ remotesToAdd.push(remote)
56
+ }
57
+ }
58
+
59
+ if (remotesToAdd.length > 0) {
60
+ this.emit("change-remote-subs", {
61
+ add: remotesToAdd,
62
+ peers: Array.from(this.#generousPeers),
63
+ })
64
+ }
65
+ }
66
+
67
+ unsubscribeFromRemotes(remotes: StorageId[]) {
68
+ this.#log("subscribeToRemotes", remotes)
69
+ const remotesToRemove = []
70
+
71
+ for (const remote of remotes) {
72
+ if (this.#ourSubscriptions.has(remote)) {
73
+ this.#ourSubscriptions.delete(remote)
74
+
75
+ if (!this.#theirSubscriptions.has(remote)) {
76
+ remotesToRemove.push(remote)
77
+ }
78
+ }
79
+ }
80
+
81
+ if (remotesToRemove.length > 0) {
82
+ this.emit("change-remote-subs", {
83
+ remove: remotesToRemove,
84
+ peers: Array.from(this.#generousPeers),
85
+ })
86
+ }
87
+ }
88
+
89
+ handleControlMessage(control: RemoteSubscriptionControlMessage) {
90
+ const remotesToAdd: StorageId[] = []
91
+ const remotesToRemove: StorageId[] = []
92
+
93
+ this.#log("handleControlMessage", control)
94
+ if (control.add) {
95
+ for (const remote of control.add) {
96
+ let theirSubs = this.#theirSubscriptions.get(remote)
97
+ if (!theirSubs) {
98
+ theirSubs = new Set()
99
+ this.#theirSubscriptions.set(remote, theirSubs)
100
+
101
+ if (!this.#ourSubscriptions.has(remote)) {
102
+ remotesToAdd.push(remote)
103
+ }
104
+ }
105
+
106
+ theirSubs.add(control.senderId)
107
+ }
108
+ }
109
+
110
+ if (control.remove) {
111
+ for (const remote of control.remove) {
112
+ const theirSubs = this.#theirSubscriptions.get(remote)
113
+ if (theirSubs) {
114
+ theirSubs.delete(control.senderId)
115
+
116
+ // if no one is subscribed anymore remove remote
117
+ if (theirSubs.size == 0 && !this.#ourSubscriptions.has(remote)) {
118
+ remotesToRemove.push(remote)
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ if (remotesToAdd.length > 0 || remotesToRemove.length > 0) {
125
+ this.emit("change-remote-subs", {
126
+ peers: Array.from(this.#generousPeers),
127
+ add: remotesToAdd,
128
+ remove: remotesToRemove,
129
+ })
130
+ }
131
+ }
132
+
133
+ /** A peer we are not directly connected to has changed their heads */
134
+ handleRemoteHeads(msg: RemoteHeadsChanged) {
135
+ this.#log("handleRemoteHeads", msg)
136
+ const changedHeads = this.#changedHeads(msg)
137
+
138
+ // Emit a remote-heads-changed event to update local dochandles
139
+ for (const event of changedHeads) {
140
+ if (this.#ourSubscriptions.has(event.storageId)) {
141
+ this.emit("remote-heads-changed", event)
142
+ }
143
+ }
144
+
145
+ // Notify generous peers of these changes regardless of if they are subscribed to us
146
+ for (const event of changedHeads) {
147
+ for (const peer of this.#generousPeers) {
148
+ // don't emit event to sender if sender is a generous peer
149
+ if (peer === msg.senderId) {
150
+ continue
151
+ }
152
+
153
+ this.emit("notify-remote-heads", {
154
+ targetId: peer,
155
+ documentId: event.documentId,
156
+ heads: event.remoteHeads,
157
+ timestamp: event.timestamp,
158
+ storageId: event.storageId,
159
+ })
160
+ }
161
+ }
162
+
163
+ // Notify subscribers of these changes
164
+ for (const event of changedHeads) {
165
+ const theirSubs = this.#theirSubscriptions.get(event.storageId)
166
+ if (theirSubs) {
167
+ for (const peerId of theirSubs) {
168
+ this.emit("notify-remote-heads", {
169
+ targetId: peerId,
170
+ documentId: event.documentId,
171
+ heads: event.remoteHeads,
172
+ timestamp: event.timestamp,
173
+ storageId: event.storageId,
174
+ })
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ /** A peer we are directly connected to has updated their heads */
181
+ handleImmediateRemoteHeadsChanged(
182
+ documentId: DocumentId,
183
+ storageId: StorageId,
184
+ heads: A.Heads
185
+ ) {
186
+ this.#log("handleLocalHeadsChanged", documentId, storageId, heads)
187
+ const remote = this.#knownHeads.get(documentId)
188
+ const timestamp = Date.now()
189
+ if (!remote) {
190
+ this.#knownHeads.set(
191
+ documentId,
192
+ new Map([[storageId, { heads, timestamp }]])
193
+ )
194
+ } else {
195
+ const docRemote = remote.get(storageId)
196
+ if (!docRemote || docRemote.timestamp < Date.now()) {
197
+ remote.set(storageId, { heads, timestamp: Date.now() })
198
+ }
199
+ }
200
+ const theirSubs = this.#theirSubscriptions.get(storageId)
201
+ if (theirSubs) {
202
+ for (const peerId of theirSubs) {
203
+ this.emit("notify-remote-heads", {
204
+ targetId: peerId,
205
+ documentId: documentId,
206
+ heads: heads,
207
+ timestamp: timestamp,
208
+ storageId: storageId,
209
+ })
210
+ }
211
+ }
212
+ }
213
+
214
+ addGenerousPeer(peerId: PeerId) {
215
+ this.#log("addGenerousPeer", peerId)
216
+ this.#generousPeers.add(peerId)
217
+
218
+ if (this.#ourSubscriptions.size > 0) {
219
+ this.emit("change-remote-subs", {
220
+ add: Array.from(this.#ourSubscriptions),
221
+ peers: [peerId],
222
+ })
223
+ }
224
+
225
+ for (const [documentId, remote] of this.#knownHeads) {
226
+ for (const [storageId, { heads, timestamp }] of remote) {
227
+ this.emit("notify-remote-heads", {
228
+ targetId: peerId,
229
+ documentId: documentId,
230
+ heads: heads,
231
+ timestamp: timestamp,
232
+ storageId: storageId,
233
+ })
234
+ }
235
+ }
236
+ }
237
+
238
+ removePeer(peerId: PeerId) {
239
+ this.#log("removePeer", peerId)
240
+
241
+ const remotesToRemove = []
242
+
243
+ this.#generousPeers.delete(peerId)
244
+
245
+ for (const [storageId, peerIds] of this.#theirSubscriptions) {
246
+ if (peerIds.has(peerId)) {
247
+ peerIds.delete(peerId)
248
+
249
+ if (peerIds.size == 0) {
250
+ remotesToRemove.push(storageId)
251
+ this.#theirSubscriptions.delete(storageId)
252
+ }
253
+ }
254
+ }
255
+
256
+ if (remotesToRemove.length > 0) {
257
+ this.emit("change-remote-subs", {
258
+ remove: remotesToRemove,
259
+ peers: Array.from(this.#generousPeers),
260
+ })
261
+ }
262
+ }
263
+
264
+ /** Returns the (document, storageId) pairs which have changed after processing msg */
265
+ #changedHeads(msg: RemoteHeadsChanged): {
266
+ documentId: DocumentId
267
+ storageId: StorageId
268
+ remoteHeads: A.Heads
269
+ timestamp: number
270
+ }[] {
271
+ const changedHeads = []
272
+ const { documentId, newHeads } = msg
273
+ for (const [storageId, { heads, timestamp }] of Object.entries(newHeads)) {
274
+ if (
275
+ !this.#ourSubscriptions.has(storageId as StorageId) &&
276
+ !this.#theirSubscriptions.has(storageId as StorageId)
277
+ ) {
278
+ continue
279
+ }
280
+ let remote = this.#knownHeads.get(documentId)
281
+ if (!remote) {
282
+ remote = new Map([[storageId as StorageId, { heads, timestamp }]])
283
+ this.#knownHeads.set(documentId, remote)
284
+ }
285
+
286
+ const docRemote = remote.get(storageId as StorageId)
287
+ if (docRemote && docRemote.timestamp > timestamp) {
288
+ continue
289
+ } else {
290
+ remote.set(storageId as StorageId, { timestamp, heads })
291
+ changedHeads.push({
292
+ documentId,
293
+ storageId: storageId as StorageId,
294
+ remoteHeads: heads,
295
+ timestamp,
296
+ })
297
+ }
298
+ }
299
+ return changedHeads
300
+ }
301
+ }
302
+
303
+ type LastHeads = {
304
+ timestamp: number
305
+ heads: A.Heads
306
+ }
package/src/Repo.ts CHANGED
@@ -8,13 +8,16 @@ import {
8
8
  } from "./AutomergeUrl.js"
9
9
  import { DocHandle, DocHandleEncodedChangePayload } from "./DocHandle.js"
10
10
  import { throttle } from "./helpers/throttle.js"
11
- import { NetworkAdapter } from "./network/NetworkAdapter.js"
11
+ import { NetworkAdapter, type PeerMetadata } from "./network/NetworkAdapter.js"
12
12
  import { NetworkSubsystem } from "./network/NetworkSubsystem.js"
13
13
  import { StorageAdapter } from "./storage/StorageAdapter.js"
14
14
  import { StorageSubsystem } from "./storage/StorageSubsystem.js"
15
15
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
16
16
  import type { AnyDocumentId, DocumentId, PeerId } from "./types.js"
17
- import { SyncStateMessage } from "./network/messages.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"
18
21
 
19
22
  /** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
20
23
  /** The `Repo` is the main entry point of this library
@@ -44,7 +47,19 @@ export class Repo extends EventEmitter<RepoEvents> {
44
47
  /** @hidden */
45
48
  sharePolicy: SharePolicy = async () => true
46
49
 
47
- constructor({ storage, network, peerId, sharePolicy }: RepoConfig) {
50
+ /** maps peer id to to persistence information (storageId, isEphemeral), access by collection synchronizer */
51
+ /** @hidden */
52
+ peerMetadataByPeerId: Record<PeerId, PeerMetadata> = {}
53
+
54
+ #remoteHeadsSubscriptions = new RemoteHeadsSubscriptions()
55
+
56
+ constructor({
57
+ storage,
58
+ network,
59
+ peerId,
60
+ sharePolicy,
61
+ isEphemeral = storage === undefined,
62
+ }: RepoConfig) {
48
63
  super()
49
64
  this.#log = debug(`automerge-repo:repo`)
50
65
  this.sharePolicy = sharePolicy ?? this.sharePolicy
@@ -132,33 +147,161 @@ export class Repo extends EventEmitter<RepoEvents> {
132
147
 
133
148
  // NETWORK
134
149
  // The network subsystem deals with sending and receiving messages to and from peers.
135
- const networkSubsystem = new NetworkSubsystem(network, peerId)
150
+
151
+ const myPeerMetadata: Promise<PeerMetadata> = new Promise(
152
+ async (resolve, reject) =>
153
+ resolve({
154
+ storageId: await storageSubsystem?.id(),
155
+ isEphemeral,
156
+ } as PeerMetadata)
157
+ )
158
+
159
+ const networkSubsystem = new NetworkSubsystem(
160
+ network,
161
+ peerId,
162
+ myPeerMetadata
163
+ )
136
164
  this.networkSubsystem = networkSubsystem
137
165
 
138
166
  // When we get a new peer, register it with the synchronizer
139
- networkSubsystem.on("peer", async ({ peerId }) => {
167
+ networkSubsystem.on("peer", async ({ peerId, peerMetadata }) => {
140
168
  this.#log("peer connected", { peerId })
169
+
170
+ if (peerMetadata) {
171
+ this.peerMetadataByPeerId[peerId] = { ...peerMetadata }
172
+ }
173
+
174
+ this.sharePolicy(peerId)
175
+ .then(shouldShare => {
176
+ if (shouldShare) {
177
+ this.#remoteHeadsSubscriptions.addGenerousPeer(peerId)
178
+ }
179
+ })
180
+ .catch(err => {
181
+ console.log("error in share policy", { err })
182
+ })
183
+
141
184
  this.#synchronizer.addPeer(peerId)
142
185
  })
143
186
 
144
187
  // When a peer disconnects, remove it from the synchronizer
145
188
  networkSubsystem.on("peer-disconnected", ({ peerId }) => {
146
189
  this.#synchronizer.removePeer(peerId)
190
+ this.#remoteHeadsSubscriptions.removePeer(peerId)
147
191
  })
148
192
 
149
193
  // Handle incoming messages
150
194
  networkSubsystem.on("message", async msg => {
151
- await this.#synchronizer.receiveMessage(msg)
195
+ this.#receiveMessage(msg)
196
+ })
197
+
198
+ this.#synchronizer.on("sync-state", message => {
199
+ this.#saveSyncState(message)
200
+
201
+ const handle = this.#handleCache[message.documentId]
202
+
203
+ const { storageId } = this.peerMetadataByPeerId[message.peerId] || {}
204
+ if (!storageId) {
205
+ return
206
+ }
207
+
208
+ const heads = handle.getRemoteHeads(storageId)
209
+ const haveHeadsChanged =
210
+ message.syncState.theirHeads &&
211
+ (!heads || !headsAreSame(heads, message.syncState.theirHeads))
212
+
213
+ if (haveHeadsChanged) {
214
+ handle.setRemoteHeads(storageId, message.syncState.theirHeads)
215
+
216
+ if (storageId) {
217
+ this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
218
+ message.documentId,
219
+ storageId,
220
+ message.syncState.theirHeads
221
+ )
222
+ }
223
+ }
224
+ })
225
+
226
+ this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
227
+ this.networkSubsystem.send({
228
+ type: "remote-heads-changed",
229
+ targetId: message.targetId,
230
+ documentId: message.documentId,
231
+ newHeads: {
232
+ [message.storageId]: {
233
+ heads: message.heads,
234
+ timestamp: message.timestamp,
235
+ },
236
+ },
237
+ })
152
238
  })
153
239
 
154
- if (storageSubsystem) {
155
- const debouncedSaveSyncState: (syncState: SyncStateMessage) => void =
156
- throttle(({ documentId, peerId, syncState }: SyncStateMessage) => {
157
- storageSubsystem.saveSyncState(documentId, peerId, syncState)
158
- }, this.saveDebounceRate)
240
+ this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
241
+ this.#log("change-remote-subs", message)
242
+ for (const peer of message.peers) {
243
+ this.networkSubsystem.send({
244
+ type: "remote-subscription-change",
245
+ targetId: peer,
246
+ add: message.add,
247
+ remove: message.remove,
248
+ })
249
+ }
250
+ })
251
+
252
+ this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
253
+ const handle = this.#handleCache[message.documentId]
254
+ handle.setRemoteHeads(message.storageId, message.remoteHeads)
255
+ })
256
+ }
257
+
258
+ #receiveMessage(message: RepoMessage) {
259
+ switch (message.type) {
260
+ case "remote-subscription-change":
261
+ this.#remoteHeadsSubscriptions.handleControlMessage(message)
262
+ break
263
+ case "remote-heads-changed":
264
+ this.#remoteHeadsSubscriptions.handleRemoteHeads(message)
265
+ break
266
+ case "sync":
267
+ case "request":
268
+ case "ephemeral":
269
+ case "doc-unavailable":
270
+ this.#synchronizer.receiveMessage(message).catch(err => {
271
+ console.log("error receiving message", { err })
272
+ })
273
+ }
274
+ }
275
+
276
+ #throttledSaveSyncStateHandlers: Record<
277
+ StorageId,
278
+ (message: SyncStateMessage) => void
279
+ > = {}
280
+
281
+ /** saves sync state throttled per storage id, if a peer doesn't have a storage id it's sync state is not persisted */
282
+ #saveSyncState(message: SyncStateMessage) {
283
+ if (!this.storageSubsystem) {
284
+ return
285
+ }
286
+
287
+ const { storageId, isEphemeral } =
288
+ this.peerMetadataByPeerId[message.peerId] || {}
289
+
290
+ if (!storageId || isEphemeral) {
291
+ return
292
+ }
159
293
 
160
- this.#synchronizer.on("sync-state", debouncedSaveSyncState)
294
+ let handler = this.#throttledSaveSyncStateHandlers[storageId]
295
+ if (!handler) {
296
+ handler = this.#throttledSaveSyncStateHandlers[storageId] = throttle(
297
+ ({ documentId, syncState }: SyncStateMessage) => {
298
+ this.storageSubsystem!.saveSyncState(documentId, storageId, syncState)
299
+ },
300
+ this.saveDebounceRate
301
+ )
161
302
  }
303
+
304
+ handler(message)
162
305
  }
163
306
 
164
307
  /** Returns an existing handle if we have it; creates one otherwise. */
@@ -298,12 +441,29 @@ export class Repo extends EventEmitter<RepoEvents> {
298
441
  delete this.#handleCache[documentId]
299
442
  this.emit("delete-document", { documentId })
300
443
  }
444
+
445
+ subscribeToRemotes = (remotes: StorageId[]) => {
446
+ this.#log("subscribeToRemotes", { remotes })
447
+ this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes)
448
+ }
449
+
450
+ storageId = async (): Promise<StorageId | undefined> => {
451
+ if (!this.storageSubsystem) {
452
+ return undefined
453
+ } else {
454
+ return this.storageSubsystem.id()
455
+ }
456
+ }
301
457
  }
302
458
 
303
459
  export interface RepoConfig {
304
460
  /** Our unique identifier */
305
461
  peerId?: PeerId
306
462
 
463
+ /** Indicates whether other peers should persist the sync state of this peer.
464
+ * Sync state is only persisted for non-ephemeral peers */
465
+ isEphemeral?: boolean
466
+
307
467
  /** A storage adapter can be provided, or not */
308
468
  storage?: StorageAdapter
309
469
 
package/src/index.ts CHANGED
@@ -67,6 +67,7 @@ export type {
67
67
  OpenPayload,
68
68
  PeerCandidatePayload,
69
69
  PeerDisconnectedPayload,
70
+ PeerMetadata,
70
71
  } from "./network/NetworkAdapter.js"
71
72
 
72
73
  export type {
@@ -83,6 +84,7 @@ export type {
83
84
  ChunkInfo,
84
85
  ChunkType,
85
86
  StorageKey,
87
+ StorageId,
86
88
  } from "./storage/types.js"
87
89
 
88
90
  export * from "./types.js"
@@ -1,6 +1,18 @@
1
1
  import { EventEmitter } from "eventemitter3"
2
2
  import { PeerId } from "../types.js"
3
3
  import { Message } from "./messages.js"
4
+ import { StorageId } from "../storage/types.js"
5
+
6
+ /**
7
+ * Describes a peer intent to the system
8
+ * storageId: the key for syncState to decide what the other peer already has
9
+ * isEphemeral: to decide if we bother recording this peer's sync state
10
+ *
11
+ */
12
+ export interface PeerMetadata {
13
+ storageId?: StorageId
14
+ isEphemeral?: boolean
15
+ }
4
16
 
5
17
  /** An interface representing some way to connect to other peers
6
18
  *
@@ -11,12 +23,17 @@ import { Message } from "./messages.js"
11
23
  */
12
24
  export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
13
25
  peerId?: PeerId // hmmm, maybe not
26
+ peerMetadata?: PeerMetadata
14
27
 
15
28
  /** Called by the {@link Repo} to start the connection process
16
29
  *
17
30
  * @argument peerId - the peerId of this repo
31
+ * @argument peerMetadata - how this adapter should present itself to other peers
18
32
  */
19
- abstract connect(peerId: PeerId): void
33
+ abstract connect(
34
+ peerId: PeerId,
35
+ peerMetadata?: PeerMetadata
36
+ ): void
20
37
 
21
38
  /** Called by the {@link Repo} to send a message to a peer
22
39
  *
@@ -53,6 +70,7 @@ export interface OpenPayload {
53
70
 
54
71
  export interface PeerCandidatePayload {
55
72
  peerId: PeerId
73
+ peerMetadata: PeerMetadata
56
74
  }
57
75
 
58
76
  export interface PeerDisconnectedPayload {
@@ -1,7 +1,11 @@
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,
@@ -9,6 +13,7 @@ import {
9
13
  isEphemeralMessage,
10
14
  isValidRepoMessage,
11
15
  } from "./messages.js"
16
+ import { StorageId } from "../storage/types.js"
12
17
 
13
18
  type EphemeralMessageSource = `${PeerId}:${SessionId}`
14
19
 
@@ -25,7 +30,11 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
25
30
  #readyAdapterCount = 0
26
31
  #adapters: NetworkAdapter[] = []
27
32
 
28
- constructor(adapters: NetworkAdapter[], public peerId = randomPeerId()) {
33
+ constructor(
34
+ adapters: NetworkAdapter[],
35
+ public peerId = randomPeerId(),
36
+ private peerMetadata: Promise<PeerMetadata>
37
+ ) {
29
38
  super()
30
39
  this.#log = debug(`automerge-repo:network:${this.peerId}`)
31
40
  adapters.forEach(a => this.addNetworkAdapter(a))
@@ -46,9 +55,8 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
46
55
  }
47
56
  })
48
57
 
49
- networkAdapter.on("peer-candidate", ({ peerId }) => {
58
+ networkAdapter.on("peer-candidate", ({ peerId, peerMetadata }) => {
50
59
  this.#log(`peer candidate: ${peerId} `)
51
-
52
60
  // TODO: This is where authentication would happen
53
61
 
54
62
  if (!this.#adaptersByPeer[peerId]) {
@@ -56,7 +64,7 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
56
64
  this.#adaptersByPeer[peerId] = networkAdapter
57
65
  }
58
66
 
59
- this.emit("peer", { peerId })
67
+ this.emit("peer", { peerId, peerMetadata })
60
68
  })
61
69
 
62
70
  networkAdapter.on("peer-disconnected", ({ peerId }) => {
@@ -98,7 +106,9 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
98
106
  })
99
107
  })
100
108
 
101
- networkAdapter.connect(this.peerId)
109
+ this.peerMetadata.then(peerMetadata => {
110
+ networkAdapter.connect(this.peerId, peerMetadata)
111
+ })
102
112
  }
103
113
 
104
114
  send(message: MessageContents) {
@@ -171,4 +181,5 @@ export interface NetworkSubsystemEvents {
171
181
 
172
182
  export interface PeerPayload {
173
183
  peerId: PeerId
184
+ peerMetadata: PeerMetadata
174
185
  }