@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.
- package/dist/DocHandle.d.ts +6 -5
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +7 -7
- package/dist/RemoteHeadsSubscriptions.d.ts +41 -0
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -0
- package/dist/RemoteHeadsSubscriptions.js +224 -0
- package/dist/Repo.d.ts +11 -2
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +117 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.d.ts +15 -1
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.js +1 -0
- package/dist/network/NetworkSubsystem.d.ts +4 -2
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +8 -4
- package/dist/network/messages.d.ts +24 -1
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +5 -1
- package/dist/storage/StorageSubsystem.d.ts +5 -3
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +23 -5
- package/dist/storage/types.d.ts +4 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts +2 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +7 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +0 -9
- package/package.json +3 -3
- package/src/DocHandle.ts +10 -9
- package/src/RemoteHeadsSubscriptions.ts +306 -0
- package/src/Repo.ts +172 -12
- package/src/index.ts +2 -0
- package/src/network/NetworkAdapter.ts +19 -1
- package/src/network/NetworkSubsystem.ts +17 -6
- package/src/network/messages.ts +30 -1
- package/src/storage/StorageSubsystem.ts +30 -7
- package/src/storage/types.ts +3 -0
- package/src/synchronizer/CollectionSynchronizer.ts +11 -5
- package/src/synchronizer/DocSynchronizer.ts +0 -12
- package/test/DocHandle.test.ts +0 -17
- package/test/RemoteHeadsSubscriptions.test.ts +343 -0
- package/test/Repo.test.ts +51 -15
- package/test/StorageSubsystem.test.ts +28 -6
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
}
|