@automerge/automerge-repo 1.0.19 → 1.1.0-alpha.1
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 +15 -1
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +118 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.d.ts +8 -1
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.js +2 -0
- package/dist/network/NetworkSubsystem.d.ts +7 -1
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +11 -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 +17 -4
- 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 +173 -11
- package/src/index.ts +1 -0
- package/src/network/NetworkAdapter.ts +12 -1
- package/src/network/NetworkSubsystem.ts +24 -11
- package/src/network/messages.ts +30 -1
- package/src/storage/StorageSubsystem.ts +24 -6
- package/src/storage/types.ts +3 -0
- package/src/synchronizer/CollectionSynchronizer.ts +10 -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
|
@@ -14,7 +14,10 @@ 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 persistance information (storageId, isEphemeral), access by collection synchronizer */
|
|
51
|
+
/** @hidden */
|
|
52
|
+
persistanceInfoByPeerId: Record<PeerId, PersistanceInfo> = {}
|
|
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,158 @@ 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(
|
|
150
|
+
const networkSubsystem = new NetworkSubsystem(
|
|
151
|
+
network,
|
|
152
|
+
peerId,
|
|
153
|
+
storageSubsystem?.id() ?? Promise.resolve(undefined),
|
|
154
|
+
isEphemeral
|
|
155
|
+
)
|
|
136
156
|
this.networkSubsystem = networkSubsystem
|
|
137
157
|
|
|
138
158
|
// When we get a new peer, register it with the synchronizer
|
|
139
|
-
networkSubsystem.on("peer", async ({ peerId }) => {
|
|
159
|
+
networkSubsystem.on("peer", async ({ peerId, storageId, isEphemeral }) => {
|
|
140
160
|
this.#log("peer connected", { peerId })
|
|
161
|
+
|
|
162
|
+
if (storageId) {
|
|
163
|
+
this.persistanceInfoByPeerId[peerId] = {
|
|
164
|
+
storageId,
|
|
165
|
+
isEphemeral,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.sharePolicy(peerId)
|
|
170
|
+
.then(shouldShare => {
|
|
171
|
+
if (shouldShare) {
|
|
172
|
+
this.#remoteHeadsSubscriptions.addGenerousPeer(peerId)
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.catch(err => {
|
|
176
|
+
console.log("error in share policy", { err })
|
|
177
|
+
})
|
|
178
|
+
|
|
141
179
|
this.#synchronizer.addPeer(peerId)
|
|
142
180
|
})
|
|
143
181
|
|
|
144
182
|
// When a peer disconnects, remove it from the synchronizer
|
|
145
183
|
networkSubsystem.on("peer-disconnected", ({ peerId }) => {
|
|
146
184
|
this.#synchronizer.removePeer(peerId)
|
|
185
|
+
this.#remoteHeadsSubscriptions.removePeer(peerId)
|
|
147
186
|
})
|
|
148
187
|
|
|
149
188
|
// Handle incoming messages
|
|
150
189
|
networkSubsystem.on("message", async msg => {
|
|
151
|
-
|
|
190
|
+
this.#receiveMessage(msg)
|
|
152
191
|
})
|
|
153
192
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
193
|
+
this.#synchronizer.on("sync-state", message => {
|
|
194
|
+
this.#saveSyncState(message)
|
|
195
|
+
|
|
196
|
+
const handle = this.#handleCache[message.documentId]
|
|
197
|
+
|
|
198
|
+
const info = this.persistanceInfoByPeerId[message.peerId]
|
|
199
|
+
if (!info) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const { storageId } = info
|
|
204
|
+
const heads = handle.getRemoteHeads(storageId)
|
|
205
|
+
const haveHeadsChanged =
|
|
206
|
+
message.syncState.theirHeads &&
|
|
207
|
+
(!heads || !headsAreSame(heads, message.syncState.theirHeads))
|
|
208
|
+
|
|
209
|
+
if (haveHeadsChanged) {
|
|
210
|
+
handle.setRemoteHeads(storageId, message.syncState.theirHeads)
|
|
211
|
+
|
|
212
|
+
if (storageId) {
|
|
213
|
+
this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
|
|
214
|
+
message.documentId,
|
|
215
|
+
storageId,
|
|
216
|
+
message.syncState.theirHeads
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
|
|
223
|
+
this.networkSubsystem.send({
|
|
224
|
+
type: "remote-heads-changed",
|
|
225
|
+
targetId: message.targetId,
|
|
226
|
+
documentId: message.documentId,
|
|
227
|
+
newHeads: {
|
|
228
|
+
[message.storageId]: {
|
|
229
|
+
heads: message.heads,
|
|
230
|
+
timestamp: message.timestamp,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
|
|
237
|
+
this.#log("change-remote-subs", message)
|
|
238
|
+
for (const peer of message.peers) {
|
|
239
|
+
this.networkSubsystem.send({
|
|
240
|
+
type: "remote-subscription-change",
|
|
241
|
+
targetId: peer,
|
|
242
|
+
add: message.add,
|
|
243
|
+
remove: message.remove,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
})
|
|
159
247
|
|
|
160
|
-
|
|
248
|
+
this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
|
|
249
|
+
const handle = this.#handleCache[message.documentId]
|
|
250
|
+
handle.setRemoteHeads(message.storageId, message.remoteHeads)
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#receiveMessage(message: RepoMessage) {
|
|
255
|
+
switch (message.type) {
|
|
256
|
+
case "remote-subscription-change":
|
|
257
|
+
this.#remoteHeadsSubscriptions.handleControlMessage(message)
|
|
258
|
+
break
|
|
259
|
+
case "remote-heads-changed":
|
|
260
|
+
this.#remoteHeadsSubscriptions.handleRemoteHeads(message)
|
|
261
|
+
break
|
|
262
|
+
case "sync":
|
|
263
|
+
case "request":
|
|
264
|
+
case "ephemeral":
|
|
265
|
+
case "doc-unavailable":
|
|
266
|
+
this.#synchronizer.receiveMessage(message).catch(err => {
|
|
267
|
+
console.log("error receiving message", { err })
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#throttledSaveSyncStateHandlers: Record<
|
|
273
|
+
StorageId,
|
|
274
|
+
(message: SyncStateMessage) => void
|
|
275
|
+
> = {}
|
|
276
|
+
|
|
277
|
+
/** saves sync state throttled per storage id, if a peer doesn't have a storage id it's sync state is not persisted */
|
|
278
|
+
#saveSyncState(message: SyncStateMessage) {
|
|
279
|
+
if (!this.storageSubsystem) {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const persistanceInfo = this.persistanceInfoByPeerId[message.peerId]
|
|
284
|
+
|
|
285
|
+
if (!persistanceInfo || persistanceInfo.isEphemeral) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const { storageId } = persistanceInfo
|
|
290
|
+
|
|
291
|
+
let handler = this.#throttledSaveSyncStateHandlers[storageId]
|
|
292
|
+
if (!handler) {
|
|
293
|
+
handler = this.#throttledSaveSyncStateHandlers[storageId] = throttle(
|
|
294
|
+
({ documentId, syncState }: SyncStateMessage) => {
|
|
295
|
+
this.storageSubsystem!.saveSyncState(documentId, storageId, syncState)
|
|
296
|
+
},
|
|
297
|
+
this.saveDebounceRate
|
|
298
|
+
)
|
|
161
299
|
}
|
|
300
|
+
|
|
301
|
+
handler(message)
|
|
162
302
|
}
|
|
163
303
|
|
|
164
304
|
/** Returns an existing handle if we have it; creates one otherwise. */
|
|
@@ -298,12 +438,34 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
298
438
|
delete this.#handleCache[documentId]
|
|
299
439
|
this.emit("delete-document", { documentId })
|
|
300
440
|
}
|
|
441
|
+
|
|
442
|
+
subscribeToRemotes = (remotes: StorageId[]) => {
|
|
443
|
+
this.#log("subscribeToRemotes", { remotes })
|
|
444
|
+
this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
storageId = async (): Promise<StorageId | undefined> => {
|
|
448
|
+
if (!this.storageSubsystem) {
|
|
449
|
+
return undefined
|
|
450
|
+
} else {
|
|
451
|
+
return this.storageSubsystem.id()
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
interface PersistanceInfo {
|
|
457
|
+
storageId: StorageId
|
|
458
|
+
isEphemeral: boolean
|
|
301
459
|
}
|
|
302
460
|
|
|
303
461
|
export interface RepoConfig {
|
|
304
462
|
/** Our unique identifier */
|
|
305
463
|
peerId?: PeerId
|
|
306
464
|
|
|
465
|
+
/** Indicates whether other peers should persist the sync state of this peer.
|
|
466
|
+
* Sync state is only persisted for non-ephemeral peers */
|
|
467
|
+
isEphemeral?: boolean
|
|
468
|
+
|
|
307
469
|
/** A storage adapter can be provided, or not */
|
|
308
470
|
storage?: StorageAdapter
|
|
309
471
|
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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"
|
|
4
5
|
|
|
5
6
|
/** An interface representing some way to connect to other peers
|
|
6
7
|
*
|
|
@@ -11,12 +12,20 @@ import { Message } from "./messages.js"
|
|
|
11
12
|
*/
|
|
12
13
|
export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
|
|
13
14
|
peerId?: PeerId // hmmm, maybe not
|
|
15
|
+
storageId?: StorageId
|
|
16
|
+
isEphemeral = true
|
|
14
17
|
|
|
15
18
|
/** Called by the {@link Repo} to start the connection process
|
|
16
19
|
*
|
|
17
20
|
* @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
|
|
18
23
|
*/
|
|
19
|
-
abstract connect(
|
|
24
|
+
abstract connect(
|
|
25
|
+
peerId: PeerId,
|
|
26
|
+
storageId: StorageId | undefined,
|
|
27
|
+
isEphemeral: boolean
|
|
28
|
+
): void
|
|
20
29
|
|
|
21
30
|
/** Called by the {@link Repo} to send a message to a peer
|
|
22
31
|
*
|
|
@@ -53,6 +62,8 @@ export interface OpenPayload {
|
|
|
53
62
|
|
|
54
63
|
export interface PeerCandidatePayload {
|
|
55
64
|
peerId: PeerId
|
|
65
|
+
storageId?: StorageId
|
|
66
|
+
isEphemeral: boolean
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
export interface PeerDisconnectedPayload {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isEphemeralMessage,
|
|
10
10
|
isValidRepoMessage,
|
|
11
11
|
} from "./messages.js"
|
|
12
|
+
import { StorageId } from "../storage/types.js"
|
|
12
13
|
|
|
13
14
|
type EphemeralMessageSource = `${PeerId}:${SessionId}`
|
|
14
15
|
|
|
@@ -25,7 +26,12 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
25
26
|
#readyAdapterCount = 0
|
|
26
27
|
#adapters: NetworkAdapter[] = []
|
|
27
28
|
|
|
28
|
-
constructor(
|
|
29
|
+
constructor(
|
|
30
|
+
adapters: NetworkAdapter[],
|
|
31
|
+
public peerId = randomPeerId(),
|
|
32
|
+
private storageId: Promise<StorageId | undefined>, // todo: we shouldn't pass a promise here
|
|
33
|
+
private isEphemeral: boolean
|
|
34
|
+
) {
|
|
29
35
|
super()
|
|
30
36
|
this.#log = debug(`automerge-repo:network:${this.peerId}`)
|
|
31
37
|
adapters.forEach(a => this.addNetworkAdapter(a))
|
|
@@ -46,18 +52,21 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
46
52
|
}
|
|
47
53
|
})
|
|
48
54
|
|
|
49
|
-
networkAdapter.on(
|
|
50
|
-
|
|
55
|
+
networkAdapter.on(
|
|
56
|
+
"peer-candidate",
|
|
57
|
+
({ peerId, storageId, isEphemeral }) => {
|
|
58
|
+
this.#log(`peer candidate: ${peerId} `)
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
// TODO: This is where authentication would happen
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
if (!this.#adaptersByPeer[peerId]) {
|
|
63
|
+
// TODO: handle losing a server here
|
|
64
|
+
this.#adaptersByPeer[peerId] = networkAdapter
|
|
65
|
+
}
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
this.emit("peer", { peerId, storageId, isEphemeral })
|
|
68
|
+
}
|
|
69
|
+
)
|
|
61
70
|
|
|
62
71
|
networkAdapter.on("peer-disconnected", ({ peerId }) => {
|
|
63
72
|
this.#log(`peer disconnected: ${peerId} `)
|
|
@@ -98,7 +107,9 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
98
107
|
})
|
|
99
108
|
})
|
|
100
109
|
|
|
101
|
-
|
|
110
|
+
this.storageId.then(storageId => {
|
|
111
|
+
networkAdapter.connect(this.peerId, storageId, this.isEphemeral)
|
|
112
|
+
})
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
send(message: MessageContents) {
|
|
@@ -171,4 +182,6 @@ export interface NetworkSubsystemEvents {
|
|
|
171
182
|
|
|
172
183
|
export interface PeerPayload {
|
|
173
184
|
peerId: PeerId
|
|
185
|
+
storageId?: StorageId
|
|
186
|
+
isEphemeral: boolean
|
|
174
187
|
}
|