@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { next as A } from "@automerge/automerge/slim";
|
|
2
2
|
import { decode } from "cbor-x";
|
|
3
3
|
import debug from "debug";
|
|
4
4
|
import { READY, REQUESTING, UNAVAILABLE, } from "../DocHandle.js";
|
|
5
|
+
import { isRequestMessage, } from "../network/messages.js";
|
|
5
6
|
import { Synchronizer } from "./Synchronizer.js";
|
|
6
|
-
import {
|
|
7
|
+
import { throttle } from "../helpers/throttle.js";
|
|
7
8
|
/**
|
|
8
9
|
* DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
|
|
9
10
|
* to bring it inline with all other peers' versions.
|
|
@@ -13,43 +14,49 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
13
14
|
syncDebounceRate = 100;
|
|
14
15
|
/** Active peers */
|
|
15
16
|
#peers = [];
|
|
17
|
+
#pendingSyncStateCallbacks = {};
|
|
16
18
|
#peerDocumentStatuses = {};
|
|
17
|
-
|
|
19
|
+
/** Sync state for each peer we've communicated with (including inactive peers) */
|
|
20
|
+
#syncStates = {};
|
|
21
|
+
#pendingSyncMessages = [];
|
|
22
|
+
// We keep this around at least in part for debugging.
|
|
23
|
+
// eslint-disable-next-line no-unused-private-class-members
|
|
24
|
+
#peerId;
|
|
18
25
|
#syncStarted = false;
|
|
19
|
-
#beelay;
|
|
20
26
|
#handle;
|
|
21
|
-
#
|
|
22
|
-
constructor({ handle,
|
|
27
|
+
#onLoadSyncState;
|
|
28
|
+
constructor({ handle, peerId, onLoadSyncState }) {
|
|
23
29
|
super();
|
|
30
|
+
this.#peerId = peerId;
|
|
24
31
|
this.#handle = handle;
|
|
25
|
-
this.#
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
this.#onLoadSyncState =
|
|
33
|
+
onLoadSyncState ?? (() => Promise.resolve(undefined));
|
|
34
|
+
const docId = handle.documentId.slice(0, 5);
|
|
35
|
+
this.#log = debug(`automerge-repo:docsync:${docId}`);
|
|
36
|
+
handle.on("change", throttle(() => this.#syncWithPeers(), this.syncDebounceRate));
|
|
28
37
|
handle.on("ephemeral-message-outbound", payload => this.#broadcastToPeers(payload));
|
|
29
|
-
handle
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (patch.value instanceof A.Link) {
|
|
34
|
-
return patch.value;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
})
|
|
39
|
-
.filter(v => v != null);
|
|
40
|
-
for (const link of newLinks) {
|
|
41
|
-
const { documentId: target } = parseAutomergeUrl(link.target);
|
|
42
|
-
this.#beelay.addLink({ from: this.#handle.documentId, to: target });
|
|
43
|
-
}
|
|
44
|
-
});
|
|
38
|
+
// Process pending sync messages immediately after the handle becomes ready.
|
|
39
|
+
void (async () => {
|
|
40
|
+
this.#processAllPendingSyncMessages();
|
|
41
|
+
})();
|
|
45
42
|
}
|
|
46
43
|
get peerStates() {
|
|
47
44
|
return this.#peerDocumentStatuses;
|
|
48
45
|
}
|
|
49
46
|
get documentId() {
|
|
50
|
-
return this.#
|
|
47
|
+
return this.#handle.documentId;
|
|
51
48
|
}
|
|
52
49
|
/// PRIVATE
|
|
50
|
+
async #syncWithPeers() {
|
|
51
|
+
try {
|
|
52
|
+
await this.#handle.whenReady();
|
|
53
|
+
const doc = this.#handle.doc(); // XXX THIS ONE IS WEIRD
|
|
54
|
+
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc));
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.log("sync with peers threw an exception");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
53
60
|
async #broadcastToPeers({ data, }) {
|
|
54
61
|
this.#log(`broadcastToPeers`, this.#peers);
|
|
55
62
|
this.#peers.forEach(peerId => this.#sendEphemeralMessage(peerId, data));
|
|
@@ -59,78 +66,155 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
59
66
|
const message = {
|
|
60
67
|
type: "ephemeral",
|
|
61
68
|
targetId: peerId,
|
|
62
|
-
documentId: this.documentId,
|
|
69
|
+
documentId: this.#handle.documentId,
|
|
63
70
|
data,
|
|
64
71
|
};
|
|
65
72
|
this.emit("message", message);
|
|
66
73
|
}
|
|
74
|
+
#withSyncState(peerId, callback) {
|
|
75
|
+
this.#addPeer(peerId);
|
|
76
|
+
if (!(peerId in this.#peerDocumentStatuses)) {
|
|
77
|
+
this.#peerDocumentStatuses[peerId] = "unknown";
|
|
78
|
+
}
|
|
79
|
+
const syncState = this.#syncStates[peerId];
|
|
80
|
+
if (syncState) {
|
|
81
|
+
callback(syncState);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let pendingCallbacks = this.#pendingSyncStateCallbacks[peerId];
|
|
85
|
+
if (!pendingCallbacks) {
|
|
86
|
+
this.#onLoadSyncState(peerId)
|
|
87
|
+
.then(syncState => {
|
|
88
|
+
this.#initSyncState(peerId, syncState ?? A.initSyncState());
|
|
89
|
+
})
|
|
90
|
+
.catch(err => {
|
|
91
|
+
this.#log(`Error loading sync state for ${peerId}: ${err}`);
|
|
92
|
+
});
|
|
93
|
+
pendingCallbacks = this.#pendingSyncStateCallbacks[peerId] = [];
|
|
94
|
+
}
|
|
95
|
+
pendingCallbacks.push(callback);
|
|
96
|
+
}
|
|
97
|
+
#addPeer(peerId) {
|
|
98
|
+
if (!this.#peers.includes(peerId)) {
|
|
99
|
+
this.#peers.push(peerId);
|
|
100
|
+
this.emit("open-doc", { documentId: this.documentId, peerId });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
#initSyncState(peerId, syncState) {
|
|
104
|
+
const pendingCallbacks = this.#pendingSyncStateCallbacks[peerId];
|
|
105
|
+
if (pendingCallbacks) {
|
|
106
|
+
for (const callback of pendingCallbacks) {
|
|
107
|
+
callback(syncState);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
delete this.#pendingSyncStateCallbacks[peerId];
|
|
111
|
+
this.#syncStates[peerId] = syncState;
|
|
112
|
+
}
|
|
113
|
+
#setSyncState(peerId, syncState) {
|
|
114
|
+
this.#syncStates[peerId] = syncState;
|
|
115
|
+
this.emit("sync-state", {
|
|
116
|
+
peerId,
|
|
117
|
+
syncState,
|
|
118
|
+
documentId: this.#handle.documentId,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
#sendSyncMessage(peerId, doc) {
|
|
122
|
+
this.#log(`sendSyncMessage ->${peerId}`);
|
|
123
|
+
this.#withSyncState(peerId, syncState => {
|
|
124
|
+
const [newSyncState, message] = A.generateSyncMessage(doc, syncState);
|
|
125
|
+
if (message) {
|
|
126
|
+
this.#setSyncState(peerId, newSyncState);
|
|
127
|
+
const isNew = A.getHeads(doc).length === 0;
|
|
128
|
+
if (!this.#handle.isReady() &&
|
|
129
|
+
isNew &&
|
|
130
|
+
newSyncState.sharedHeads.length === 0 &&
|
|
131
|
+
!Object.values(this.#peerDocumentStatuses).includes("has") &&
|
|
132
|
+
this.#peerDocumentStatuses[peerId] === "unknown") {
|
|
133
|
+
// we don't have the document (or access to it), so we request it
|
|
134
|
+
this.emit("message", {
|
|
135
|
+
type: "request",
|
|
136
|
+
targetId: peerId,
|
|
137
|
+
documentId: this.#handle.documentId,
|
|
138
|
+
data: message,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.emit("message", {
|
|
143
|
+
type: "sync",
|
|
144
|
+
targetId: peerId,
|
|
145
|
+
data: message,
|
|
146
|
+
documentId: this.#handle.documentId,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// if we have sent heads, then the peer now has or will have the document
|
|
150
|
+
if (!isNew) {
|
|
151
|
+
this.#peerDocumentStatuses[peerId] = "has";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
67
156
|
/// PUBLIC
|
|
68
157
|
hasPeer(peerId) {
|
|
69
158
|
return this.#peers.includes(peerId);
|
|
70
159
|
}
|
|
71
|
-
beginSync(peerIds) {
|
|
72
|
-
this.#
|
|
73
|
-
const docPromise = this.#handle
|
|
160
|
+
async beginSync(peerIds) {
|
|
161
|
+
void this.#handle
|
|
74
162
|
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
75
|
-
.then(
|
|
163
|
+
.then(() => {
|
|
76
164
|
this.#syncStarted = true;
|
|
77
165
|
this.#checkDocUnavailable();
|
|
78
166
|
})
|
|
79
|
-
|
|
80
|
-
.
|
|
167
|
+
.catch(e => {
|
|
168
|
+
console.log("caught whenready", e);
|
|
169
|
+
this.#syncStarted = true;
|
|
170
|
+
this.#checkDocUnavailable();
|
|
171
|
+
});
|
|
172
|
+
const peersWithDocument = this.#peers.some(peerId => {
|
|
173
|
+
return this.#peerDocumentStatuses[peerId] == "has";
|
|
174
|
+
});
|
|
175
|
+
if (peersWithDocument) {
|
|
176
|
+
await this.#handle.whenReady();
|
|
177
|
+
}
|
|
81
178
|
peerIds.forEach(peerId => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.#
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let doc = d;
|
|
102
|
-
for (const commitOrBundle of commitOrBundles) {
|
|
103
|
-
doc = A.loadIncremental(doc, commitOrBundle.contents);
|
|
104
|
-
}
|
|
105
|
-
return doc;
|
|
106
|
-
});
|
|
107
|
-
this.#checkDocUnavailable();
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
this.#checkDocUnavailable();
|
|
179
|
+
this.#withSyncState(peerId, syncState => {
|
|
180
|
+
// HACK: if we have a sync state already, we round-trip it through the encoding system to make
|
|
181
|
+
// sure state is preserved. This prevents an infinite loop caused by failed attempts to send
|
|
182
|
+
// messages during disconnection.
|
|
183
|
+
// TODO: cover that case with a test and remove this hack
|
|
184
|
+
const reparsedSyncState = A.decodeSyncState(A.encodeSyncState(syncState));
|
|
185
|
+
this.#setSyncState(peerId, reparsedSyncState);
|
|
186
|
+
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
187
|
+
// with; but we don't want to surface that state to the front end
|
|
188
|
+
this.#handle
|
|
189
|
+
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
190
|
+
.then(() => {
|
|
191
|
+
const doc = this.#handle.isReady()
|
|
192
|
+
? this.#handle.doc()
|
|
193
|
+
: A.init();
|
|
194
|
+
const noPeersWithDocument = peerIds.every(peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]);
|
|
195
|
+
const wasUnavailable = doc === undefined;
|
|
196
|
+
if (wasUnavailable && noPeersWithDocument) {
|
|
197
|
+
return;
|
|
113
198
|
}
|
|
114
|
-
|
|
199
|
+
// If the doc is unavailable we still need a blank document to generate
|
|
200
|
+
// the sync message from
|
|
201
|
+
this.#sendSyncMessage(peerId, doc ?? A.init());
|
|
202
|
+
})
|
|
203
|
+
.catch(err => {
|
|
204
|
+
this.#log(`Error loading doc for ${peerId}: ${err}`);
|
|
115
205
|
});
|
|
116
206
|
});
|
|
117
207
|
});
|
|
118
208
|
}
|
|
119
|
-
peerWantsDocument(peerId) {
|
|
120
|
-
this.#peerDocumentStatuses[peerId] = "wants";
|
|
121
|
-
if (!this.#peers.includes(peerId)) {
|
|
122
|
-
this.beginSync([peerId]);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
209
|
endSync(peerId) {
|
|
126
210
|
this.#log(`removing peer ${peerId}`);
|
|
127
211
|
this.#peers = this.#peers.filter(p => p !== peerId);
|
|
128
|
-
this.#beelay.cancelListens(peerId);
|
|
129
212
|
}
|
|
130
213
|
receiveMessage(message) {
|
|
131
214
|
switch (message.type) {
|
|
132
215
|
case "sync":
|
|
133
216
|
case "request":
|
|
217
|
+
this.receiveSyncMessage(message);
|
|
134
218
|
break;
|
|
135
219
|
case "ephemeral":
|
|
136
220
|
this.receiveEphemeralMessage(message);
|
|
@@ -144,7 +228,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
144
228
|
}
|
|
145
229
|
}
|
|
146
230
|
receiveEphemeralMessage(message) {
|
|
147
|
-
if (message.documentId !== this.documentId)
|
|
231
|
+
if (message.documentId !== this.#handle.documentId)
|
|
148
232
|
throw new Error(`channelId doesn't match documentId`);
|
|
149
233
|
const { senderId, data } = message;
|
|
150
234
|
const contents = decode(new Uint8Array(data));
|
|
@@ -162,7 +246,45 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
162
246
|
});
|
|
163
247
|
});
|
|
164
248
|
}
|
|
165
|
-
receiveSyncMessage(message) {
|
|
249
|
+
receiveSyncMessage(message) {
|
|
250
|
+
if (message.documentId !== this.#handle.documentId)
|
|
251
|
+
throw new Error(`channelId doesn't match documentId`);
|
|
252
|
+
// We need to block receiving the syncMessages until we've checked local storage
|
|
253
|
+
if (!this.#handle.inState([READY, REQUESTING, UNAVAILABLE])) {
|
|
254
|
+
this.#pendingSyncMessages.push({ message, received: new Date() });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.#processAllPendingSyncMessages();
|
|
258
|
+
this.#processSyncMessage(message);
|
|
259
|
+
}
|
|
260
|
+
#processSyncMessage(message) {
|
|
261
|
+
if (isRequestMessage(message)) {
|
|
262
|
+
this.#peerDocumentStatuses[message.senderId] = "wants";
|
|
263
|
+
}
|
|
264
|
+
this.#checkDocUnavailable();
|
|
265
|
+
// if the message has heads, then the peer has the document
|
|
266
|
+
if (A.decodeSyncMessage(message.data).heads.length > 0) {
|
|
267
|
+
this.#peerDocumentStatuses[message.senderId] = "has";
|
|
268
|
+
}
|
|
269
|
+
this.#withSyncState(message.senderId, syncState => {
|
|
270
|
+
this.#handle.update(doc => {
|
|
271
|
+
const start = performance.now();
|
|
272
|
+
const [newDoc, newSyncState] = A.receiveSyncMessage(doc, syncState, message.data);
|
|
273
|
+
const end = performance.now();
|
|
274
|
+
this.emit("metrics", {
|
|
275
|
+
type: "receive-sync-message",
|
|
276
|
+
documentId: this.#handle.documentId,
|
|
277
|
+
durationMillis: end - start,
|
|
278
|
+
...A.stats(doc),
|
|
279
|
+
});
|
|
280
|
+
this.#setSyncState(message.senderId, newSyncState);
|
|
281
|
+
// respond to just this peer (as required)
|
|
282
|
+
this.#sendSyncMessage(message.senderId, doc);
|
|
283
|
+
return newDoc;
|
|
284
|
+
});
|
|
285
|
+
this.#checkDocUnavailable();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
166
288
|
#checkDocUnavailable() {
|
|
167
289
|
// if we know none of the peers have the document, tell all our peers that we don't either
|
|
168
290
|
if (this.#syncStarted &&
|
|
@@ -174,20 +296,24 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
174
296
|
.forEach(peerId => {
|
|
175
297
|
const message = {
|
|
176
298
|
type: "doc-unavailable",
|
|
177
|
-
documentId: this.documentId,
|
|
299
|
+
documentId: this.#handle.documentId,
|
|
178
300
|
targetId: peerId,
|
|
179
301
|
};
|
|
180
302
|
this.emit("message", message);
|
|
181
303
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
304
|
+
this.#handle.unavailable();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
#processAllPendingSyncMessages() {
|
|
308
|
+
for (const message of this.#pendingSyncMessages) {
|
|
309
|
+
this.#processSyncMessage(message.message);
|
|
185
310
|
}
|
|
311
|
+
this.#pendingSyncMessages = [];
|
|
186
312
|
}
|
|
187
313
|
metrics() {
|
|
188
314
|
return {
|
|
189
315
|
peers: this.#peers,
|
|
190
|
-
size: this.#handle
|
|
316
|
+
size: this.#handle.metrics(),
|
|
191
317
|
};
|
|
192
318
|
}
|
|
193
319
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -26,12 +26,15 @@ export type LegacyDocumentId = string & {
|
|
|
26
26
|
__legacyDocumentId: true;
|
|
27
27
|
};
|
|
28
28
|
export type AnyDocumentId = AutomergeUrl | DocumentId | BinaryDocumentId | LegacyDocumentId;
|
|
29
|
+
export type UrlHeads = string[] & {
|
|
30
|
+
__automergeUrlHeads: unknown;
|
|
31
|
+
};
|
|
29
32
|
/** A branded type for peer IDs */
|
|
30
33
|
export type PeerId = string & {
|
|
31
34
|
__peerId: true;
|
|
32
35
|
};
|
|
33
36
|
/** A randomly generated string created when the {@link Repo} starts up */
|
|
34
37
|
export type SessionId = string & {
|
|
35
|
-
|
|
38
|
+
__sessionId: true;
|
|
36
39
|
};
|
|
37
40
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AAExD,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAEpE,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AAExD,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAEpE,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,gBAAgB,CAAA;AAGpB,MAAM,MAAM,QAAQ,GAAG,MAAM,EAAE,GAAG;IAAE,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAAA;AAElE,kCAAkC;AAClC,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAA;AAEhD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,CAAA"}
|
package/fuzz/fuzz.ts
CHANGED
|
@@ -107,9 +107,9 @@ for (let i = 0; i < 100000; i++) {
|
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
await pause(0)
|
|
110
|
-
const a = await aliceRepo.find(doc.url).doc()
|
|
111
|
-
const b = await bobRepo.find(doc.url).doc()
|
|
112
|
-
const c = await charlieRepo.find(doc.url).doc()
|
|
110
|
+
const a = (await aliceRepo.find(doc.url)).doc()
|
|
111
|
+
const b = (await bobRepo.find(doc.url)).doc()
|
|
112
|
+
const c = (await charlieRepo.find(doc.url)).doc()
|
|
113
113
|
assert.deepStrictEqual(a, b, "A and B should be equal")
|
|
114
114
|
assert.deepStrictEqual(b, c, "B and C should be equal")
|
|
115
115
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
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>",
|
|
@@ -20,17 +20,16 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"http-server": "^14.1.0",
|
|
23
|
+
"ts-node": "^10.9.2",
|
|
23
24
|
"vite": "^5.0.8"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@automerge/automerge": "
|
|
27
|
+
"@automerge/automerge": "^2.2.8",
|
|
27
28
|
"bs58check": "^3.0.1",
|
|
28
29
|
"cbor-x": "^1.3.0",
|
|
29
30
|
"debug": "^4.3.4",
|
|
30
31
|
"eventemitter3": "^5.0.1",
|
|
31
32
|
"fast-sha256": "^1.3.0",
|
|
32
|
-
"tiny-typed-emitter": "^2.1.0",
|
|
33
|
-
"ts-node": "^10.9.1",
|
|
34
33
|
"uuid": "^9.0.0",
|
|
35
34
|
"xstate": "^5.9.1"
|
|
36
35
|
},
|
|
@@ -60,5 +59,5 @@
|
|
|
60
59
|
"publishConfig": {
|
|
61
60
|
"access": "public"
|
|
62
61
|
},
|
|
63
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "6e8d13f3aebcbc31ff3dd478be85191eee1ad120"
|
|
64
63
|
}
|
package/src/AutomergeUrl.ts
CHANGED
|
@@ -4,26 +4,54 @@ import type {
|
|
|
4
4
|
BinaryDocumentId,
|
|
5
5
|
DocumentId,
|
|
6
6
|
AnyDocumentId,
|
|
7
|
+
UrlHeads,
|
|
7
8
|
} from "./types.js"
|
|
9
|
+
|
|
8
10
|
import * as Uuid from "uuid"
|
|
9
11
|
import bs58check from "bs58check"
|
|
12
|
+
import {
|
|
13
|
+
uint8ArrayFromHexString,
|
|
14
|
+
uint8ArrayToHexString,
|
|
15
|
+
} from "./helpers/bufferFromHex.js"
|
|
16
|
+
|
|
17
|
+
import type { Heads as AutomergeHeads } from "@automerge/automerge/slim"
|
|
10
18
|
|
|
11
19
|
export const urlPrefix = "automerge:"
|
|
12
20
|
|
|
21
|
+
interface ParsedAutomergeUrl {
|
|
22
|
+
/** unencoded DocumentId */
|
|
23
|
+
binaryDocumentId: BinaryDocumentId
|
|
24
|
+
/** bs58 encoded DocumentId */
|
|
25
|
+
documentId: DocumentId
|
|
26
|
+
/** Optional array of heads, if specified in URL */
|
|
27
|
+
heads?: UrlHeads
|
|
28
|
+
/** Optional hex array of heads, in Automerge core format */
|
|
29
|
+
hexHeads?: string[] // AKA: heads
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/** Given an Automerge URL, returns the DocumentId in both base58check-encoded form and binary form */
|
|
14
|
-
export const parseAutomergeUrl = (url: AutomergeUrl) => {
|
|
33
|
+
export const parseAutomergeUrl = (url: AutomergeUrl): ParsedAutomergeUrl => {
|
|
34
|
+
const [baseUrl, headsSection, ...rest] = url.split("#")
|
|
35
|
+
if (rest.length > 0) {
|
|
36
|
+
throw new Error("Invalid URL: contains multiple heads sections")
|
|
37
|
+
}
|
|
15
38
|
const regex = new RegExp(`^${urlPrefix}(\\w+)$`)
|
|
16
|
-
const [, docMatch] =
|
|
39
|
+
const [, docMatch] = baseUrl.match(regex) || []
|
|
17
40
|
const documentId = docMatch as DocumentId
|
|
18
41
|
const binaryDocumentId = documentIdToBinary(documentId)
|
|
19
42
|
|
|
20
43
|
if (!binaryDocumentId) throw new Error("Invalid document URL: " + url)
|
|
21
|
-
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
if (headsSection === undefined) return { binaryDocumentId, documentId }
|
|
45
|
+
|
|
46
|
+
const heads = (headsSection === "" ? [] : headsSection.split("|")) as UrlHeads
|
|
47
|
+
const hexHeads = heads.map(head => {
|
|
48
|
+
try {
|
|
49
|
+
return uint8ArrayToHexString(bs58check.decode(head))
|
|
50
|
+
} catch (e) {
|
|
51
|
+
throw new Error(`Invalid head in URL: ${head}`)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return { binaryDocumentId, hexHeads, documentId, heads }
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
/**
|
|
@@ -32,38 +60,78 @@ export const parseAutomergeUrl = (url: AutomergeUrl) => {
|
|
|
32
60
|
*/
|
|
33
61
|
export const stringifyAutomergeUrl = (
|
|
34
62
|
arg: UrlOptions | DocumentId | BinaryDocumentId
|
|
35
|
-
) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
): AutomergeUrl => {
|
|
64
|
+
if (arg instanceof Uint8Array || typeof arg === "string") {
|
|
65
|
+
return (urlPrefix +
|
|
66
|
+
(arg instanceof Uint8Array
|
|
67
|
+
? binaryToDocumentId(arg)
|
|
68
|
+
: arg)) as AutomergeUrl
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { documentId, heads = undefined } = arg
|
|
72
|
+
|
|
73
|
+
if (documentId === undefined)
|
|
74
|
+
throw new Error("Invalid documentId: " + documentId)
|
|
42
75
|
|
|
43
76
|
const encodedDocumentId =
|
|
44
77
|
documentId instanceof Uint8Array
|
|
45
78
|
? binaryToDocumentId(documentId)
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
: documentId
|
|
80
|
+
|
|
81
|
+
let url = `${urlPrefix}${encodedDocumentId}`
|
|
82
|
+
|
|
83
|
+
if (heads !== undefined) {
|
|
84
|
+
heads.forEach(head => {
|
|
85
|
+
try {
|
|
86
|
+
bs58check.decode(head)
|
|
87
|
+
} catch (e) {
|
|
88
|
+
throw new Error(`Invalid head: ${head}`)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
url += "#" + heads.join("|")
|
|
92
|
+
}
|
|
49
93
|
|
|
50
|
-
|
|
51
|
-
|
|
94
|
+
return url as AutomergeUrl
|
|
95
|
+
}
|
|
52
96
|
|
|
53
|
-
|
|
97
|
+
/** Helper to extract just the heads from a URL if they exist */
|
|
98
|
+
export const getHeadsFromUrl = (url: AutomergeUrl): string[] | undefined => {
|
|
99
|
+
const { heads } = parseAutomergeUrl(url)
|
|
100
|
+
return heads
|
|
54
101
|
}
|
|
55
102
|
|
|
103
|
+
export const anyDocumentIdToAutomergeUrl = (id: AnyDocumentId) =>
|
|
104
|
+
isValidAutomergeUrl(id)
|
|
105
|
+
? id
|
|
106
|
+
: isValidDocumentId(id)
|
|
107
|
+
? stringifyAutomergeUrl({ documentId: id })
|
|
108
|
+
: isValidUuid(id)
|
|
109
|
+
? parseLegacyUUID(id)
|
|
110
|
+
: undefined
|
|
111
|
+
|
|
56
112
|
/**
|
|
57
113
|
* Given a string, returns true if it is a valid Automerge URL. This function also acts as a type
|
|
58
114
|
* discriminator in Typescript.
|
|
59
115
|
*/
|
|
60
116
|
export const isValidAutomergeUrl = (str: unknown): str is AutomergeUrl => {
|
|
61
|
-
if (typeof str !== "string"
|
|
62
|
-
|
|
63
|
-
const automergeUrl = str as AutomergeUrl
|
|
117
|
+
if (typeof str !== "string" || !str || !str.startsWith(urlPrefix))
|
|
118
|
+
return false
|
|
64
119
|
try {
|
|
65
|
-
const { documentId } = parseAutomergeUrl(
|
|
66
|
-
|
|
120
|
+
const { documentId, heads } = parseAutomergeUrl(str as AutomergeUrl)
|
|
121
|
+
if (!isValidDocumentId(documentId)) return false
|
|
122
|
+
if (
|
|
123
|
+
heads &&
|
|
124
|
+
!heads.every(head => {
|
|
125
|
+
try {
|
|
126
|
+
bs58check.decode(head)
|
|
127
|
+
return true
|
|
128
|
+
} catch {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
return false
|
|
134
|
+
return true
|
|
67
135
|
} catch {
|
|
68
136
|
return false
|
|
69
137
|
}
|
|
@@ -97,6 +165,12 @@ export const documentIdToBinary = (docId: DocumentId) =>
|
|
|
97
165
|
export const binaryToDocumentId = (docId: BinaryDocumentId) =>
|
|
98
166
|
bs58check.encode(docId) as DocumentId
|
|
99
167
|
|
|
168
|
+
export const encodeHeads = (heads: AutomergeHeads): UrlHeads =>
|
|
169
|
+
heads.map(h => bs58check.encode(uint8ArrayFromHexString(h))) as UrlHeads
|
|
170
|
+
|
|
171
|
+
export const decodeHeads = (heads: UrlHeads): AutomergeHeads =>
|
|
172
|
+
heads.map(h => uint8ArrayToHexString(bs58check.decode(h))) as AutomergeHeads
|
|
173
|
+
|
|
100
174
|
export const parseLegacyUUID = (str: string) => {
|
|
101
175
|
if (!Uuid.validate(str)) return undefined
|
|
102
176
|
const documentId = Uuid.parse(str) as BinaryDocumentId
|
|
@@ -141,4 +215,5 @@ export const interpretAsDocumentId = (id: AnyDocumentId) => {
|
|
|
141
215
|
|
|
142
216
|
type UrlOptions = {
|
|
143
217
|
documentId: DocumentId | BinaryDocumentId
|
|
218
|
+
heads?: UrlHeads
|
|
144
219
|
}
|