@automerge/automerge-repo 2.0.0-alpha.3 → 2.0.0-alpha.5
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/Repo.d.ts +3 -0
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +57 -56
- package/dist/entrypoints/fullfat.d.ts +1 -0
- package/dist/entrypoints/fullfat.d.ts.map +1 -1
- package/dist/entrypoints/fullfat.js +1 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -0
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +8 -7
- package/package.json +2 -2
- package/src/Repo.ts +54 -54
- package/src/entrypoints/fullfat.ts +1 -2
- package/src/synchronizer/CollectionSynchronizer.ts +8 -7
- package/test/Repo.test.ts +40 -0
package/dist/Repo.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
|
|
|
5
5
|
import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js";
|
|
6
6
|
import { StorageSubsystem } from "./storage/StorageSubsystem.js";
|
|
7
7
|
import { StorageId } from "./storage/types.js";
|
|
8
|
+
import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
|
|
8
9
|
import type { AnyDocumentId, DocumentId, PeerId } from "./types.js";
|
|
9
10
|
/** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
|
|
10
11
|
/** The `Repo` is the main entry point of this library
|
|
@@ -23,6 +24,8 @@ export declare class Repo extends EventEmitter<RepoEvents> {
|
|
|
23
24
|
/** The debounce rate is adjustable on the repo. */
|
|
24
25
|
/** @hidden */
|
|
25
26
|
saveDebounceRate: number;
|
|
27
|
+
/** @hidden */
|
|
28
|
+
synchronizer: CollectionSynchronizer;
|
|
26
29
|
/** By default, we share generously with all peers. */
|
|
27
30
|
/** @hidden */
|
|
28
31
|
sharePolicy: SharePolicy;
|
package/dist/Repo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAM5C,OAAO,EAAE,SAAS,EAAiC,MAAM,gBAAgB,CAAA;AAIzE,OAAO,EACL,uBAAuB,EACvB,KAAK,YAAY,EAClB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAEhE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"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"}
|
package/dist/Repo.js
CHANGED
|
@@ -30,7 +30,8 @@ export class Repo extends EventEmitter {
|
|
|
30
30
|
/** @hidden */
|
|
31
31
|
saveDebounceRate = 100;
|
|
32
32
|
#handleCache = {};
|
|
33
|
-
|
|
33
|
+
/** @hidden */
|
|
34
|
+
synchronizer;
|
|
34
35
|
/** By default, we share generously with all peers. */
|
|
35
36
|
/** @hidden */
|
|
36
37
|
sharePolicy = async () => true;
|
|
@@ -44,26 +45,6 @@ export class Repo extends EventEmitter {
|
|
|
44
45
|
this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping;
|
|
45
46
|
this.#log = debug(`automerge-repo:repo`);
|
|
46
47
|
this.sharePolicy = sharePolicy ?? this.sharePolicy;
|
|
47
|
-
// DOC COLLECTION
|
|
48
|
-
// The `document` event is fired by the DocCollection any time we create a new document or look
|
|
49
|
-
// up a document by ID. We listen for it in order to wire up storage and network synchronization.
|
|
50
|
-
this.on("document", async ({ handle }) => {
|
|
51
|
-
if (storageSubsystem) {
|
|
52
|
-
// Save when the document changes, but no more often than saveDebounceRate.
|
|
53
|
-
const saveFn = ({ handle, doc, }) => {
|
|
54
|
-
void storageSubsystem.saveDoc(handle.documentId, doc);
|
|
55
|
-
};
|
|
56
|
-
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
|
|
57
|
-
}
|
|
58
|
-
handle.on("unavailable", () => {
|
|
59
|
-
this.#log("document unavailable", { documentId: handle.documentId });
|
|
60
|
-
this.emit("unavailable-document", {
|
|
61
|
-
documentId: handle.documentId,
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
65
|
-
this.#synchronizer.addDocument(handle.documentId);
|
|
66
|
-
});
|
|
67
48
|
this.on("delete-document", ({ documentId }) => {
|
|
68
49
|
// TODO Pass the delete on to the network
|
|
69
50
|
// synchronizer.removeDocument(documentId)
|
|
@@ -75,14 +56,14 @@ export class Repo extends EventEmitter {
|
|
|
75
56
|
});
|
|
76
57
|
// SYNCHRONIZER
|
|
77
58
|
// The synchronizer uses the network subsystem to keep documents in sync with peers.
|
|
78
|
-
this
|
|
59
|
+
this.synchronizer = new CollectionSynchronizer(this);
|
|
79
60
|
// When the synchronizer emits messages, send them to peers
|
|
80
|
-
this
|
|
61
|
+
this.synchronizer.on("message", message => {
|
|
81
62
|
this.#log(`sending ${message.type} message to ${message.targetId}`);
|
|
82
63
|
networkSubsystem.send(message);
|
|
83
64
|
});
|
|
84
65
|
if (this.#remoteHeadsGossipingEnabled) {
|
|
85
|
-
this
|
|
66
|
+
this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
|
|
86
67
|
this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId);
|
|
87
68
|
});
|
|
88
69
|
}
|
|
@@ -113,18 +94,18 @@ export class Repo extends EventEmitter {
|
|
|
113
94
|
.catch(err => {
|
|
114
95
|
console.log("error in share policy", { err });
|
|
115
96
|
});
|
|
116
|
-
this
|
|
97
|
+
this.synchronizer.addPeer(peerId);
|
|
117
98
|
});
|
|
118
99
|
// When a peer disconnects, remove it from the synchronizer
|
|
119
100
|
networkSubsystem.on("peer-disconnected", ({ peerId }) => {
|
|
120
|
-
this
|
|
101
|
+
this.synchronizer.removePeer(peerId);
|
|
121
102
|
this.#remoteHeadsSubscriptions.removePeer(peerId);
|
|
122
103
|
});
|
|
123
104
|
// Handle incoming messages
|
|
124
105
|
networkSubsystem.on("message", async (msg) => {
|
|
125
106
|
this.#receiveMessage(msg);
|
|
126
107
|
});
|
|
127
|
-
this
|
|
108
|
+
this.synchronizer.on("sync-state", message => {
|
|
128
109
|
this.#saveSyncState(message);
|
|
129
110
|
const handle = this.#handleCache[message.documentId];
|
|
130
111
|
const { storageId } = this.peerMetadataByPeerId[message.peerId] || {};
|
|
@@ -172,6 +153,28 @@ export class Repo extends EventEmitter {
|
|
|
172
153
|
});
|
|
173
154
|
}
|
|
174
155
|
}
|
|
156
|
+
// The `document` event is fired by the DocCollection any time we create a new document or look
|
|
157
|
+
// up a document by ID. We listen for it in order to wire up storage and network synchronization.
|
|
158
|
+
#registerHandleWithSubsystems(handle) {
|
|
159
|
+
const { storageSubsystem } = this;
|
|
160
|
+
if (storageSubsystem) {
|
|
161
|
+
// Save when the document changes, but no more often than saveDebounceRate.
|
|
162
|
+
const saveFn = ({ handle, doc }) => {
|
|
163
|
+
void storageSubsystem.saveDoc(handle.documentId, doc);
|
|
164
|
+
};
|
|
165
|
+
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
|
|
166
|
+
}
|
|
167
|
+
handle.on("unavailable", () => {
|
|
168
|
+
this.#log("document unavailable", { documentId: handle.documentId });
|
|
169
|
+
this.emit("unavailable-document", {
|
|
170
|
+
documentId: handle.documentId,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
174
|
+
this.synchronizer.addDocument(handle.documentId);
|
|
175
|
+
// Preserve the old event in case anyone was using it.
|
|
176
|
+
this.emit("document", { handle });
|
|
177
|
+
}
|
|
175
178
|
#receiveMessage(message) {
|
|
176
179
|
switch (message.type) {
|
|
177
180
|
case "remote-subscription-change":
|
|
@@ -188,7 +191,7 @@ export class Repo extends EventEmitter {
|
|
|
188
191
|
case "request":
|
|
189
192
|
case "ephemeral":
|
|
190
193
|
case "doc-unavailable":
|
|
191
|
-
this
|
|
194
|
+
this.synchronizer.receiveMessage(message).catch(err => {
|
|
192
195
|
console.log("error receiving message", { err });
|
|
193
196
|
});
|
|
194
197
|
}
|
|
@@ -229,7 +232,7 @@ export class Repo extends EventEmitter {
|
|
|
229
232
|
}
|
|
230
233
|
/** Returns a list of all connected peer ids */
|
|
231
234
|
get peers() {
|
|
232
|
-
return this
|
|
235
|
+
return this.synchronizer.peers;
|
|
233
236
|
}
|
|
234
237
|
getStorageIdOfPeer(peerId) {
|
|
235
238
|
return this.peerMetadataByPeerId[peerId]?.storageId;
|
|
@@ -245,7 +248,7 @@ export class Repo extends EventEmitter {
|
|
|
245
248
|
const handle = this.#getHandle({
|
|
246
249
|
documentId,
|
|
247
250
|
});
|
|
248
|
-
this
|
|
251
|
+
this.#registerHandleWithSubsystems(handle);
|
|
249
252
|
handle.update(() => {
|
|
250
253
|
let nextDoc;
|
|
251
254
|
if (initialValue) {
|
|
@@ -314,31 +317,29 @@ export class Repo extends EventEmitter {
|
|
|
314
317
|
const handle = this.#getHandle({
|
|
315
318
|
documentId,
|
|
316
319
|
});
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.emit("document", { handle });
|
|
341
|
-
}
|
|
320
|
+
// Loading & network is going to be asynchronous no matter what,
|
|
321
|
+
// but we want to return the handle immediately.
|
|
322
|
+
const attemptLoad = this.storageSubsystem
|
|
323
|
+
? this.storageSubsystem.loadDoc(handle.documentId)
|
|
324
|
+
: Promise.resolve(null);
|
|
325
|
+
attemptLoad
|
|
326
|
+
.then(async (loadedDoc) => {
|
|
327
|
+
if (loadedDoc) {
|
|
328
|
+
// uhhhh, sorry if you're reading this because we were lying to the type system
|
|
329
|
+
handle.update(() => loadedDoc);
|
|
330
|
+
handle.doneLoading();
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// we want to wait for the network subsystem to be ready before
|
|
334
|
+
// we request the document. this prevents entering unavailable during initialization.
|
|
335
|
+
await this.networkSubsystem.whenReady();
|
|
336
|
+
handle.request();
|
|
337
|
+
}
|
|
338
|
+
this.#registerHandleWithSubsystems(handle);
|
|
339
|
+
})
|
|
340
|
+
.catch(err => {
|
|
341
|
+
this.#log("error waiting for network", { err });
|
|
342
|
+
});
|
|
342
343
|
return handle;
|
|
343
344
|
}
|
|
344
345
|
delete(
|
|
@@ -422,6 +423,6 @@ export class Repo extends EventEmitter {
|
|
|
422
423
|
return this.flush();
|
|
423
424
|
}
|
|
424
425
|
metrics() {
|
|
425
|
-
return { documents: this
|
|
426
|
+
return { documents: this.synchronizer.metrics() };
|
|
426
427
|
}
|
|
427
428
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fullfat.d.ts","sourceRoot":"","sources":["../../src/entrypoints/fullfat.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"fullfat.d.ts","sourceRoot":"","sources":["../../src/entrypoints/fullfat.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAS3B,OAAO,sBAAsB,CAAA"}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Repo } from "../Repo.js";
|
|
2
2
|
import { DocMessage } from "../network/messages.js";
|
|
3
3
|
import { DocumentId, PeerId } from "../types.js";
|
|
4
|
+
import { DocSynchronizer } from "./DocSynchronizer.js";
|
|
4
5
|
import { Synchronizer } from "./Synchronizer.js";
|
|
5
6
|
/** A CollectionSynchronizer is responsible for synchronizing a DocCollection with peers. */
|
|
6
7
|
export declare class CollectionSynchronizer extends Synchronizer {
|
|
7
8
|
#private;
|
|
8
9
|
private repo;
|
|
10
|
+
/** A map of documentIds to their synchronizers */
|
|
11
|
+
/** @hidden */
|
|
12
|
+
docSynchronizers: Record<DocumentId, DocSynchronizer>;
|
|
9
13
|
constructor(repo: Repo);
|
|
10
14
|
/**
|
|
11
15
|
* When we receive a sync message for a document we haven't got in memory, we
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAW1C,OAAO,CAAC,IAAI;IAPxB,kDAAkD;IAClD,cAAc;IACd,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAK;gBAKtC,IAAI,EAAE,IAAI;IAqD9B;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU;IAyBxC;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAalC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAgBtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;IASzB,+CAA+C;IAC/C,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED,OAAO,IAAI;QACT,CAAC,GAAG,EAAE,MAAM,GAAG;YACb,KAAK,EAAE,MAAM,EAAE,CAAA;YACf,IAAI,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,UAAU,EAAE,MAAM,CAAA;aAAE,CAAA;SAC7C,CAAA;KACF;CASF"}
|
|
@@ -9,7 +9,8 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
9
9
|
/** The set of peers we are connected with */
|
|
10
10
|
#peers = new Set();
|
|
11
11
|
/** A map of documentIds to their synchronizers */
|
|
12
|
-
|
|
12
|
+
/** @hidden */
|
|
13
|
+
docSynchronizers = {};
|
|
13
14
|
/** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
|
|
14
15
|
#docSetUp = {};
|
|
15
16
|
constructor(repo) {
|
|
@@ -18,11 +19,11 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
18
19
|
}
|
|
19
20
|
/** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
|
|
20
21
|
#fetchDocSynchronizer(documentId) {
|
|
21
|
-
if (!this
|
|
22
|
+
if (!this.docSynchronizers[documentId]) {
|
|
22
23
|
const handle = this.repo.find(stringifyAutomergeUrl({ documentId }));
|
|
23
|
-
this
|
|
24
|
+
this.docSynchronizers[documentId] = this.#initDocSynchronizer(handle);
|
|
24
25
|
}
|
|
25
|
-
return this
|
|
26
|
+
return this.docSynchronizers[documentId];
|
|
26
27
|
}
|
|
27
28
|
/** Creates a new docSynchronizer and sets it up to propagate messages */
|
|
28
29
|
#initDocSynchronizer(handle) {
|
|
@@ -98,7 +99,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
98
99
|
return;
|
|
99
100
|
}
|
|
100
101
|
this.#peers.add(peerId);
|
|
101
|
-
for (const docSynchronizer of Object.values(this
|
|
102
|
+
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
102
103
|
const { documentId } = docSynchronizer;
|
|
103
104
|
void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
104
105
|
if (okToShare)
|
|
@@ -110,7 +111,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
110
111
|
removePeer(peerId) {
|
|
111
112
|
log(`removing peer ${peerId}`);
|
|
112
113
|
this.#peers.delete(peerId);
|
|
113
|
-
for (const docSynchronizer of Object.values(this
|
|
114
|
+
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
114
115
|
docSynchronizer.endSync(peerId);
|
|
115
116
|
}
|
|
116
117
|
}
|
|
@@ -119,7 +120,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
119
120
|
return Array.from(this.#peers);
|
|
120
121
|
}
|
|
121
122
|
metrics() {
|
|
122
|
-
return Object.fromEntries(Object.entries(this
|
|
123
|
+
return Object.fromEntries(Object.entries(this.docSynchronizers).map(([documentId, synchronizer]) => {
|
|
123
124
|
return [documentId, synchronizer.metrics()];
|
|
124
125
|
}));
|
|
125
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.5",
|
|
4
4
|
"description": "A repository object to manage a collection of automerge documents",
|
|
5
5
|
"repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo",
|
|
6
6
|
"author": "Peter van Hardenberg <pvh@pvh.ca>",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"publishConfig": {
|
|
61
61
|
"access": "public"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "9f10cdceb172f92cdf2dc1be361a940dfd3c5858"
|
|
64
64
|
}
|
package/src/Repo.ts
CHANGED
|
@@ -49,7 +49,8 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
49
49
|
|
|
50
50
|
#handleCache: Record<DocumentId, DocHandle<any>> = {}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
/** @hidden */
|
|
53
|
+
synchronizer: CollectionSynchronizer
|
|
53
54
|
|
|
54
55
|
/** By default, we share generously with all peers. */
|
|
55
56
|
/** @hidden */
|
|
@@ -75,33 +76,6 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
75
76
|
this.#log = debug(`automerge-repo:repo`)
|
|
76
77
|
this.sharePolicy = sharePolicy ?? this.sharePolicy
|
|
77
78
|
|
|
78
|
-
// DOC COLLECTION
|
|
79
|
-
|
|
80
|
-
// The `document` event is fired by the DocCollection any time we create a new document or look
|
|
81
|
-
// up a document by ID. We listen for it in order to wire up storage and network synchronization.
|
|
82
|
-
this.on("document", async ({ handle }) => {
|
|
83
|
-
if (storageSubsystem) {
|
|
84
|
-
// Save when the document changes, but no more often than saveDebounceRate.
|
|
85
|
-
const saveFn = ({
|
|
86
|
-
handle,
|
|
87
|
-
doc,
|
|
88
|
-
}: DocHandleEncodedChangePayload<any>) => {
|
|
89
|
-
void storageSubsystem.saveDoc(handle.documentId, doc)
|
|
90
|
-
}
|
|
91
|
-
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
handle.on("unavailable", () => {
|
|
95
|
-
this.#log("document unavailable", { documentId: handle.documentId })
|
|
96
|
-
this.emit("unavailable-document", {
|
|
97
|
-
documentId: handle.documentId,
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
102
|
-
this.#synchronizer.addDocument(handle.documentId)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
79
|
this.on("delete-document", ({ documentId }) => {
|
|
106
80
|
// TODO Pass the delete on to the network
|
|
107
81
|
// synchronizer.removeDocument(documentId)
|
|
@@ -115,16 +89,16 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
115
89
|
|
|
116
90
|
// SYNCHRONIZER
|
|
117
91
|
// The synchronizer uses the network subsystem to keep documents in sync with peers.
|
|
118
|
-
this
|
|
92
|
+
this.synchronizer = new CollectionSynchronizer(this)
|
|
119
93
|
|
|
120
94
|
// When the synchronizer emits messages, send them to peers
|
|
121
|
-
this
|
|
95
|
+
this.synchronizer.on("message", message => {
|
|
122
96
|
this.#log(`sending ${message.type} message to ${message.targetId}`)
|
|
123
97
|
networkSubsystem.send(message)
|
|
124
98
|
})
|
|
125
99
|
|
|
126
100
|
if (this.#remoteHeadsGossipingEnabled) {
|
|
127
|
-
this
|
|
101
|
+
this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
|
|
128
102
|
this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId)
|
|
129
103
|
})
|
|
130
104
|
}
|
|
@@ -167,12 +141,12 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
167
141
|
console.log("error in share policy", { err })
|
|
168
142
|
})
|
|
169
143
|
|
|
170
|
-
this
|
|
144
|
+
this.synchronizer.addPeer(peerId)
|
|
171
145
|
})
|
|
172
146
|
|
|
173
147
|
// When a peer disconnects, remove it from the synchronizer
|
|
174
148
|
networkSubsystem.on("peer-disconnected", ({ peerId }) => {
|
|
175
|
-
this
|
|
149
|
+
this.synchronizer.removePeer(peerId)
|
|
176
150
|
this.#remoteHeadsSubscriptions.removePeer(peerId)
|
|
177
151
|
})
|
|
178
152
|
|
|
@@ -181,7 +155,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
181
155
|
this.#receiveMessage(msg)
|
|
182
156
|
})
|
|
183
157
|
|
|
184
|
-
this
|
|
158
|
+
this.synchronizer.on("sync-state", message => {
|
|
185
159
|
this.#saveSyncState(message)
|
|
186
160
|
|
|
187
161
|
const handle = this.#handleCache[message.documentId]
|
|
@@ -243,6 +217,32 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
243
217
|
}
|
|
244
218
|
}
|
|
245
219
|
|
|
220
|
+
// The `document` event is fired by the DocCollection any time we create a new document or look
|
|
221
|
+
// up a document by ID. We listen for it in order to wire up storage and network synchronization.
|
|
222
|
+
#registerHandleWithSubsystems(handle: DocHandle<any>) {
|
|
223
|
+
const { storageSubsystem } = this
|
|
224
|
+
if (storageSubsystem) {
|
|
225
|
+
// Save when the document changes, but no more often than saveDebounceRate.
|
|
226
|
+
const saveFn = ({ handle, doc }: DocHandleEncodedChangePayload<any>) => {
|
|
227
|
+
void storageSubsystem.saveDoc(handle.documentId, doc)
|
|
228
|
+
}
|
|
229
|
+
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
handle.on("unavailable", () => {
|
|
233
|
+
this.#log("document unavailable", { documentId: handle.documentId })
|
|
234
|
+
this.emit("unavailable-document", {
|
|
235
|
+
documentId: handle.documentId,
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
240
|
+
this.synchronizer.addDocument(handle.documentId)
|
|
241
|
+
|
|
242
|
+
// Preserve the old event in case anyone was using it.
|
|
243
|
+
this.emit("document", { handle })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
246
|
#receiveMessage(message: RepoMessage) {
|
|
247
247
|
switch (message.type) {
|
|
248
248
|
case "remote-subscription-change":
|
|
@@ -259,7 +259,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
259
259
|
case "request":
|
|
260
260
|
case "ephemeral":
|
|
261
261
|
case "doc-unavailable":
|
|
262
|
-
this
|
|
262
|
+
this.synchronizer.receiveMessage(message).catch(err => {
|
|
263
263
|
console.log("error receiving message", { err })
|
|
264
264
|
})
|
|
265
265
|
}
|
|
@@ -324,7 +324,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
324
324
|
|
|
325
325
|
/** Returns a list of all connected peer ids */
|
|
326
326
|
get peers(): PeerId[] {
|
|
327
|
-
return this
|
|
327
|
+
return this.synchronizer.peers
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
getStorageIdOfPeer(peerId: PeerId): StorageId | undefined {
|
|
@@ -343,7 +343,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
343
343
|
documentId,
|
|
344
344
|
}) as DocHandle<T>
|
|
345
345
|
|
|
346
|
-
this
|
|
346
|
+
this.#registerHandleWithSubsystems(handle)
|
|
347
347
|
|
|
348
348
|
handle.update(() => {
|
|
349
349
|
let nextDoc: Automerge.Doc<T>
|
|
@@ -425,29 +425,29 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
425
425
|
documentId,
|
|
426
426
|
}) as DocHandle<T>
|
|
427
427
|
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
// Loading & network is going to be asynchronous no matter what,
|
|
429
|
+
// but we want to return the handle immediately.
|
|
430
|
+
const attemptLoad = this.storageSubsystem
|
|
431
|
+
? this.storageSubsystem.loadDoc(handle.documentId)
|
|
432
|
+
: Promise.resolve(null)
|
|
433
|
+
|
|
434
|
+
attemptLoad
|
|
435
|
+
.then(async loadedDoc => {
|
|
431
436
|
if (loadedDoc) {
|
|
432
437
|
// uhhhh, sorry if you're reading this because we were lying to the type system
|
|
433
438
|
handle.update(() => loadedDoc as Automerge.Doc<T>)
|
|
434
439
|
handle.doneLoading()
|
|
435
440
|
} else {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
})
|
|
441
|
-
.catch(err => {
|
|
442
|
-
this.#log("error waiting for network", { err })
|
|
443
|
-
})
|
|
444
|
-
this.emit("document", { handle })
|
|
441
|
+
// we want to wait for the network subsystem to be ready before
|
|
442
|
+
// we request the document. this prevents entering unavailable during initialization.
|
|
443
|
+
await this.networkSubsystem.whenReady()
|
|
444
|
+
handle.request()
|
|
445
445
|
}
|
|
446
|
+
this.#registerHandleWithSubsystems(handle)
|
|
447
|
+
})
|
|
448
|
+
.catch(err => {
|
|
449
|
+
this.#log("error waiting for network", { err })
|
|
446
450
|
})
|
|
447
|
-
} else {
|
|
448
|
-
handle.request()
|
|
449
|
-
this.emit("document", { handle })
|
|
450
|
-
}
|
|
451
451
|
return handle
|
|
452
452
|
}
|
|
453
453
|
|
|
@@ -547,7 +547,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
metrics(): { documents: { [key: string]: any } } {
|
|
550
|
-
return { documents: this
|
|
550
|
+
return { documents: this.synchronizer.metrics() }
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
@@ -15,7 +15,8 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
15
15
|
#peers: Set<PeerId> = new Set()
|
|
16
16
|
|
|
17
17
|
/** A map of documentIds to their synchronizers */
|
|
18
|
-
|
|
18
|
+
/** @hidden */
|
|
19
|
+
docSynchronizers: Record<DocumentId, DocSynchronizer> = {}
|
|
19
20
|
|
|
20
21
|
/** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
|
|
21
22
|
#docSetUp: Record<DocumentId, boolean> = {}
|
|
@@ -26,11 +27,11 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
26
27
|
|
|
27
28
|
/** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
|
|
28
29
|
#fetchDocSynchronizer(documentId: DocumentId) {
|
|
29
|
-
if (!this
|
|
30
|
+
if (!this.docSynchronizers[documentId]) {
|
|
30
31
|
const handle = this.repo.find(stringifyAutomergeUrl({ documentId }))
|
|
31
|
-
this
|
|
32
|
+
this.docSynchronizers[documentId] = this.#initDocSynchronizer(handle)
|
|
32
33
|
}
|
|
33
|
-
return this
|
|
34
|
+
return this.docSynchronizers[documentId]
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** Creates a new docSynchronizer and sets it up to propagate messages */
|
|
@@ -131,7 +132,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
this.#peers.add(peerId)
|
|
134
|
-
for (const docSynchronizer of Object.values(this
|
|
135
|
+
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
135
136
|
const { documentId } = docSynchronizer
|
|
136
137
|
void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
137
138
|
if (okToShare) docSynchronizer.beginSync([peerId])
|
|
@@ -144,7 +145,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
144
145
|
log(`removing peer ${peerId}`)
|
|
145
146
|
this.#peers.delete(peerId)
|
|
146
147
|
|
|
147
|
-
for (const docSynchronizer of Object.values(this
|
|
148
|
+
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
148
149
|
docSynchronizer.endSync(peerId)
|
|
149
150
|
}
|
|
150
151
|
}
|
|
@@ -161,7 +162,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
161
162
|
}
|
|
162
163
|
} {
|
|
163
164
|
return Object.fromEntries(
|
|
164
|
-
Object.entries(this
|
|
165
|
+
Object.entries(this.docSynchronizers).map(
|
|
165
166
|
([documentId, synchronizer]) => {
|
|
166
167
|
return [documentId, synchronizer.metrics()]
|
|
167
168
|
}
|
package/test/Repo.test.ts
CHANGED
|
@@ -832,6 +832,46 @@ describe("Repo", () => {
|
|
|
832
832
|
teardown()
|
|
833
833
|
})
|
|
834
834
|
|
|
835
|
+
it("synchronizes changes from bobRepo to charlieRepo when loading from storage", async () => {
|
|
836
|
+
const { bobRepo, bobStorage, charlieRepo, aliceHandle, teardown } =
|
|
837
|
+
await setup()
|
|
838
|
+
|
|
839
|
+
// We create a repo that uses bobStorage to put a document into its imaginary disk
|
|
840
|
+
// without it knowing about it
|
|
841
|
+
const bobRepo2 = new Repo({
|
|
842
|
+
storage: bobStorage,
|
|
843
|
+
})
|
|
844
|
+
const inStorageHandle = bobRepo2.create<TestDoc>({
|
|
845
|
+
foo: "foundOnFakeDisk",
|
|
846
|
+
})
|
|
847
|
+
await bobRepo2.flush()
|
|
848
|
+
|
|
849
|
+
console.log("loading from disk", inStorageHandle.url)
|
|
850
|
+
// Now, let's load it on the original bob repo (which shares a "disk")
|
|
851
|
+
const bobFoundIt = bobRepo.find<TestDoc>(inStorageHandle.url)
|
|
852
|
+
await bobFoundIt.whenReady()
|
|
853
|
+
|
|
854
|
+
// Before checking if it syncs, make sure we have it!
|
|
855
|
+
// (This behaviour is mostly test-validation, we are already testing load/save elsewhere.)
|
|
856
|
+
assert.deepStrictEqual(await bobFoundIt.doc(), { foo: "foundOnFakeDisk" })
|
|
857
|
+
|
|
858
|
+
// We should have a docSynchronizer and its peers should be alice and charlie
|
|
859
|
+
assert.strictEqual(
|
|
860
|
+
bobRepo.synchronizer.docSynchronizers[bobFoundIt.documentId]?.hasPeer(
|
|
861
|
+
"alice" as PeerId
|
|
862
|
+
),
|
|
863
|
+
true
|
|
864
|
+
)
|
|
865
|
+
assert.strictEqual(
|
|
866
|
+
bobRepo.synchronizer.docSynchronizers[bobFoundIt.documentId]?.hasPeer(
|
|
867
|
+
"charlie" as PeerId
|
|
868
|
+
),
|
|
869
|
+
true
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
teardown()
|
|
873
|
+
})
|
|
874
|
+
|
|
835
875
|
it("charlieRepo doesn't have a document it's not supposed to have", async () => {
|
|
836
876
|
const { aliceRepo, bobRepo, charlieRepo, notForCharlie, teardown } =
|
|
837
877
|
await setup()
|