@automerge/automerge-repo 2.0.0-collectionsync-alpha.1 → 2.0.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/README.md +8 -8
- package/dist/AutomergeUrl.d.ts +17 -5
- package/dist/AutomergeUrl.d.ts.map +1 -1
- package/dist/AutomergeUrl.js +71 -24
- package/dist/DocHandle.d.ts +33 -41
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +105 -66
- package/dist/FindProgress.d.ts +30 -0
- package/dist/FindProgress.d.ts.map +1 -0
- package/dist/FindProgress.js +1 -0
- package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
- package/dist/RemoteHeadsSubscriptions.js +4 -1
- package/dist/Repo.d.ts +24 -5
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +355 -169
- package/dist/helpers/abortable.d.ts +36 -0
- package/dist/helpers/abortable.d.ts.map +1 -0
- package/dist/helpers/abortable.js +47 -0
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
- package/dist/helpers/bufferFromHex.d.ts +3 -0
- package/dist/helpers/bufferFromHex.d.ts.map +1 -0
- package/dist/helpers/bufferFromHex.js +13 -0
- package/dist/helpers/debounce.d.ts.map +1 -1
- package/dist/helpers/eventPromise.d.ts.map +1 -1
- package/dist/helpers/headsAreSame.d.ts +2 -2
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/mergeArrays.d.ts +1 -1
- package/dist/helpers/mergeArrays.d.ts.map +1 -1
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +13 -13
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.js +6 -9
- package/dist/helpers/throttle.d.ts.map +1 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/index.d.ts +35 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -6
- package/dist/network/NetworkSubsystem.d.ts +0 -1
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +0 -3
- package/dist/network/messages.d.ts +1 -7
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/network/messages.js +1 -2
- package/dist/storage/StorageAdapter.d.ts +0 -9
- package/dist/storage/StorageAdapter.d.ts.map +1 -1
- package/dist/storage/StorageAdapter.js +0 -33
- package/dist/storage/StorageSubsystem.d.ts +6 -2
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +131 -37
- package/dist/storage/keyHash.d.ts +1 -1
- package/dist/storage/keyHash.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -4
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +32 -26
- package/dist/synchronizer/DocSynchronizer.d.ts +8 -8
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +205 -79
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +3 -3
- package/package.json +4 -5
- package/src/AutomergeUrl.ts +101 -26
- package/src/DocHandle.ts +158 -77
- package/src/FindProgress.ts +48 -0
- package/src/RemoteHeadsSubscriptions.ts +11 -9
- package/src/Repo.ts +465 -180
- package/src/helpers/abortable.ts +62 -0
- package/src/helpers/bufferFromHex.ts +14 -0
- package/src/helpers/headsAreSame.ts +2 -2
- package/src/helpers/tests/network-adapter-tests.ts +14 -13
- package/src/helpers/tests/storage-adapter-tests.ts +13 -24
- package/src/index.ts +57 -38
- package/src/network/NetworkSubsystem.ts +0 -4
- package/src/network/messages.ts +2 -11
- package/src/storage/StorageAdapter.ts +0 -42
- package/src/storage/StorageSubsystem.ts +155 -45
- package/src/storage/keyHash.ts +1 -1
- package/src/synchronizer/CollectionSynchronizer.ts +42 -29
- package/src/synchronizer/DocSynchronizer.ts +263 -89
- package/src/types.ts +4 -1
- package/test/AutomergeUrl.test.ts +130 -0
- package/test/CollectionSynchronizer.test.ts +6 -8
- package/test/DocHandle.test.ts +161 -77
- package/test/DocSynchronizer.test.ts +11 -9
- package/test/RemoteHeadsSubscriptions.test.ts +1 -1
- package/test/Repo.test.ts +406 -341
- package/test/StorageSubsystem.test.ts +95 -20
- package/test/remoteHeads.test.ts +28 -13
- package/dist/CollectionHandle.d.ts +0 -14
- package/dist/CollectionHandle.d.ts.map +0 -1
- package/dist/CollectionHandle.js +0 -37
- package/dist/DocUrl.d.ts +0 -47
- package/dist/DocUrl.d.ts.map +0 -1
- package/dist/DocUrl.js +0 -72
- package/dist/EphemeralData.d.ts +0 -20
- package/dist/EphemeralData.d.ts.map +0 -1
- package/dist/EphemeralData.js +0 -1
- package/dist/ferigan.d.ts +0 -51
- package/dist/ferigan.d.ts.map +0 -1
- package/dist/ferigan.js +0 -98
- package/dist/src/DocHandle.d.ts +0 -182
- package/dist/src/DocHandle.d.ts.map +0 -1
- package/dist/src/DocHandle.js +0 -405
- package/dist/src/DocUrl.d.ts +0 -49
- package/dist/src/DocUrl.d.ts.map +0 -1
- package/dist/src/DocUrl.js +0 -72
- package/dist/src/EphemeralData.d.ts +0 -19
- package/dist/src/EphemeralData.d.ts.map +0 -1
- package/dist/src/EphemeralData.js +0 -1
- package/dist/src/Repo.d.ts +0 -74
- package/dist/src/Repo.d.ts.map +0 -1
- package/dist/src/Repo.js +0 -208
- package/dist/src/helpers/arraysAreEqual.d.ts +0 -2
- package/dist/src/helpers/arraysAreEqual.d.ts.map +0 -1
- package/dist/src/helpers/arraysAreEqual.js +0 -2
- package/dist/src/helpers/cbor.d.ts +0 -4
- package/dist/src/helpers/cbor.d.ts.map +0 -1
- package/dist/src/helpers/cbor.js +0 -8
- package/dist/src/helpers/eventPromise.d.ts +0 -11
- package/dist/src/helpers/eventPromise.d.ts.map +0 -1
- package/dist/src/helpers/eventPromise.js +0 -7
- package/dist/src/helpers/headsAreSame.d.ts +0 -2
- package/dist/src/helpers/headsAreSame.d.ts.map +0 -1
- package/dist/src/helpers/headsAreSame.js +0 -4
- package/dist/src/helpers/mergeArrays.d.ts +0 -2
- package/dist/src/helpers/mergeArrays.d.ts.map +0 -1
- package/dist/src/helpers/mergeArrays.js +0 -15
- package/dist/src/helpers/pause.d.ts +0 -6
- package/dist/src/helpers/pause.d.ts.map +0 -1
- package/dist/src/helpers/pause.js +0 -10
- package/dist/src/helpers/tests/network-adapter-tests.d.ts +0 -21
- package/dist/src/helpers/tests/network-adapter-tests.d.ts.map +0 -1
- package/dist/src/helpers/tests/network-adapter-tests.js +0 -122
- package/dist/src/helpers/withTimeout.d.ts +0 -12
- package/dist/src/helpers/withTimeout.d.ts.map +0 -1
- package/dist/src/helpers/withTimeout.js +0 -24
- package/dist/src/index.d.ts +0 -53
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -40
- package/dist/src/network/NetworkAdapter.d.ts +0 -26
- package/dist/src/network/NetworkAdapter.d.ts.map +0 -1
- package/dist/src/network/NetworkAdapter.js +0 -4
- package/dist/src/network/NetworkSubsystem.d.ts +0 -23
- package/dist/src/network/NetworkSubsystem.d.ts.map +0 -1
- package/dist/src/network/NetworkSubsystem.js +0 -120
- package/dist/src/network/messages.d.ts +0 -85
- package/dist/src/network/messages.d.ts.map +0 -1
- package/dist/src/network/messages.js +0 -23
- package/dist/src/storage/StorageAdapter.d.ts +0 -14
- package/dist/src/storage/StorageAdapter.d.ts.map +0 -1
- package/dist/src/storage/StorageAdapter.js +0 -1
- package/dist/src/storage/StorageSubsystem.d.ts +0 -12
- package/dist/src/storage/StorageSubsystem.d.ts.map +0 -1
- package/dist/src/storage/StorageSubsystem.js +0 -145
- package/dist/src/synchronizer/CollectionSynchronizer.d.ts +0 -25
- package/dist/src/synchronizer/CollectionSynchronizer.d.ts.map +0 -1
- package/dist/src/synchronizer/CollectionSynchronizer.js +0 -106
- package/dist/src/synchronizer/DocSynchronizer.d.ts +0 -29
- package/dist/src/synchronizer/DocSynchronizer.d.ts.map +0 -1
- package/dist/src/synchronizer/DocSynchronizer.js +0 -263
- package/dist/src/synchronizer/Synchronizer.d.ts +0 -9
- package/dist/src/synchronizer/Synchronizer.d.ts.map +0 -1
- package/dist/src/synchronizer/Synchronizer.js +0 -2
- package/dist/src/types.d.ts +0 -16
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -1
- package/dist/test/CollectionSynchronizer.test.d.ts +0 -2
- package/dist/test/CollectionSynchronizer.test.d.ts.map +0 -1
- package/dist/test/CollectionSynchronizer.test.js +0 -57
- package/dist/test/DocHandle.test.d.ts +0 -2
- package/dist/test/DocHandle.test.d.ts.map +0 -1
- package/dist/test/DocHandle.test.js +0 -238
- package/dist/test/DocSynchronizer.test.d.ts +0 -2
- package/dist/test/DocSynchronizer.test.d.ts.map +0 -1
- package/dist/test/DocSynchronizer.test.js +0 -111
- package/dist/test/Network.test.d.ts +0 -2
- package/dist/test/Network.test.d.ts.map +0 -1
- package/dist/test/Network.test.js +0 -11
- package/dist/test/Repo.test.d.ts +0 -2
- package/dist/test/Repo.test.d.ts.map +0 -1
- package/dist/test/Repo.test.js +0 -568
- package/dist/test/StorageSubsystem.test.d.ts +0 -2
- package/dist/test/StorageSubsystem.test.d.ts.map +0 -1
- package/dist/test/StorageSubsystem.test.js +0 -56
- package/dist/test/helpers/DummyNetworkAdapter.d.ts +0 -9
- package/dist/test/helpers/DummyNetworkAdapter.d.ts.map +0 -1
- package/dist/test/helpers/DummyNetworkAdapter.js +0 -15
- package/dist/test/helpers/DummyStorageAdapter.d.ts +0 -16
- package/dist/test/helpers/DummyStorageAdapter.d.ts.map +0 -1
- package/dist/test/helpers/DummyStorageAdapter.js +0 -33
- package/dist/test/helpers/generate-large-object.d.ts +0 -5
- package/dist/test/helpers/generate-large-object.d.ts.map +0 -1
- package/dist/test/helpers/generate-large-object.js +0 -9
- package/dist/test/helpers/getRandomItem.d.ts +0 -2
- package/dist/test/helpers/getRandomItem.d.ts.map +0 -1
- package/dist/test/helpers/getRandomItem.js +0 -4
- package/dist/test/types.d.ts +0 -4
- package/dist/test/types.d.ts.map +0 -1
- package/dist/test/types.js +0 -1
- package/src/CollectionHandle.ts +0 -54
- package/src/ferigan.ts +0 -184
package/dist/Repo.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { next as Automerge } from "@automerge/automerge/slim";
|
|
2
2
|
import debug from "debug";
|
|
3
3
|
import { EventEmitter } from "eventemitter3";
|
|
4
|
-
import { generateAutomergeUrl, interpretAsDocumentId, parseAutomergeUrl, } from "./AutomergeUrl.js";
|
|
4
|
+
import { encodeHeads, generateAutomergeUrl, interpretAsDocumentId, isValidAutomergeUrl, parseAutomergeUrl, } from "./AutomergeUrl.js";
|
|
5
5
|
import { DELETED, DocHandle, READY, UNAVAILABLE, UNLOADED, } from "./DocHandle.js";
|
|
6
|
+
import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js";
|
|
7
|
+
import { headsAreSame } from "./helpers/headsAreSame.js";
|
|
8
|
+
import { throttle } from "./helpers/throttle.js";
|
|
6
9
|
import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
|
|
7
10
|
import { StorageSubsystem } from "./storage/StorageSubsystem.js";
|
|
8
11
|
import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
|
|
9
|
-
import {
|
|
10
|
-
import { InMemoryStorageAdapter } from "./storage/StorageAdapter.js";
|
|
12
|
+
import { abortable } from "./helpers/abortable.js";
|
|
11
13
|
function randomPeerId() {
|
|
12
14
|
return ("peer-" + Math.random().toString(36).slice(4));
|
|
13
15
|
}
|
|
@@ -23,7 +25,11 @@ export class Repo extends EventEmitter {
|
|
|
23
25
|
#log;
|
|
24
26
|
/** @hidden */
|
|
25
27
|
networkSubsystem;
|
|
28
|
+
/** @hidden */
|
|
26
29
|
storageSubsystem;
|
|
30
|
+
/** The debounce rate is adjustable on the repo. */
|
|
31
|
+
/** @hidden */
|
|
32
|
+
saveDebounceRate = 100;
|
|
27
33
|
#handleCache = {};
|
|
28
34
|
/** @hidden */
|
|
29
35
|
synchronizer;
|
|
@@ -33,73 +39,49 @@ export class Repo extends EventEmitter {
|
|
|
33
39
|
/** maps peer id to to persistence information (storageId, isEphemeral), access by collection synchronizer */
|
|
34
40
|
/** @hidden */
|
|
35
41
|
peerMetadataByPeerId = {};
|
|
36
|
-
#
|
|
42
|
+
#remoteHeadsSubscriptions = new RemoteHeadsSubscriptions();
|
|
43
|
+
#remoteHeadsGossipingEnabled = false;
|
|
44
|
+
#progressCache = {};
|
|
37
45
|
constructor({ storage, network = [], peerId = randomPeerId(), sharePolicy, isEphemeral = storage === undefined, enableRemoteHeadsGossiping = false, denylist = [], } = {}) {
|
|
38
46
|
super();
|
|
39
|
-
|
|
40
|
-
// beelayStorage = new InMemoryStorageAdapter()
|
|
41
|
-
storage = new InMemoryStorageAdapter();
|
|
42
|
-
}
|
|
43
|
-
this.#beelay = new A.beelay.Beelay({
|
|
44
|
-
storage,
|
|
45
|
-
peerId,
|
|
46
|
-
requestPolicy: async ({ docId }) => {
|
|
47
|
-
const peers = Array.from(this.networkSubsystem.peers);
|
|
48
|
-
const generousPeers = [];
|
|
49
|
-
for (const peerId of peers) {
|
|
50
|
-
const okToShare = await this.sharePolicy(peerId);
|
|
51
|
-
if (okToShare)
|
|
52
|
-
generousPeers.push(peerId);
|
|
53
|
-
}
|
|
54
|
-
return generousPeers;
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
this.storageSubsystem = new StorageSubsystem(this.#beelay, storage);
|
|
47
|
+
this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping;
|
|
58
48
|
this.#log = debug(`automerge-repo:repo`);
|
|
59
49
|
this.sharePolicy = sharePolicy ?? this.sharePolicy;
|
|
60
|
-
this
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
this.#beelay.on("docEvent", event => {
|
|
69
|
-
this.#log(`received ${event.data.type} event for ${event.docId}`);
|
|
70
|
-
const handle = this.#handleCache[event.docId];
|
|
71
|
-
if (handle != null) {
|
|
72
|
-
handle.update(d => Automerge.loadIncremental(d, event.data.contents));
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
this.#beelay.on("bundleRequired", ({ start, end, checkpoints, docId }) => {
|
|
76
|
-
;
|
|
77
|
-
(async () => {
|
|
78
|
-
const doc = await this.storageSubsystem.loadDoc(docId);
|
|
79
|
-
if (doc == null) {
|
|
80
|
-
console.warn("document not found when creating bundle");
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const bundle = A.saveBundle(doc, start, end);
|
|
84
|
-
this.#beelay.addBundle({
|
|
85
|
-
docId,
|
|
86
|
-
checkpoints,
|
|
87
|
-
start,
|
|
88
|
-
end,
|
|
89
|
-
data: bundle,
|
|
50
|
+
this.on("delete-document", ({ documentId }) => {
|
|
51
|
+
// TODO Pass the delete on to the network
|
|
52
|
+
// synchronizer.removeDocument(documentId)
|
|
53
|
+
if (storageSubsystem) {
|
|
54
|
+
storageSubsystem.removeDoc(documentId).catch(err => {
|
|
55
|
+
this.#log("error deleting document", { documentId, err });
|
|
90
56
|
});
|
|
91
|
-
}
|
|
57
|
+
}
|
|
92
58
|
});
|
|
93
59
|
// SYNCHRONIZER
|
|
94
|
-
|
|
60
|
+
// The synchronizer uses the network subsystem to keep documents in sync with peers.
|
|
61
|
+
this.synchronizer = new CollectionSynchronizer(this, denylist);
|
|
62
|
+
// When the synchronizer emits messages, send them to peers
|
|
95
63
|
this.synchronizer.on("message", message => {
|
|
96
64
|
this.#log(`sending ${message.type} message to ${message.targetId}`);
|
|
97
65
|
networkSubsystem.send(message);
|
|
98
66
|
});
|
|
67
|
+
// Forward metrics from doc synchronizers
|
|
68
|
+
this.synchronizer.on("metrics", event => this.emit("doc-metrics", event));
|
|
69
|
+
if (this.#remoteHeadsGossipingEnabled) {
|
|
70
|
+
this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
|
|
71
|
+
this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// STORAGE
|
|
75
|
+
// The storage subsystem has access to some form of persistence, and deals with save and loading documents.
|
|
76
|
+
const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined;
|
|
77
|
+
if (storageSubsystem) {
|
|
78
|
+
storageSubsystem.on("document-loaded", event => this.emit("doc-metrics", { type: "doc-loaded", ...event }));
|
|
79
|
+
}
|
|
80
|
+
this.storageSubsystem = storageSubsystem;
|
|
99
81
|
// NETWORK
|
|
100
82
|
// The network subsystem deals with sending and receiving messages to and from peers.
|
|
101
83
|
const myPeerMetadata = (async () => ({
|
|
102
|
-
|
|
84
|
+
storageId: await storageSubsystem?.id(),
|
|
103
85
|
isEphemeral,
|
|
104
86
|
}))();
|
|
105
87
|
const networkSubsystem = new NetworkSubsystem(network, peerId, myPeerMetadata);
|
|
@@ -110,69 +92,128 @@ export class Repo extends EventEmitter {
|
|
|
110
92
|
if (peerMetadata) {
|
|
111
93
|
this.peerMetadataByPeerId[peerId] = { ...peerMetadata };
|
|
112
94
|
}
|
|
95
|
+
this.sharePolicy(peerId)
|
|
96
|
+
.then(shouldShare => {
|
|
97
|
+
if (shouldShare && this.#remoteHeadsGossipingEnabled) {
|
|
98
|
+
this.#remoteHeadsSubscriptions.addGenerousPeer(peerId);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch(err => {
|
|
102
|
+
console.log("error in share policy", { err });
|
|
103
|
+
});
|
|
113
104
|
this.synchronizer.addPeer(peerId);
|
|
114
105
|
});
|
|
106
|
+
// When a peer disconnects, remove it from the synchronizer
|
|
107
|
+
networkSubsystem.on("peer-disconnected", ({ peerId }) => {
|
|
108
|
+
this.synchronizer.removePeer(peerId);
|
|
109
|
+
this.#remoteHeadsSubscriptions.removePeer(peerId);
|
|
110
|
+
});
|
|
115
111
|
// Handle incoming messages
|
|
116
112
|
networkSubsystem.on("message", async (msg) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// So, even though `msg.message` _is_ a `Uint8Array`, we have to do this
|
|
126
|
-
// absurd thing to get the tests to pass
|
|
127
|
-
msg.message = Uint8Array.from(msg.message);
|
|
128
|
-
}
|
|
129
|
-
this.#beelay.receiveMessage({
|
|
130
|
-
message: {
|
|
131
|
-
sender: msg.senderId,
|
|
132
|
-
recipient: msg.targetId,
|
|
133
|
-
message: msg.message,
|
|
134
|
-
},
|
|
135
|
-
});
|
|
113
|
+
this.#receiveMessage(msg);
|
|
114
|
+
});
|
|
115
|
+
this.synchronizer.on("sync-state", message => {
|
|
116
|
+
this.#saveSyncState(message);
|
|
117
|
+
const handle = this.#handleCache[message.documentId];
|
|
118
|
+
const { storageId } = this.peerMetadataByPeerId[message.peerId] || {};
|
|
119
|
+
if (!storageId) {
|
|
120
|
+
return;
|
|
136
121
|
}
|
|
137
|
-
|
|
138
|
-
|
|
122
|
+
const heads = handle.getRemoteHeads(storageId);
|
|
123
|
+
const haveHeadsChanged = message.syncState.theirHeads &&
|
|
124
|
+
(!heads ||
|
|
125
|
+
!headsAreSame(heads, encodeHeads(message.syncState.theirHeads)));
|
|
126
|
+
if (haveHeadsChanged && message.syncState.theirHeads) {
|
|
127
|
+
handle.setRemoteHeads(storageId, encodeHeads(message.syncState.theirHeads));
|
|
128
|
+
if (storageId && this.#remoteHeadsGossipingEnabled) {
|
|
129
|
+
this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(message.documentId, storageId, encodeHeads(message.syncState.theirHeads));
|
|
130
|
+
}
|
|
139
131
|
}
|
|
140
132
|
});
|
|
133
|
+
if (this.#remoteHeadsGossipingEnabled) {
|
|
134
|
+
this.#remoteHeadsSubscriptions.on("notify-remote-heads", message => {
|
|
135
|
+
this.networkSubsystem.send({
|
|
136
|
+
type: "remote-heads-changed",
|
|
137
|
+
targetId: message.targetId,
|
|
138
|
+
documentId: message.documentId,
|
|
139
|
+
newHeads: {
|
|
140
|
+
[message.storageId]: {
|
|
141
|
+
heads: message.heads,
|
|
142
|
+
timestamp: message.timestamp,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
this.#remoteHeadsSubscriptions.on("change-remote-subs", message => {
|
|
148
|
+
this.#log("change-remote-subs", message);
|
|
149
|
+
for (const peer of message.peers) {
|
|
150
|
+
this.networkSubsystem.send({
|
|
151
|
+
type: "remote-subscription-change",
|
|
152
|
+
targetId: peer,
|
|
153
|
+
add: message.add,
|
|
154
|
+
remove: message.remove,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
this.#remoteHeadsSubscriptions.on("remote-heads-changed", message => {
|
|
159
|
+
const handle = this.#handleCache[message.documentId];
|
|
160
|
+
handle.setRemoteHeads(message.storageId, message.remoteHeads);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
141
163
|
}
|
|
142
164
|
// The `document` event is fired by the DocCollection any time we create a new document or look
|
|
143
165
|
// up a document by ID. We listen for it in order to wire up storage and network synchronization.
|
|
144
166
|
#registerHandleWithSubsystems(handle) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
this.synchronizer.addDocument(handle.documentId);
|
|
158
|
-
// Preserve the old event in case anyone was using it.
|
|
159
|
-
this.emit("document", { handle });
|
|
167
|
+
const { storageSubsystem } = this;
|
|
168
|
+
if (storageSubsystem) {
|
|
169
|
+
// Save when the document changes, but no more often than saveDebounceRate.
|
|
170
|
+
const saveFn = ({ handle, doc }) => {
|
|
171
|
+
void storageSubsystem.saveDoc(handle.documentId, doc);
|
|
172
|
+
};
|
|
173
|
+
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
|
|
174
|
+
}
|
|
175
|
+
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
176
|
+
this.synchronizer.addDocument(handle);
|
|
160
177
|
}
|
|
161
178
|
#receiveMessage(message) {
|
|
162
179
|
switch (message.type) {
|
|
163
180
|
case "remote-subscription-change":
|
|
181
|
+
if (this.#remoteHeadsGossipingEnabled) {
|
|
182
|
+
this.#remoteHeadsSubscriptions.handleControlMessage(message);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
164
185
|
case "remote-heads-changed":
|
|
186
|
+
if (this.#remoteHeadsGossipingEnabled) {
|
|
187
|
+
this.#remoteHeadsSubscriptions.handleRemoteHeads(message);
|
|
188
|
+
}
|
|
165
189
|
break;
|
|
166
190
|
case "sync":
|
|
167
191
|
case "request":
|
|
168
192
|
case "ephemeral":
|
|
169
193
|
case "doc-unavailable":
|
|
170
194
|
this.synchronizer.receiveMessage(message).catch(err => {
|
|
171
|
-
console.
|
|
195
|
+
console.log("error receiving message", { err });
|
|
172
196
|
});
|
|
173
|
-
break;
|
|
174
197
|
}
|
|
175
198
|
}
|
|
199
|
+
#throttledSaveSyncStateHandlers = {};
|
|
200
|
+
/** saves sync state throttled per storage id, if a peer doesn't have a storage id it's sync state is not persisted */
|
|
201
|
+
#saveSyncState(payload) {
|
|
202
|
+
if (!this.storageSubsystem) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const { storageId, isEphemeral } = this.peerMetadataByPeerId[payload.peerId] || {};
|
|
206
|
+
if (!storageId || isEphemeral) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
let handler = this.#throttledSaveSyncStateHandlers[storageId];
|
|
210
|
+
if (!handler) {
|
|
211
|
+
handler = this.#throttledSaveSyncStateHandlers[storageId] = throttle(({ documentId, syncState }) => {
|
|
212
|
+
void this.storageSubsystem.saveSyncState(documentId, storageId, syncState);
|
|
213
|
+
}, this.saveDebounceRate);
|
|
214
|
+
}
|
|
215
|
+
handler(payload);
|
|
216
|
+
}
|
|
176
217
|
/** Returns an existing handle if we have it; creates one otherwise. */
|
|
177
218
|
#getHandle({ documentId, }) {
|
|
178
219
|
// If we have the handle cached, return it
|
|
@@ -191,7 +232,7 @@ export class Repo extends EventEmitter {
|
|
|
191
232
|
}
|
|
192
233
|
/** Returns a list of all connected peer ids */
|
|
193
234
|
get peers() {
|
|
194
|
-
return this.
|
|
235
|
+
return this.synchronizer.peers;
|
|
195
236
|
}
|
|
196
237
|
getStorageIdOfPeer(peerId) {
|
|
197
238
|
return this.peerMetadataByPeerId[peerId]?.storageId;
|
|
@@ -207,7 +248,7 @@ export class Repo extends EventEmitter {
|
|
|
207
248
|
const handle = this.#getHandle({
|
|
208
249
|
documentId,
|
|
209
250
|
});
|
|
210
|
-
|
|
251
|
+
this.#registerHandleWithSubsystems(handle);
|
|
211
252
|
handle.update(() => {
|
|
212
253
|
let nextDoc;
|
|
213
254
|
if (initialValue) {
|
|
@@ -216,27 +257,8 @@ export class Repo extends EventEmitter {
|
|
|
216
257
|
else {
|
|
217
258
|
nextDoc = Automerge.emptyChange(Automerge.init());
|
|
218
259
|
}
|
|
219
|
-
const patches = A.diff(nextDoc, [], A.getHeads(nextDoc));
|
|
220
|
-
for (const patch of patches) {
|
|
221
|
-
initialLinks = patches
|
|
222
|
-
.map(patch => {
|
|
223
|
-
if (patch.action === "put") {
|
|
224
|
-
if (patch.value instanceof A.Link) {
|
|
225
|
-
return patch.value;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return null;
|
|
229
|
-
})
|
|
230
|
-
.filter(v => v != null);
|
|
231
|
-
}
|
|
232
260
|
return nextDoc;
|
|
233
261
|
});
|
|
234
|
-
for (const link of initialLinks) {
|
|
235
|
-
const { documentId: target } = parseAutomergeUrl(link.target);
|
|
236
|
-
this.#beelay.addLink({ from: documentId, to: target });
|
|
237
|
-
}
|
|
238
|
-
this.storageSubsystem.saveDoc(handle.documentId, handle.docSync());
|
|
239
|
-
this.#registerHandleWithSubsystems(handle);
|
|
240
262
|
handle.doneLoading();
|
|
241
263
|
return handle;
|
|
242
264
|
}
|
|
@@ -252,18 +274,13 @@ export class Repo extends EventEmitter {
|
|
|
252
274
|
* Any peers this `Repo` is connected to for whom `sharePolicy` returns `true` will
|
|
253
275
|
* be notified of the newly created DocHandle.
|
|
254
276
|
*
|
|
255
|
-
* @throws if the cloned handle is not yet ready or if
|
|
256
|
-
* `clonedHandle.docSync()` returns `undefined` (i.e. the handle is unavailable).
|
|
257
277
|
*/
|
|
258
278
|
clone(clonedHandle) {
|
|
259
279
|
if (!clonedHandle.isReady()) {
|
|
260
280
|
throw new Error(`Cloned handle is not yet in ready state.
|
|
261
281
|
(Try await handle.whenReady() first.)`);
|
|
262
282
|
}
|
|
263
|
-
const sourceDoc = clonedHandle.
|
|
264
|
-
if (!sourceDoc) {
|
|
265
|
-
throw new Error("Cloned handle doesn't have a document.");
|
|
266
|
-
}
|
|
283
|
+
const sourceDoc = clonedHandle.doc();
|
|
267
284
|
const handle = this.create();
|
|
268
285
|
handle.update(() => {
|
|
269
286
|
// we replace the document with the new cloned one
|
|
@@ -271,55 +288,220 @@ export class Repo extends EventEmitter {
|
|
|
271
288
|
});
|
|
272
289
|
return handle;
|
|
273
290
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
id) {
|
|
281
|
-
this.#log("find", { id });
|
|
282
|
-
const documentId = interpretAsDocumentId(id);
|
|
283
|
-
// If we have the handle cached, return it
|
|
291
|
+
findWithProgress(id, options = {}) {
|
|
292
|
+
const { signal } = options;
|
|
293
|
+
const { documentId, heads } = isValidAutomergeUrl(id)
|
|
294
|
+
? parseAutomergeUrl(id)
|
|
295
|
+
: { documentId: interpretAsDocumentId(id), heads: undefined };
|
|
296
|
+
// Check handle cache first - return plain FindStep for terminal states
|
|
284
297
|
if (this.#handleCache[documentId]) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
298
|
+
const handle = this.#handleCache[documentId];
|
|
299
|
+
if (handle.state === UNAVAILABLE) {
|
|
300
|
+
const result = {
|
|
301
|
+
state: "unavailable",
|
|
302
|
+
error: new Error(`Document ${id} is unavailable`),
|
|
303
|
+
handle,
|
|
304
|
+
};
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
if (handle.state === DELETED) {
|
|
308
|
+
const result = {
|
|
309
|
+
state: "failed",
|
|
310
|
+
error: new Error(`Document ${id} was deleted`),
|
|
311
|
+
handle,
|
|
312
|
+
};
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
if (handle.state === READY) {
|
|
316
|
+
const result = {
|
|
317
|
+
state: "ready",
|
|
318
|
+
handle: heads ? handle.view(heads) : handle,
|
|
319
|
+
};
|
|
320
|
+
return result;
|
|
292
321
|
}
|
|
293
|
-
return this.#handleCache[documentId];
|
|
294
322
|
}
|
|
295
|
-
//
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
323
|
+
// Check progress cache for any existing signal
|
|
324
|
+
const cachedProgress = this.#progressCache[documentId];
|
|
325
|
+
if (cachedProgress) {
|
|
326
|
+
const handle = this.#handleCache[documentId];
|
|
327
|
+
// Return cached progress if we have a handle and it's either in a terminal state or loading
|
|
328
|
+
if (handle &&
|
|
329
|
+
(handle.state === READY ||
|
|
330
|
+
handle.state === UNAVAILABLE ||
|
|
331
|
+
handle.state === DELETED ||
|
|
332
|
+
handle.state === "loading")) {
|
|
333
|
+
return cachedProgress;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const handle = this.#getHandle({ documentId });
|
|
337
|
+
const initial = {
|
|
338
|
+
state: "loading",
|
|
339
|
+
progress: 0,
|
|
340
|
+
handle,
|
|
341
|
+
};
|
|
342
|
+
// Create a new progress signal
|
|
343
|
+
const progressSignal = {
|
|
344
|
+
subscribers: new Set(),
|
|
345
|
+
currentProgress: undefined,
|
|
346
|
+
notify: (progress) => {
|
|
347
|
+
progressSignal.currentProgress = progress;
|
|
348
|
+
progressSignal.subscribers.forEach(callback => callback(progress));
|
|
349
|
+
// Cache all states, not just terminal ones
|
|
350
|
+
this.#progressCache[documentId] = progress;
|
|
351
|
+
},
|
|
352
|
+
peek: () => progressSignal.currentProgress || initial,
|
|
353
|
+
subscribe: (callback) => {
|
|
354
|
+
progressSignal.subscribers.add(callback);
|
|
355
|
+
return () => progressSignal.subscribers.delete(callback);
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
progressSignal.notify(initial);
|
|
359
|
+
// Start the loading process
|
|
360
|
+
void this.#loadDocumentWithProgress(id, documentId, handle, progressSignal, signal ? abortable(new Promise(() => { }), signal) : new Promise(() => { }));
|
|
361
|
+
const result = {
|
|
362
|
+
...initial,
|
|
363
|
+
peek: progressSignal.peek,
|
|
364
|
+
subscribe: progressSignal.subscribe,
|
|
365
|
+
};
|
|
366
|
+
this.#progressCache[documentId] = result;
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
async #loadDocumentWithProgress(id, documentId, handle, progressSignal, abortPromise) {
|
|
370
|
+
try {
|
|
371
|
+
progressSignal.notify({
|
|
372
|
+
state: "loading",
|
|
373
|
+
progress: 25,
|
|
374
|
+
handle,
|
|
375
|
+
});
|
|
376
|
+
const loadingPromise = await (this.storageSubsystem
|
|
377
|
+
? this.storageSubsystem.loadDoc(handle.documentId)
|
|
378
|
+
: Promise.resolve(null));
|
|
379
|
+
const loadedDoc = await Promise.race([loadingPromise, abortPromise]);
|
|
304
380
|
if (loadedDoc) {
|
|
305
|
-
// uhhhh, sorry if you're reading this because we were lying to the type system
|
|
306
381
|
handle.update(() => loadedDoc);
|
|
307
382
|
handle.doneLoading();
|
|
383
|
+
progressSignal.notify({
|
|
384
|
+
state: "loading",
|
|
385
|
+
progress: 50,
|
|
386
|
+
handle,
|
|
387
|
+
});
|
|
308
388
|
}
|
|
309
389
|
else {
|
|
310
|
-
|
|
311
|
-
// we request the document. this prevents entering unavailable during initialization.
|
|
312
|
-
await this.networkSubsystem.whenReady();
|
|
313
|
-
console.log("we didn't find it so we're requesting");
|
|
390
|
+
await Promise.race([this.networkSubsystem.whenReady(), abortPromise]);
|
|
314
391
|
handle.request();
|
|
392
|
+
progressSignal.notify({
|
|
393
|
+
state: "loading",
|
|
394
|
+
progress: 75,
|
|
395
|
+
handle,
|
|
396
|
+
});
|
|
315
397
|
}
|
|
316
398
|
this.#registerHandleWithSubsystems(handle);
|
|
317
|
-
|
|
318
|
-
.
|
|
319
|
-
|
|
320
|
-
|
|
399
|
+
await Promise.race([handle.whenReady([READY, UNAVAILABLE]), abortPromise]);
|
|
400
|
+
if (handle.state === UNAVAILABLE) {
|
|
401
|
+
const unavailableProgress = {
|
|
402
|
+
state: "unavailable",
|
|
403
|
+
handle,
|
|
404
|
+
};
|
|
405
|
+
progressSignal.notify(unavailableProgress);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (handle.state === DELETED) {
|
|
409
|
+
throw new Error(`Document ${id} was deleted`);
|
|
410
|
+
}
|
|
411
|
+
progressSignal.notify({ state: "ready", handle });
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
progressSignal.notify({
|
|
415
|
+
state: "failed",
|
|
416
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
417
|
+
handle: this.#getHandle({ documentId }),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async find(id, options = {}) {
|
|
422
|
+
const { allowableStates = ["ready"], signal } = options;
|
|
423
|
+
// Check if already aborted
|
|
424
|
+
if (signal?.aborted) {
|
|
425
|
+
throw new Error("Operation aborted");
|
|
426
|
+
}
|
|
427
|
+
const progress = this.findWithProgress(id, { signal });
|
|
428
|
+
if ("subscribe" in progress) {
|
|
429
|
+
this.#registerHandleWithSubsystems(progress.handle);
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const unsubscribe = progress.subscribe(state => {
|
|
432
|
+
if (allowableStates.includes(state.handle.state)) {
|
|
433
|
+
unsubscribe();
|
|
434
|
+
resolve(state.handle);
|
|
435
|
+
}
|
|
436
|
+
else if (state.state === "unavailable") {
|
|
437
|
+
unsubscribe();
|
|
438
|
+
reject(new Error(`Document ${id} is unavailable`));
|
|
439
|
+
}
|
|
440
|
+
else if (state.state === "failed") {
|
|
441
|
+
unsubscribe();
|
|
442
|
+
reject(state.error);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
if (progress.handle.state === READY) {
|
|
449
|
+
return progress.handle;
|
|
450
|
+
}
|
|
451
|
+
// If the handle isn't ready, wait for it and then return it
|
|
452
|
+
await progress.handle.whenReady([READY, UNAVAILABLE]);
|
|
453
|
+
return progress.handle;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Loads a document without waiting for ready state
|
|
458
|
+
*/
|
|
459
|
+
async #loadDocument(documentId) {
|
|
460
|
+
// If we have the handle cached, return it
|
|
461
|
+
if (this.#handleCache[documentId]) {
|
|
462
|
+
return this.#handleCache[documentId];
|
|
463
|
+
}
|
|
464
|
+
// If we don't already have the handle, make an empty one and try loading it
|
|
465
|
+
const handle = this.#getHandle({ documentId });
|
|
466
|
+
const loadedDoc = await (this.storageSubsystem
|
|
467
|
+
? this.storageSubsystem.loadDoc(handle.documentId)
|
|
468
|
+
: Promise.resolve(null));
|
|
469
|
+
if (loadedDoc) {
|
|
470
|
+
// We need to cast this to <T> because loadDoc operates in <unknowns>.
|
|
471
|
+
// This is really where we ought to be validating the input matches <T>.
|
|
472
|
+
handle.update(() => loadedDoc);
|
|
473
|
+
handle.doneLoading();
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
// Because the network subsystem might still be booting up, we wait
|
|
477
|
+
// here so that we don't immediately give up loading because we're still
|
|
478
|
+
// making our initial connection to a sync server.
|
|
479
|
+
await this.networkSubsystem.whenReady();
|
|
480
|
+
handle.request();
|
|
481
|
+
}
|
|
482
|
+
this.#registerHandleWithSubsystems(handle);
|
|
321
483
|
return handle;
|
|
322
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Retrieves a document by id. It gets data from the local system, but also emits a `document`
|
|
487
|
+
* event to advertise interest in the document.
|
|
488
|
+
*/
|
|
489
|
+
async findClassic(
|
|
490
|
+
/** The url or documentId of the handle to retrieve */
|
|
491
|
+
id, options = {}) {
|
|
492
|
+
const documentId = interpretAsDocumentId(id);
|
|
493
|
+
const { allowableStates, signal } = options;
|
|
494
|
+
return abortable((async () => {
|
|
495
|
+
const handle = await this.#loadDocument(documentId);
|
|
496
|
+
if (!allowableStates) {
|
|
497
|
+
await handle.whenReady([READY, UNAVAILABLE]);
|
|
498
|
+
if (handle.state === UNAVAILABLE && !signal?.aborted) {
|
|
499
|
+
throw new Error(`Document ${id} is unavailable`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return handle;
|
|
503
|
+
})(), signal);
|
|
504
|
+
}
|
|
323
505
|
delete(
|
|
324
506
|
/** The url or documentId of the handle to delete */
|
|
325
507
|
id) {
|
|
@@ -327,6 +509,7 @@ export class Repo extends EventEmitter {
|
|
|
327
509
|
const handle = this.#getHandle({ documentId });
|
|
328
510
|
handle.delete();
|
|
329
511
|
delete this.#handleCache[documentId];
|
|
512
|
+
delete this.#progressCache[documentId];
|
|
330
513
|
this.emit("delete-document", { documentId });
|
|
331
514
|
}
|
|
332
515
|
/**
|
|
@@ -339,9 +522,7 @@ export class Repo extends EventEmitter {
|
|
|
339
522
|
async export(id) {
|
|
340
523
|
const documentId = interpretAsDocumentId(id);
|
|
341
524
|
const handle = this.#getHandle({ documentId });
|
|
342
|
-
const doc =
|
|
343
|
-
if (!doc)
|
|
344
|
-
return undefined;
|
|
525
|
+
const doc = handle.doc();
|
|
345
526
|
return Automerge.save(doc);
|
|
346
527
|
}
|
|
347
528
|
/**
|
|
@@ -356,7 +537,15 @@ export class Repo extends EventEmitter {
|
|
|
356
537
|
});
|
|
357
538
|
return handle;
|
|
358
539
|
}
|
|
359
|
-
subscribeToRemotes = (remotes) => {
|
|
540
|
+
subscribeToRemotes = (remotes) => {
|
|
541
|
+
if (this.#remoteHeadsGossipingEnabled) {
|
|
542
|
+
this.#log("subscribeToRemotes", { remotes });
|
|
543
|
+
this.#remoteHeadsSubscriptions.subscribeToRemotes(remotes);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
this.#log("WARN: subscribeToRemotes called but remote heads gossiping is not enabled");
|
|
547
|
+
}
|
|
548
|
+
};
|
|
360
549
|
storageId = async () => {
|
|
361
550
|
if (!this.storageSubsystem) {
|
|
362
551
|
return undefined;
|
|
@@ -379,11 +568,7 @@ export class Repo extends EventEmitter {
|
|
|
379
568
|
? documents.map(id => this.#handleCache[id])
|
|
380
569
|
: Object.values(this.#handleCache);
|
|
381
570
|
await Promise.all(handles.map(async (handle) => {
|
|
382
|
-
|
|
383
|
-
if (!doc) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
return this.storageSubsystem.saveDoc(handle.documentId, doc);
|
|
571
|
+
return this.storageSubsystem.saveDoc(handle.documentId, handle.doc());
|
|
387
572
|
}));
|
|
388
573
|
}
|
|
389
574
|
/**
|
|
@@ -398,7 +583,9 @@ export class Repo extends EventEmitter {
|
|
|
398
583
|
return;
|
|
399
584
|
}
|
|
400
585
|
const handle = this.#getHandle({ documentId });
|
|
401
|
-
|
|
586
|
+
await handle.whenReady([READY, UNLOADED, DELETED, UNAVAILABLE]);
|
|
587
|
+
const doc = handle.doc();
|
|
588
|
+
// because this is an internal-ish function, we'll be extra careful about undefined docs here
|
|
402
589
|
if (doc) {
|
|
403
590
|
if (handle.isReady()) {
|
|
404
591
|
handle.unload();
|
|
@@ -421,7 +608,6 @@ export class Repo extends EventEmitter {
|
|
|
421
608
|
return this.flush();
|
|
422
609
|
}
|
|
423
610
|
metrics() {
|
|
424
|
-
|
|
425
|
-
return { documents: {} };
|
|
611
|
+
return { documents: this.synchronizer.metrics() };
|
|
426
612
|
}
|
|
427
613
|
}
|