@automerge/automerge-repo 1.0.0-alpha.2 → 1.0.0-alpha.4
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/DocCollection.d.ts +4 -2
- package/dist/DocCollection.d.ts.map +1 -1
- package/dist/DocCollection.js +20 -11
- package/dist/DocHandle.d.ts +34 -6
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +69 -9
- package/dist/DocUrl.d.ts +4 -4
- package/dist/DocUrl.d.ts.map +1 -1
- package/dist/DocUrl.js +9 -9
- package/dist/EphemeralData.d.ts +8 -16
- package/dist/EphemeralData.d.ts.map +1 -1
- package/dist/EphemeralData.js +1 -28
- package/dist/Repo.d.ts +0 -2
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +37 -39
- package/dist/helpers/cbor.d.ts +4 -0
- package/dist/helpers/cbor.d.ts.map +1 -0
- package/dist/helpers/cbor.js +8 -0
- package/dist/helpers/eventPromise.d.ts +1 -1
- package/dist/helpers/eventPromise.d.ts.map +1 -1
- package/dist/helpers/headsAreSame.d.ts +0 -1
- package/dist/helpers/headsAreSame.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 +15 -13
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/network/NetworkAdapter.d.ts +6 -15
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.js +1 -1
- package/dist/network/NetworkSubsystem.d.ts +9 -6
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +69 -32
- package/dist/network/messages.d.ts +57 -0
- package/dist/network/messages.d.ts.map +1 -0
- package/dist/network/messages.js +21 -0
- package/dist/storage/StorageSubsystem.d.ts +1 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +2 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +19 -13
- package/dist/synchronizer/DocSynchronizer.d.ts +9 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +149 -34
- package/dist/synchronizer/Synchronizer.d.ts +4 -5
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/synchronizer/Synchronizer.js +1 -1
- package/dist/types.d.ts +1 -3
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +5 -5
- package/package.json +3 -3
- package/src/DocCollection.ts +23 -12
- package/src/DocHandle.ts +120 -13
- package/src/DocUrl.ts +10 -10
- package/src/EphemeralData.ts +6 -36
- package/src/Repo.ts +37 -55
- package/src/helpers/cbor.ts +10 -0
- package/src/helpers/eventPromise.ts +1 -1
- package/src/helpers/headsAreSame.ts +1 -1
- package/src/helpers/tests/network-adapter-tests.ts +18 -14
- package/src/index.ts +14 -2
- package/src/network/NetworkAdapter.ts +6 -22
- package/src/network/NetworkSubsystem.ts +94 -44
- package/src/network/messages.ts +123 -0
- package/src/storage/StorageSubsystem.ts +2 -2
- package/src/synchronizer/CollectionSynchronizer.ts +38 -19
- package/src/synchronizer/DocSynchronizer.ts +201 -43
- package/src/synchronizer/Synchronizer.ts +4 -9
- package/src/types.ts +4 -1
- package/test/CollectionSynchronizer.test.ts +6 -7
- package/test/DocCollection.test.ts +2 -2
- package/test/DocHandle.test.ts +32 -17
- package/test/DocSynchronizer.test.ts +85 -9
- package/test/Repo.test.ts +267 -63
- package/test/StorageSubsystem.test.ts +4 -5
- package/test/helpers/DummyNetworkAdapter.ts +12 -3
- package/test/helpers/DummyStorageAdapter.ts +1 -1
- package/tsconfig.json +4 -3
- package/test/EphemeralData.test.ts +0 -44
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DocCollection } from "../DocCollection.js";
|
|
2
|
-
import {
|
|
2
|
+
import { PeerId, DocumentId } from "../types.js";
|
|
3
3
|
import { Synchronizer } from "./Synchronizer.js";
|
|
4
|
+
import { SynchronizerMessage } from "../network/messages.js";
|
|
4
5
|
/** A CollectionSynchronizer is responsible for synchronizing a DocCollection with peers. */
|
|
5
6
|
export declare class CollectionSynchronizer extends Synchronizer {
|
|
6
7
|
#private;
|
|
@@ -10,7 +11,7 @@ export declare class CollectionSynchronizer extends Synchronizer {
|
|
|
10
11
|
* When we receive a sync message for a document we haven't got in memory, we
|
|
11
12
|
* register it with the repo and start synchronizing
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
+
receiveMessage(message: SynchronizerMessage): Promise<void>;
|
|
14
15
|
/**
|
|
15
16
|
* Starts synchronizing the given document with all peers that we share it generously with.
|
|
16
17
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAOnD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAOnD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,OAAO,EAGL,mBAAmB,EAEpB,MAAM,wBAAwB,CAAA;AAG/B,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAU1C,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,aAAa;IAiCvC;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,mBAAmB;IAyBjD;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAYlC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAgBtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;CAQ1B"}
|
|
@@ -10,6 +10,8 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
10
10
|
#peers = new Set();
|
|
11
11
|
/** A map of documentIds to their synchronizers */
|
|
12
12
|
#docSynchronizers = {};
|
|
13
|
+
/** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
|
|
14
|
+
#docSetUp = {};
|
|
13
15
|
constructor(repo) {
|
|
14
16
|
super();
|
|
15
17
|
this.repo = repo;
|
|
@@ -44,29 +46,30 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
44
46
|
* When we receive a sync message for a document we haven't got in memory, we
|
|
45
47
|
* register it with the repo and start synchronizing
|
|
46
48
|
*/
|
|
47
|
-
async
|
|
48
|
-
log(`onSyncMessage: ${
|
|
49
|
-
const documentId =
|
|
49
|
+
async receiveMessage(message) {
|
|
50
|
+
log(`onSyncMessage: ${message.senderId}, ${message.documentId}, ${"data" in message ? message.data.byteLength + "bytes" : ""}`);
|
|
51
|
+
const documentId = message.documentId;
|
|
50
52
|
if (!documentId) {
|
|
51
53
|
throw new Error("received a message with an invalid documentId");
|
|
52
54
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
this.#docSetUp[documentId] = true;
|
|
56
|
+
const docSynchronizer = this.#fetchDocSynchronizer(documentId);
|
|
57
|
+
docSynchronizer.receiveMessage(message);
|
|
55
58
|
// Initiate sync with any new peers
|
|
56
59
|
const peers = await this.#documentGenerousPeers(documentId);
|
|
57
|
-
peers
|
|
58
|
-
.filter(peerId => !docSynchronizer.hasPeer(peerId))
|
|
59
|
-
.forEach(peerId => docSynchronizer.beginSync(peerId));
|
|
60
|
+
docSynchronizer.beginSync(peers.filter(peerId => !docSynchronizer.hasPeer(peerId)));
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* Starts synchronizing the given document with all peers that we share it generously with.
|
|
63
64
|
*/
|
|
64
65
|
addDocument(documentId) {
|
|
66
|
+
// HACK: this is a hack to prevent us from adding the same document twice
|
|
67
|
+
if (this.#docSetUp[documentId]) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
65
70
|
const docSynchronizer = this.#fetchDocSynchronizer(documentId);
|
|
66
71
|
void this.#documentGenerousPeers(documentId).then(peers => {
|
|
67
|
-
|
|
68
|
-
docSynchronizer.beginSync(peerId);
|
|
69
|
-
});
|
|
72
|
+
docSynchronizer.beginSync(peers);
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
// TODO: implement this
|
|
@@ -76,12 +79,15 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
76
79
|
/** Adds a peer and maybe starts synchronizing with them */
|
|
77
80
|
addPeer(peerId) {
|
|
78
81
|
log(`adding ${peerId} & synchronizing with them`);
|
|
82
|
+
if (this.#peers.has(peerId)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
79
85
|
this.#peers.add(peerId);
|
|
80
86
|
for (const docSynchronizer of Object.values(this.#docSynchronizers)) {
|
|
81
87
|
const { documentId } = docSynchronizer;
|
|
82
|
-
|
|
88
|
+
this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
83
89
|
if (okToShare)
|
|
84
|
-
docSynchronizer.beginSync(peerId);
|
|
90
|
+
docSynchronizer.beginSync([peerId]);
|
|
85
91
|
});
|
|
86
92
|
}
|
|
87
93
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DocHandle } from "../DocHandle.js";
|
|
2
|
-
import {
|
|
2
|
+
import { PeerId } from "../types.js";
|
|
3
3
|
import { Synchronizer } from "./Synchronizer.js";
|
|
4
|
+
import { EphemeralMessage, RequestMessage, SynchronizerMessage, SyncMessage } from "../network/messages.js";
|
|
5
|
+
type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants";
|
|
4
6
|
/**
|
|
5
7
|
* DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
|
|
6
8
|
* to bring it inline with all other peers' versions.
|
|
@@ -9,10 +11,14 @@ export declare class DocSynchronizer extends Synchronizer {
|
|
|
9
11
|
#private;
|
|
10
12
|
private handle;
|
|
11
13
|
constructor(handle: DocHandle<any>);
|
|
14
|
+
get peerStates(): Record<PeerId, PeerDocumentStatus>;
|
|
12
15
|
get documentId(): import("../types.js").DocumentId;
|
|
13
16
|
hasPeer(peerId: PeerId): boolean;
|
|
14
|
-
beginSync(
|
|
17
|
+
beginSync(peerIds: PeerId[]): void;
|
|
15
18
|
endSync(peerId: PeerId): void;
|
|
16
|
-
|
|
19
|
+
receiveMessage(message: SynchronizerMessage): void;
|
|
20
|
+
receiveEphemeralMessage(message: EphemeralMessage): void;
|
|
21
|
+
receiveSyncMessage(message: SyncMessage | RequestMessage): void;
|
|
17
22
|
}
|
|
23
|
+
export {};
|
|
18
24
|
//# sourceMappingURL=DocSynchronizer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,SAAS,EAKV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,OAAO,EACL,gBAAgB,EAIhB,cAAc,EACd,mBAAmB,EACnB,WAAW,EACZ,MAAM,wBAAwB,CAAA;AAE/B,KAAK,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,CAAA;AAGrE;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,YAAY;;IAiBnC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC;IAoB1C,IAAI,UAAU,uCAEb;IAED,IAAI,UAAU,qCAEb;IAiHD,OAAO,CAAC,MAAM,EAAE,MAAM;IAItB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE;IA6B3B,OAAO,CAAC,MAAM,EAAE,MAAM;IAKtB,cAAc,CAAC,OAAO,EAAE,mBAAmB;IAkB3C,uBAAuB,CAAC,OAAO,EAAE,gBAAgB;IAuBjD,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc;CA2EzD"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import * as A from "@automerge/automerge";
|
|
2
|
-
import { READY, REQUESTING } from "../DocHandle.js";
|
|
1
|
+
import * as A from "@automerge/automerge/next";
|
|
2
|
+
import { READY, REQUESTING, UNAVAILABLE, } from "../DocHandle.js";
|
|
3
3
|
import { Synchronizer } from "./Synchronizer.js";
|
|
4
4
|
import debug from "debug";
|
|
5
|
+
import { isRequestMessage, } from "../network/messages.js";
|
|
6
|
+
import { decode } from "cbor-x";
|
|
5
7
|
/**
|
|
6
8
|
* DocSynchronizer takes a handle to an Automerge document, and receives & dispatches sync messages
|
|
7
9
|
* to bring it inline with all other peers' versions.
|
|
@@ -13,9 +15,11 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
13
15
|
#opsLog;
|
|
14
16
|
/** Active peers */
|
|
15
17
|
#peers = [];
|
|
18
|
+
#peerDocumentStatuses = {};
|
|
16
19
|
/** Sync state for each peer we've communicated with (including inactive peers) */
|
|
17
20
|
#syncStates = {};
|
|
18
21
|
#pendingSyncMessages = [];
|
|
22
|
+
#syncStarted = false;
|
|
19
23
|
constructor(handle) {
|
|
20
24
|
super();
|
|
21
25
|
this.handle = handle;
|
|
@@ -24,12 +28,16 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
24
28
|
this.#log = debug(`automerge-repo:docsync:${docId}`);
|
|
25
29
|
this.#opsLog = debug(`automerge-repo:ops:docsync:${docId}`); // Log list of ops of each message
|
|
26
30
|
handle.on("change", () => this.#syncWithPeers());
|
|
31
|
+
handle.on("ephemeral-message-outbound", payload => this.#broadcastToPeers(payload));
|
|
27
32
|
// Process pending sync messages immediately after the handle becomes ready.
|
|
28
33
|
void (async () => {
|
|
29
34
|
await handle.doc([READY, REQUESTING]);
|
|
30
35
|
this.#processAllPendingSyncMessages();
|
|
31
36
|
})();
|
|
32
37
|
}
|
|
38
|
+
get peerStates() {
|
|
39
|
+
return this.#peerDocumentStatuses;
|
|
40
|
+
}
|
|
33
41
|
get documentId() {
|
|
34
42
|
return this.handle.documentId;
|
|
35
43
|
}
|
|
@@ -37,13 +45,32 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
37
45
|
async #syncWithPeers() {
|
|
38
46
|
this.#log(`syncWithPeers`);
|
|
39
47
|
const doc = await this.handle.doc();
|
|
48
|
+
if (doc === undefined)
|
|
49
|
+
return;
|
|
40
50
|
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc));
|
|
41
51
|
}
|
|
52
|
+
async #broadcastToPeers({ data }) {
|
|
53
|
+
this.#log(`broadcastToPeers`, this.#peers);
|
|
54
|
+
this.#peers.forEach(peerId => this.#sendEphemeralMessage(peerId, data));
|
|
55
|
+
}
|
|
56
|
+
#sendEphemeralMessage(peerId, data) {
|
|
57
|
+
this.#log(`sendEphemeralMessage ->${peerId}`);
|
|
58
|
+
this.emit("message", {
|
|
59
|
+
type: "ephemeral",
|
|
60
|
+
targetId: peerId,
|
|
61
|
+
documentId: this.handle.documentId,
|
|
62
|
+
data,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
42
65
|
#getSyncState(peerId) {
|
|
43
66
|
if (!this.#peers.includes(peerId)) {
|
|
44
67
|
this.#log("adding a new peer", peerId);
|
|
45
68
|
this.#peers.push(peerId);
|
|
46
69
|
}
|
|
70
|
+
// when a peer is added, we don't know if it has the document or not
|
|
71
|
+
if (!(peerId in this.#peerDocumentStatuses)) {
|
|
72
|
+
this.#peerDocumentStatuses[peerId] = "unknown";
|
|
73
|
+
}
|
|
47
74
|
return this.#syncStates[peerId] ?? A.initSyncState();
|
|
48
75
|
}
|
|
49
76
|
#setSyncState(peerId, syncState) {
|
|
@@ -59,16 +86,32 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
59
86
|
this.#setSyncState(peerId, newSyncState);
|
|
60
87
|
if (message) {
|
|
61
88
|
this.#logMessage(`sendSyncMessage 🡒 ${peerId}`, message);
|
|
62
|
-
const
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
const decoded = A.decodeSyncMessage(message);
|
|
90
|
+
if (!this.handle.isReady() &&
|
|
91
|
+
decoded.heads.length === 0 &&
|
|
92
|
+
newSyncState.sharedHeads.length === 0 &&
|
|
93
|
+
!Object.values(this.#peerDocumentStatuses).includes("has") &&
|
|
94
|
+
this.#peerDocumentStatuses[peerId] === "unknown") {
|
|
95
|
+
// we don't have the document (or access to it), so we request it
|
|
96
|
+
this.emit("message", {
|
|
97
|
+
type: "request",
|
|
98
|
+
targetId: peerId,
|
|
99
|
+
documentId: this.handle.documentId,
|
|
100
|
+
data: message,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.emit("message", {
|
|
105
|
+
type: "sync",
|
|
106
|
+
targetId: peerId,
|
|
107
|
+
data: message,
|
|
108
|
+
documentId: this.handle.documentId,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// if we have sent heads, then the peer now has or will have the document
|
|
112
|
+
if (decoded.heads.length > 0) {
|
|
113
|
+
this.#peerDocumentStatuses[peerId] = "has";
|
|
114
|
+
}
|
|
72
115
|
}
|
|
73
116
|
}
|
|
74
117
|
#logMessage = (label, message) => {
|
|
@@ -81,7 +124,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
81
124
|
this.#log(logText, decoded);
|
|
82
125
|
// expanding is expensive, so only do it if we're logging at this level
|
|
83
126
|
const expanded = this.#opsLog.enabled
|
|
84
|
-
? decoded.changes.flatMap(change => A.decodeChange(change).ops.map(op => JSON.stringify(op)))
|
|
127
|
+
? decoded.changes.flatMap((change) => A.decodeChange(change).ops.map((op) => JSON.stringify(op)))
|
|
85
128
|
: null;
|
|
86
129
|
this.#opsLog(logText, expanded);
|
|
87
130
|
};
|
|
@@ -89,48 +132,120 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
89
132
|
hasPeer(peerId) {
|
|
90
133
|
return this.#peers.includes(peerId);
|
|
91
134
|
}
|
|
92
|
-
beginSync(
|
|
93
|
-
this.#log(`beginSync: ${
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// messages during disconnection.
|
|
100
|
-
// TODO: cover that case with a test and remove this hack
|
|
135
|
+
beginSync(peerIds) {
|
|
136
|
+
this.#log(`beginSync: ${peerIds.join(", ")}`);
|
|
137
|
+
// HACK: if we have a sync state already, we round-trip it through the encoding system to make
|
|
138
|
+
// sure state is preserved. This prevents an infinite loop caused by failed attempts to send
|
|
139
|
+
// messages during disconnection.
|
|
140
|
+
// TODO: cover that case with a test and remove this hack
|
|
141
|
+
peerIds.forEach(peerId => {
|
|
101
142
|
const syncStateRaw = this.#getSyncState(peerId);
|
|
102
143
|
const syncState = A.decodeSyncState(A.encodeSyncState(syncStateRaw));
|
|
103
144
|
this.#setSyncState(peerId, syncState);
|
|
104
|
-
|
|
145
|
+
});
|
|
146
|
+
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
147
|
+
// with; but we don't want to surface that state to the front end
|
|
148
|
+
void this.handle.doc([READY, REQUESTING, UNAVAILABLE]).then(doc => {
|
|
149
|
+
// we register out peers first, then say that sync has started
|
|
150
|
+
this.#syncStarted = true;
|
|
151
|
+
this.#checkDocUnavailable();
|
|
152
|
+
if (doc === undefined)
|
|
153
|
+
return;
|
|
154
|
+
peerIds.forEach(peerId => {
|
|
155
|
+
this.#sendSyncMessage(peerId, doc);
|
|
156
|
+
});
|
|
105
157
|
});
|
|
106
158
|
}
|
|
107
159
|
endSync(peerId) {
|
|
108
160
|
this.#log(`removing peer ${peerId}`);
|
|
109
161
|
this.#peers = this.#peers.filter(p => p !== peerId);
|
|
110
162
|
}
|
|
111
|
-
|
|
112
|
-
|
|
163
|
+
receiveMessage(message) {
|
|
164
|
+
switch (message.type) {
|
|
165
|
+
case "sync":
|
|
166
|
+
case "request":
|
|
167
|
+
this.receiveSyncMessage(message);
|
|
168
|
+
break;
|
|
169
|
+
case "ephemeral":
|
|
170
|
+
this.receiveEphemeralMessage(message);
|
|
171
|
+
break;
|
|
172
|
+
case "doc-unavailable":
|
|
173
|
+
this.#peerDocumentStatuses[message.senderId] = "unavailable";
|
|
174
|
+
this.#checkDocUnavailable();
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`unknown message type: ${message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
receiveEphemeralMessage(message) {
|
|
181
|
+
if (message.documentId !== this.handle.documentId)
|
|
182
|
+
throw new Error(`channelId doesn't match documentId`);
|
|
183
|
+
const { senderId, data } = message;
|
|
184
|
+
const contents = decode(data);
|
|
185
|
+
this.handle.emit("ephemeral-message", {
|
|
186
|
+
handle: this.handle,
|
|
187
|
+
senderId,
|
|
188
|
+
message: contents,
|
|
189
|
+
});
|
|
190
|
+
this.#peers.forEach(peerId => {
|
|
191
|
+
if (peerId === senderId)
|
|
192
|
+
return;
|
|
193
|
+
this.emit("message", {
|
|
194
|
+
...message,
|
|
195
|
+
targetId: peerId,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
receiveSyncMessage(message) {
|
|
200
|
+
if (message.documentId !== this.handle.documentId)
|
|
113
201
|
throw new Error(`channelId doesn't match documentId`);
|
|
114
202
|
// We need to block receiving the syncMessages until we've checked local storage
|
|
115
|
-
if (!this.handle.inState([READY, REQUESTING])) {
|
|
116
|
-
this.#pendingSyncMessages.push(
|
|
203
|
+
if (!this.handle.inState([READY, REQUESTING, UNAVAILABLE])) {
|
|
204
|
+
this.#pendingSyncMessages.push(message);
|
|
117
205
|
return;
|
|
118
206
|
}
|
|
119
207
|
this.#processAllPendingSyncMessages();
|
|
120
|
-
this.#processSyncMessage(
|
|
208
|
+
this.#processSyncMessage(message);
|
|
121
209
|
}
|
|
122
|
-
#processSyncMessage(
|
|
210
|
+
#processSyncMessage(message) {
|
|
211
|
+
if (isRequestMessage(message)) {
|
|
212
|
+
this.#peerDocumentStatuses[message.senderId] = "wants";
|
|
213
|
+
}
|
|
214
|
+
this.#checkDocUnavailable();
|
|
215
|
+
// if the message has heads, then the peer has the document
|
|
216
|
+
if (A.decodeSyncMessage(message.data).heads.length > 0) {
|
|
217
|
+
this.#peerDocumentStatuses[message.senderId] = "has";
|
|
218
|
+
}
|
|
123
219
|
this.handle.update(doc => {
|
|
124
|
-
const [newDoc, newSyncState] = A.receiveSyncMessage(doc, this.#getSyncState(
|
|
125
|
-
this.#setSyncState(
|
|
220
|
+
const [newDoc, newSyncState] = A.receiveSyncMessage(doc, this.#getSyncState(message.senderId), message.data);
|
|
221
|
+
this.#setSyncState(message.senderId, newSyncState);
|
|
126
222
|
// respond to just this peer (as required)
|
|
127
|
-
this.#sendSyncMessage(
|
|
223
|
+
this.#sendSyncMessage(message.senderId, doc);
|
|
128
224
|
return newDoc;
|
|
129
225
|
});
|
|
226
|
+
this.#checkDocUnavailable();
|
|
227
|
+
}
|
|
228
|
+
#checkDocUnavailable() {
|
|
229
|
+
// if we know none of the peers have the document, tell all our peers that we don't either
|
|
230
|
+
if (this.#syncStarted &&
|
|
231
|
+
this.handle.inState([REQUESTING]) &&
|
|
232
|
+
this.#peers.every(peerId => this.#peerDocumentStatuses[peerId] === "unavailable" ||
|
|
233
|
+
this.#peerDocumentStatuses[peerId] === "wants")) {
|
|
234
|
+
this.#peers
|
|
235
|
+
.filter(peerId => this.#peerDocumentStatuses[peerId] === "wants")
|
|
236
|
+
.forEach(peerId => {
|
|
237
|
+
this.emit("message", {
|
|
238
|
+
type: "doc-unavailable",
|
|
239
|
+
documentId: this.handle.documentId,
|
|
240
|
+
targetId: peerId,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
this.handle.unavailable();
|
|
244
|
+
}
|
|
130
245
|
}
|
|
131
246
|
#processAllPendingSyncMessages() {
|
|
132
|
-
for (const
|
|
133
|
-
this.#processSyncMessage(
|
|
247
|
+
for (const message of this.#pendingSyncMessages) {
|
|
248
|
+
this.#processSyncMessage(message);
|
|
134
249
|
}
|
|
135
250
|
this.#pendingSyncMessages = [];
|
|
136
251
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import EventEmitter from "eventemitter3";
|
|
2
|
-
import {
|
|
3
|
-
import { MessagePayload } from "../network/NetworkAdapter.js";
|
|
1
|
+
import { EventEmitter } from "eventemitter3";
|
|
2
|
+
import { Message, MessageContents } from "../network/messages.js";
|
|
4
3
|
export declare abstract class Synchronizer extends EventEmitter<SynchronizerEvents> {
|
|
5
|
-
abstract
|
|
4
|
+
abstract receiveMessage(message: Message): void;
|
|
6
5
|
}
|
|
7
6
|
export interface SynchronizerEvents {
|
|
8
|
-
message: (arg:
|
|
7
|
+
message: (arg: MessageContents) => void;
|
|
9
8
|
}
|
|
10
9
|
//# sourceMappingURL=Synchronizer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Synchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/Synchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"Synchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/Synchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAEjE,8BAAsB,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;CAChD;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAA;CACxC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,7 +10,5 @@ export type BinaryDocumentId = Uint8Array & {
|
|
|
10
10
|
export type PeerId = string & {
|
|
11
11
|
__peerId: false;
|
|
12
12
|
};
|
|
13
|
-
export type
|
|
14
|
-
__channelId: false;
|
|
15
|
-
};
|
|
13
|
+
export type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
|
|
16
14
|
//# 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,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AACxD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAC3D,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AACxD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAC3D,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAA;AAEjD,MAAM,MAAM,gBAAgB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,IAAI,CAAC,SAAS,GAAG,GAChE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GACV,KAAK,CAAA"}
|
package/fuzz/fuzz.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "assert"
|
|
2
2
|
import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
|
|
3
|
-
import * as Automerge from "@automerge/automerge"
|
|
3
|
+
import * as Automerge from "@automerge/automerge/next"
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { DocHandle, DocumentId, PeerId, SharePolicy } from "../src"
|
|
6
6
|
import { eventPromise } from "../src/helpers/eventPromise.js"
|
|
7
7
|
import { pause } from "../src/helpers/pause.js"
|
|
8
8
|
import { Repo } from "../src/Repo.js"
|
|
@@ -105,9 +105,9 @@ for (let i = 0; i < 100000; i++) {
|
|
|
105
105
|
})
|
|
106
106
|
|
|
107
107
|
await pause(0)
|
|
108
|
-
const a = await aliceRepo.find(doc.
|
|
109
|
-
const b = await bobRepo.find(doc.
|
|
110
|
-
const c = await charlieRepo.find(doc.
|
|
108
|
+
const a = await aliceRepo.find(doc.url).doc()
|
|
109
|
+
const b = await bobRepo.find(doc.url).doc()
|
|
110
|
+
const c = await charlieRepo.find(doc.url).doc()
|
|
111
111
|
assert.deepStrictEqual(a, b, "A and B should be equal")
|
|
112
112
|
assert.deepStrictEqual(b, c, "B and C should be equal")
|
|
113
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.4",
|
|
4
4
|
"description": "A repository object to manage a collection of automerge documents",
|
|
5
5
|
"repository": "https://github.com/automerge/automerge-repo",
|
|
6
6
|
"author": "Peter van Hardenberg <pvh@pvh.ca>",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"typescript": "^5.1.6"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@automerge/automerge": "^2.1.0-alpha.
|
|
34
|
+
"@automerge/automerge": "^2.1.0-alpha.12"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"bs58check": "^3.0.1",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "fbf71f0c3aaa2786a4e279f336f01d665f53ce5b"
|
|
69
69
|
}
|
package/src/DocCollection.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import EventEmitter from "eventemitter3"
|
|
1
|
+
import { EventEmitter } from "eventemitter3"
|
|
2
2
|
import { DocHandle } from "./DocHandle.js"
|
|
3
3
|
import { DocumentId, type BinaryDocumentId, AutomergeUrl } from "./types.js"
|
|
4
4
|
import { type SharePolicy } from "./Repo.js"
|
|
@@ -72,9 +72,9 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
|
|
|
72
72
|
// - pass a "reify" function that takes a `<any>` and returns `<T>`
|
|
73
73
|
|
|
74
74
|
// Generate a new UUID and store it in the buffer
|
|
75
|
-
const {
|
|
76
|
-
const handle = this.#getHandle<T>(
|
|
77
|
-
this.emit("document", { handle })
|
|
75
|
+
const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
|
|
76
|
+
const handle = this.#getHandle<T>(documentId, true) as DocHandle<T>
|
|
77
|
+
this.emit("document", { handle, isNew: true })
|
|
78
78
|
return handle
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -90,13 +90,22 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
|
|
|
90
90
|
throw new Error(`Invalid AutomergeUrl: '${automergeUrl}'`)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
const {
|
|
93
|
+
const { documentId } = parseAutomergeUrl(automergeUrl)
|
|
94
94
|
// If we have the handle cached, return it
|
|
95
|
-
if (this.#handleCache[
|
|
96
|
-
|
|
95
|
+
if (this.#handleCache[documentId]) {
|
|
96
|
+
if (this.#handleCache[documentId].isUnavailable()) {
|
|
97
|
+
// this ensures that the event fires after the handle has been returned
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
this.#handleCache[documentId].emit("unavailable", {
|
|
100
|
+
handle: this.#handleCache[documentId],
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
return this.#handleCache[documentId]
|
|
105
|
+
}
|
|
97
106
|
|
|
98
|
-
const handle = this.#getHandle<T>(
|
|
99
|
-
this.emit("document", { handle })
|
|
107
|
+
const handle = this.#getHandle<T>(documentId, false) as DocHandle<T>
|
|
108
|
+
this.emit("document", { handle, isNew: false })
|
|
100
109
|
return handle
|
|
101
110
|
}
|
|
102
111
|
|
|
@@ -105,7 +114,7 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
|
|
|
105
114
|
id: DocumentId | AutomergeUrl
|
|
106
115
|
) {
|
|
107
116
|
if (isValidAutomergeUrl(id)) {
|
|
108
|
-
;({
|
|
117
|
+
;({ documentId: id } = parseAutomergeUrl(id))
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
const handle = this.#getHandle(id, false)
|
|
@@ -113,7 +122,7 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
|
|
|
113
122
|
|
|
114
123
|
delete this.#handleCache[id]
|
|
115
124
|
this.emit("delete-document", {
|
|
116
|
-
|
|
125
|
+
documentId: id,
|
|
117
126
|
})
|
|
118
127
|
}
|
|
119
128
|
}
|
|
@@ -122,12 +131,14 @@ export class DocCollection extends EventEmitter<DocCollectionEvents> {
|
|
|
122
131
|
interface DocCollectionEvents {
|
|
123
132
|
document: (arg: DocumentPayload) => void
|
|
124
133
|
"delete-document": (arg: DeleteDocumentPayload) => void
|
|
134
|
+
"unavailable-document": (arg: DeleteDocumentPayload) => void
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
interface DocumentPayload {
|
|
128
138
|
handle: DocHandle<any>
|
|
139
|
+
isNew: boolean
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
interface DeleteDocumentPayload {
|
|
132
|
-
|
|
143
|
+
documentId: DocumentId
|
|
133
144
|
}
|