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