@automerge/automerge-repo 1.2.1 → 2.0.0-alpha.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/dist/AutomergeUrl.d.ts +3 -3
- package/dist/AutomergeUrl.d.ts.map +1 -1
- package/dist/AutomergeUrl.js +5 -1
- package/dist/DocHandle.d.ts +11 -10
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +23 -43
- package/dist/Repo.d.ts +1 -1
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +53 -36
- package/dist/entrypoints/slim.d.ts +1 -0
- package/dist/entrypoints/slim.d.ts.map +1 -1
- package/dist/entrypoints/slim.js +1 -0
- package/dist/helpers/DummyNetworkAdapter.d.ts +3 -0
- package/dist/helpers/DummyNetworkAdapter.d.ts.map +1 -1
- package/dist/helpers/DummyNetworkAdapter.js +24 -5
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +88 -1
- package/dist/helpers/throttle.d.ts +1 -1
- package/dist/helpers/throttle.js +1 -1
- package/dist/network/NetworkAdapter.d.ts +2 -0
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkAdapterInterface.d.ts +2 -2
- package/dist/network/NetworkAdapterInterface.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.d.ts +5 -2
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +21 -25
- package/package.json +3 -3
- package/src/AutomergeUrl.ts +6 -6
- package/src/DocHandle.ts +27 -57
- package/src/Repo.ts +55 -40
- package/src/entrypoints/slim.ts +1 -0
- package/src/helpers/DummyNetworkAdapter.ts +29 -5
- package/src/helpers/tests/network-adapter-tests.ts +121 -1
- package/src/helpers/throttle.ts +1 -1
- package/src/network/NetworkAdapter.ts +3 -0
- package/src/network/NetworkAdapterInterface.ts +4 -3
- package/src/network/NetworkSubsystem.ts +24 -31
- package/test/AutomergeUrl.test.ts +4 -0
- package/test/DocHandle.test.ts +20 -24
- package/test/DocSynchronizer.test.ts +5 -1
- package/test/NetworkSubsystem.test.ts +107 -0
- package/test/Repo.test.ts +37 -15
- package/test/remoteHeads.test.ts +3 -3
- package/test/Network.test.ts +0 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "assert";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { Repo } from "../../index.js";
|
|
3
|
+
import { generateAutomergeUrl, parseAutomergeUrl, Repo, } from "../../index.js";
|
|
4
4
|
import { eventPromise, eventPromises } from "../eventPromise.js";
|
|
5
5
|
import { pause } from "../pause.js";
|
|
6
6
|
/**
|
|
@@ -125,6 +125,93 @@ export function runNetworkAdapterTests(_setup, title) {
|
|
|
125
125
|
});
|
|
126
126
|
teardown();
|
|
127
127
|
});
|
|
128
|
+
it("should emit disconnect events on disconnect", async () => {
|
|
129
|
+
const { adapters, teardown } = await setup();
|
|
130
|
+
const left = adapters[0][0];
|
|
131
|
+
const right = adapters[1][0];
|
|
132
|
+
const leftPeerId = "left";
|
|
133
|
+
const rightPeerId = "right";
|
|
134
|
+
const leftRepo = new Repo({
|
|
135
|
+
network: [left],
|
|
136
|
+
peerId: leftPeerId,
|
|
137
|
+
});
|
|
138
|
+
const rightRepo = new Repo({
|
|
139
|
+
network: [right],
|
|
140
|
+
peerId: rightPeerId,
|
|
141
|
+
});
|
|
142
|
+
await Promise.all([
|
|
143
|
+
eventPromise(leftRepo.networkSubsystem, "peer"),
|
|
144
|
+
eventPromise(rightRepo.networkSubsystem, "peer"),
|
|
145
|
+
]);
|
|
146
|
+
const disconnectionPromises = Promise.all([
|
|
147
|
+
eventPromise(leftRepo.networkSubsystem, "peer-disconnected"),
|
|
148
|
+
eventPromise(rightRepo.networkSubsystem, "peer-disconnected"),
|
|
149
|
+
]);
|
|
150
|
+
left.disconnect();
|
|
151
|
+
await disconnectionPromises;
|
|
152
|
+
teardown();
|
|
153
|
+
});
|
|
154
|
+
it("should not send messages after disconnect", async () => {
|
|
155
|
+
const { adapters, teardown } = await setup();
|
|
156
|
+
const left = adapters[0][0];
|
|
157
|
+
const right = adapters[1][0];
|
|
158
|
+
const leftPeerId = "left";
|
|
159
|
+
const rightPeerId = "right";
|
|
160
|
+
const leftRepo = new Repo({
|
|
161
|
+
network: [left],
|
|
162
|
+
peerId: leftPeerId,
|
|
163
|
+
});
|
|
164
|
+
const rightRepo = new Repo({
|
|
165
|
+
network: [right],
|
|
166
|
+
peerId: rightPeerId,
|
|
167
|
+
});
|
|
168
|
+
await Promise.all([
|
|
169
|
+
eventPromise(rightRepo.networkSubsystem, "peer"),
|
|
170
|
+
eventPromise(leftRepo.networkSubsystem, "peer"),
|
|
171
|
+
]);
|
|
172
|
+
const disconnected = eventPromise(right, "peer-disconnected");
|
|
173
|
+
left.disconnect();
|
|
174
|
+
await disconnected;
|
|
175
|
+
const rightReceivedFromLeft = new Promise(resolve => {
|
|
176
|
+
right.on("message", msg => {
|
|
177
|
+
if (msg.senderId === leftPeerId) {
|
|
178
|
+
resolve(null);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
const rightReceived = Promise.race([rightReceivedFromLeft, pause(10)]);
|
|
183
|
+
const documentId = parseAutomergeUrl(generateAutomergeUrl()).documentId;
|
|
184
|
+
left.send({
|
|
185
|
+
type: "foo",
|
|
186
|
+
data: new Uint8Array([1, 2, 3]),
|
|
187
|
+
documentId,
|
|
188
|
+
senderId: leftPeerId,
|
|
189
|
+
targetId: rightPeerId,
|
|
190
|
+
});
|
|
191
|
+
assert.equal(await rightReceived, null);
|
|
192
|
+
teardown();
|
|
193
|
+
});
|
|
194
|
+
it("should support reconnecting after disconnect", async () => {
|
|
195
|
+
const { adapters, teardown } = await setup();
|
|
196
|
+
const left = adapters[0][0];
|
|
197
|
+
const right = adapters[1][0];
|
|
198
|
+
const leftPeerId = "left";
|
|
199
|
+
const rightPeerId = "right";
|
|
200
|
+
const _leftRepo = new Repo({
|
|
201
|
+
network: [left],
|
|
202
|
+
peerId: leftPeerId,
|
|
203
|
+
});
|
|
204
|
+
const rightRepo = new Repo({
|
|
205
|
+
network: [right],
|
|
206
|
+
peerId: rightPeerId,
|
|
207
|
+
});
|
|
208
|
+
await eventPromise(rightRepo.networkSubsystem, "peer");
|
|
209
|
+
left.disconnect();
|
|
210
|
+
await pause(10);
|
|
211
|
+
left.connect(leftPeerId);
|
|
212
|
+
await eventPromise(left, "peer-candidate");
|
|
213
|
+
teardown();
|
|
214
|
+
});
|
|
128
215
|
});
|
|
129
216
|
}
|
|
130
217
|
const NO_OP = () => { };
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*
|
|
22
22
|
* Example usage:
|
|
23
|
-
* const callback =
|
|
23
|
+
* const callback = throttle((ev) => { doSomethingExpensiveOrOccasional() }, 100)
|
|
24
24
|
* target.addEventListener('frequent-event', callback);
|
|
25
25
|
*
|
|
26
26
|
*/
|
package/dist/helpers/throttle.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*
|
|
22
22
|
* Example usage:
|
|
23
|
-
* const callback =
|
|
23
|
+
* const callback = throttle((ev) => { doSomethingExpensiveOrOccasional() }, 100)
|
|
24
24
|
* target.addEventListener('frequent-event', callback);
|
|
25
25
|
*
|
|
26
26
|
*/
|
|
@@ -16,6 +16,8 @@ import { NetworkAdapterInterface } from "./NetworkAdapterInterface.js";
|
|
|
16
16
|
export declare abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> implements NetworkAdapterInterface {
|
|
17
17
|
peerId?: PeerId;
|
|
18
18
|
peerMetadata?: PeerMetadata;
|
|
19
|
+
abstract isReady(): boolean;
|
|
20
|
+
abstract whenReady(): Promise<void>;
|
|
19
21
|
/** Called by the {@link Repo} to start the connection process
|
|
20
22
|
*
|
|
21
23
|
* @argument peerId - the peerId of this repo
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkAdapter.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;;;;;;GASG;AACH,8BAAsB,cACpB,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,uBAAuB;IAElC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI;IAEnE;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAErC,gEAAgE;IAChE,QAAQ,CAAC,UAAU,IAAI,IAAI;CAC5B"}
|
|
1
|
+
{"version":3,"file":"NetworkAdapter.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;;;;;;GASG;AACH,8BAAsB,cACpB,SAAQ,YAAY,CAAC,oBAAoB,CACzC,YAAW,uBAAuB;IAElC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B,QAAQ,CAAC,OAAO,IAAI,OAAO;IAC3B,QAAQ,CAAC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAEnC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI;IAEnE;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAErC,gEAAgE;IAChE,QAAQ,CAAC,UAAU,IAAI,IAAI;CAC5B"}
|
|
@@ -26,6 +26,8 @@ export interface PeerMetadata {
|
|
|
26
26
|
export interface NetworkAdapterInterface extends EventEmitter<NetworkAdapterEvents> {
|
|
27
27
|
peerId?: PeerId;
|
|
28
28
|
peerMetadata?: PeerMetadata;
|
|
29
|
+
isReady(): boolean;
|
|
30
|
+
whenReady(): Promise<void>;
|
|
29
31
|
/** Called by the {@link Repo} to start the connection process
|
|
30
32
|
*
|
|
31
33
|
* @argument peerId - the peerId of this repo
|
|
@@ -41,8 +43,6 @@ export interface NetworkAdapterInterface extends EventEmitter<NetworkAdapterEven
|
|
|
41
43
|
disconnect(): void;
|
|
42
44
|
}
|
|
43
45
|
export interface NetworkAdapterEvents {
|
|
44
|
-
/** Emitted when the network is ready to be used */
|
|
45
|
-
ready: (payload: OpenPayload) => void;
|
|
46
46
|
/** Emitted when the network is closed */
|
|
47
47
|
close: () => void;
|
|
48
48
|
/** Emitted when the network adapter learns about a new peer */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkAdapterInterface.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapterInterface.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBACf,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"NetworkAdapterInterface.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapterInterface.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBACf,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B,OAAO,IAAI,OAAO,CAAA;IAClB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1B;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;IAG1D;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAE5B,gEAAgE;IAChE,UAAU,IAAI,IAAI,CAAA;CACnB;AAID,MAAM,WAAW,oBAAoB;IACnC,yCAAyC;IACzC,KAAK,EAAE,MAAM,IAAI,CAAA;IAEjB,+DAA+D;IAC/D,gBAAgB,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAEzD,2EAA2E;IAC3E,mBAAmB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAE/D,sEAAsE;IACtE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,uBAAuB,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAA;CACf"}
|
|
@@ -6,17 +6,20 @@ export declare class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvent
|
|
|
6
6
|
#private;
|
|
7
7
|
peerId: PeerId;
|
|
8
8
|
private peerMetadata;
|
|
9
|
+
adapters: NetworkAdapterInterface[];
|
|
9
10
|
constructor(adapters: NetworkAdapterInterface[], peerId: PeerId, peerMetadata: Promise<PeerMetadata>);
|
|
11
|
+
disconnect(): void;
|
|
12
|
+
reconnect(): void;
|
|
10
13
|
addNetworkAdapter(networkAdapter: NetworkAdapterInterface): void;
|
|
14
|
+
removeNetworkAdapter(networkAdapter: NetworkAdapterInterface): void;
|
|
11
15
|
send(message: MessageContents): void;
|
|
12
16
|
isReady: () => boolean;
|
|
13
|
-
whenReady: () => Promise<void>;
|
|
17
|
+
whenReady: () => Promise<void[]>;
|
|
14
18
|
}
|
|
15
19
|
export interface NetworkSubsystemEvents {
|
|
16
20
|
peer: (payload: PeerPayload) => void;
|
|
17
21
|
"peer-disconnected": (payload: PeerDisconnectedPayload) => void;
|
|
18
22
|
message: (payload: RepoMessage) => void;
|
|
19
|
-
ready: () => void;
|
|
20
23
|
}
|
|
21
24
|
export interface PeerPayload {
|
|
22
25
|
peerId: PeerId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkSubsystem.d.ts","sourceRoot":"","sources":["../../src/network/NetworkSubsystem.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAa,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACb,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAEL,eAAe,EACf,WAAW,EAGZ,MAAM,eAAe,CAAA;AAOtB,qBAAa,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;;
|
|
1
|
+
{"version":3,"file":"NetworkSubsystem.d.ts","sourceRoot":"","sources":["../../src/network/NetworkSubsystem.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAa,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACb,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAEL,eAAe,EACf,WAAW,EAGZ,MAAM,eAAe,CAAA;AAOtB,qBAAa,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;;IAW/D,MAAM,EAAE,MAAM;IACrB,OAAO,CAAC,YAAY;IALtB,QAAQ,EAAE,uBAAuB,EAAE,CAAK;gBAGtC,QAAQ,EAAE,uBAAuB,EAAE,EAC5B,MAAM,EAAE,MAAM,EACb,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC;IAO7C,UAAU;IAIV,SAAS;IAIT,iBAAiB,CAAC,cAAc,EAAE,uBAAuB;IAqEzD,oBAAoB,CAAC,cAAc,EAAE,uBAAuB;IAK5D,IAAI,CAAC,OAAO,EAAE,eAAe;IAsC7B,OAAO,gBAEN;IAED,SAAS,wBAER;CACF;AAID,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;IACpC,mBAAmB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAC/D,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;CACxC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,YAAY,CAAA;CAC3B"}
|
|
@@ -10,27 +10,29 @@ export class NetworkSubsystem extends EventEmitter {
|
|
|
10
10
|
#count = 0;
|
|
11
11
|
#sessionId = Math.random().toString(36).slice(2);
|
|
12
12
|
#ephemeralSessionCounts = {};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
constructor(adapters, peerId = randomPeerId(), peerMetadata) {
|
|
13
|
+
adapters = [];
|
|
14
|
+
constructor(adapters, peerId, peerMetadata) {
|
|
16
15
|
super();
|
|
17
16
|
this.peerId = peerId;
|
|
18
17
|
this.peerMetadata = peerMetadata;
|
|
19
18
|
this.#log = debug(`automerge-repo:network:${this.peerId}`);
|
|
20
19
|
adapters.forEach(a => this.addNetworkAdapter(a));
|
|
21
20
|
}
|
|
21
|
+
disconnect() {
|
|
22
|
+
this.adapters.forEach(a => a.disconnect());
|
|
23
|
+
}
|
|
24
|
+
reconnect() {
|
|
25
|
+
this.adapters.forEach(a => a.connect(this.peerId));
|
|
26
|
+
}
|
|
22
27
|
addNetworkAdapter(networkAdapter) {
|
|
23
|
-
this
|
|
24
|
-
networkAdapter.once("ready", () => {
|
|
25
|
-
this.#readyAdapterCount++;
|
|
26
|
-
this.#log("Adapters ready: ", this.#readyAdapterCount, "/", this.#adapters.length);
|
|
27
|
-
if (this.#readyAdapterCount === this.#adapters.length) {
|
|
28
|
-
this.emit("ready");
|
|
29
|
-
}
|
|
30
|
-
});
|
|
28
|
+
this.adapters.push(networkAdapter);
|
|
31
29
|
networkAdapter.on("peer-candidate", ({ peerId, peerMetadata }) => {
|
|
32
30
|
this.#log(`peer candidate: ${peerId} `);
|
|
33
31
|
// TODO: This is where authentication would happen
|
|
32
|
+
// TODO: on reconnection, this would create problems!
|
|
33
|
+
// the server would see a reconnection as a late-arriving channel
|
|
34
|
+
// for an existing peer and decide to ignore it until the connection
|
|
35
|
+
// times out: turns out my ICE/SIP emulation laziness did not pay off here
|
|
34
36
|
if (!this.#adaptersByPeer[peerId]) {
|
|
35
37
|
// TODO: handle losing a server here
|
|
36
38
|
this.#adaptersByPeer[peerId] = networkAdapter;
|
|
@@ -75,6 +77,12 @@ export class NetworkSubsystem extends EventEmitter {
|
|
|
75
77
|
this.#log("error connecting to network", err);
|
|
76
78
|
});
|
|
77
79
|
}
|
|
80
|
+
// TODO: this probably introduces a race condition for the ready event
|
|
81
|
+
// but I plan to refactor that as part of this branch in another patch
|
|
82
|
+
removeNetworkAdapter(networkAdapter) {
|
|
83
|
+
this.adapters = this.adapters.filter(a => a !== networkAdapter);
|
|
84
|
+
networkAdapter.disconnect();
|
|
85
|
+
}
|
|
78
86
|
send(message) {
|
|
79
87
|
const peer = this.#adaptersByPeer[message.targetId];
|
|
80
88
|
if (!peer) {
|
|
@@ -113,21 +121,9 @@ export class NetworkSubsystem extends EventEmitter {
|
|
|
113
121
|
peer.send(outbound);
|
|
114
122
|
}
|
|
115
123
|
isReady = () => {
|
|
116
|
-
return this
|
|
124
|
+
return this.adapters.every(a => a.isReady());
|
|
117
125
|
};
|
|
118
126
|
whenReady = async () => {
|
|
119
|
-
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
return new Promise(resolve => {
|
|
124
|
-
this.once("ready", () => {
|
|
125
|
-
resolve();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
127
|
+
return Promise.all(this.adapters.map(a => a.whenReady()));
|
|
129
128
|
};
|
|
130
129
|
}
|
|
131
|
-
function randomPeerId() {
|
|
132
|
-
return `user-${Math.round(Math.random() * 100000)}`;
|
|
133
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.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>",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"vite": "^5.0.8"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@automerge/automerge": "^2.2.
|
|
26
|
+
"@automerge/automerge": "^2.2.7",
|
|
27
27
|
"bs58check": "^3.0.1",
|
|
28
28
|
"cbor-x": "^1.3.0",
|
|
29
29
|
"debug": "^4.3.4",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"publishConfig": {
|
|
61
61
|
"access": "public"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "86421aba21c8037b916ee93f962df1c121fef52b"
|
|
64
64
|
}
|
package/src/AutomergeUrl.ts
CHANGED
|
@@ -57,9 +57,8 @@ export const stringifyAutomergeUrl = (
|
|
|
57
57
|
* Given a string, returns true if it is a valid Automerge URL. This function also acts as a type
|
|
58
58
|
* discriminator in Typescript.
|
|
59
59
|
*/
|
|
60
|
-
export const isValidAutomergeUrl = (
|
|
61
|
-
str
|
|
62
|
-
): str is AutomergeUrl => {
|
|
60
|
+
export const isValidAutomergeUrl = (str: unknown): str is AutomergeUrl => {
|
|
61
|
+
if (typeof str !== "string") return false
|
|
63
62
|
if (!str || !str.startsWith(urlPrefix)) return false
|
|
64
63
|
const automergeUrl = str as AutomergeUrl
|
|
65
64
|
try {
|
|
@@ -70,7 +69,8 @@ export const isValidAutomergeUrl = (
|
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
export const isValidDocumentId = (str:
|
|
72
|
+
export const isValidDocumentId = (str: unknown): str is DocumentId => {
|
|
73
|
+
if (typeof str !== "string") return false
|
|
74
74
|
// try to decode from base58
|
|
75
75
|
const binaryDocumentID = documentIdToBinary(str as DocumentId)
|
|
76
76
|
if (binaryDocumentID === undefined) return false // invalid base58check encoding
|
|
@@ -80,8 +80,8 @@ export const isValidDocumentId = (str: string): str is DocumentId => {
|
|
|
80
80
|
return Uuid.validate(documentId)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export const isValidUuid = (str:
|
|
84
|
-
Uuid.validate(str)
|
|
83
|
+
export const isValidUuid = (str: unknown): str is LegacyDocumentId =>
|
|
84
|
+
typeof str === "string" && Uuid.validate(str)
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Returns a new Automerge URL with a random UUID documentId. Called by Repo.create(), and also used by tests.
|
package/src/DocHandle.ts
CHANGED
|
@@ -29,7 +29,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
29
29
|
#machine
|
|
30
30
|
|
|
31
31
|
/** The last known state of our document. */
|
|
32
|
-
#prevDocState: T
|
|
32
|
+
#prevDocState: T = A.init<T>()
|
|
33
33
|
|
|
34
34
|
/** How long to wait before giving up on a document. (Note that a document will be marked
|
|
35
35
|
* unavailable much sooner if all known peers respond that they don't have it.) */
|
|
@@ -49,17 +49,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
49
49
|
this.#timeoutDelay = options.timeoutDelay
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
const isNew = "isNew" in options && options.isNew
|
|
54
|
-
if (isNew) {
|
|
55
|
-
// T should really be constrained to extend `Record<string, unknown>` (an automerge doc can't be
|
|
56
|
-
// e.g. a primitive, an array, etc. - it must be an object). But adding that constraint creates
|
|
57
|
-
// a bunch of other problems elsewhere so for now we'll just cast it here to make Automerge happy.
|
|
58
|
-
doc = A.from(options.initialValue as Record<string, unknown>) as T
|
|
59
|
-
doc = A.emptyChange<T>(doc)
|
|
60
|
-
} else {
|
|
61
|
-
doc = A.init<T>()
|
|
62
|
-
}
|
|
52
|
+
const doc = A.init<T>()
|
|
63
53
|
|
|
64
54
|
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
|
|
65
55
|
|
|
@@ -80,7 +70,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
80
70
|
}),
|
|
81
71
|
onDelete: assign(() => {
|
|
82
72
|
this.emit("delete", { handle: this })
|
|
83
|
-
return { doc:
|
|
73
|
+
return { doc: A.init() }
|
|
84
74
|
}),
|
|
85
75
|
onUnavailable: () => {
|
|
86
76
|
this.emit("unavailable", { handle: this })
|
|
@@ -101,21 +91,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
101
91
|
states: {
|
|
102
92
|
idle: {
|
|
103
93
|
on: {
|
|
104
|
-
|
|
105
|
-
FIND: "loading",
|
|
94
|
+
BEGIN: "loading",
|
|
106
95
|
},
|
|
107
96
|
},
|
|
108
97
|
loading: {
|
|
109
98
|
on: {
|
|
110
99
|
REQUEST: "requesting",
|
|
111
100
|
DOC_READY: "ready",
|
|
112
|
-
AWAIT_NETWORK: "awaitingNetwork",
|
|
113
101
|
},
|
|
114
102
|
after: { [delay]: "unavailable" },
|
|
115
103
|
},
|
|
116
|
-
awaitingNetwork: {
|
|
117
|
-
on: { NETWORK_READY: "requesting" },
|
|
118
|
-
},
|
|
119
104
|
requesting: {
|
|
120
105
|
on: {
|
|
121
106
|
DOC_UNAVAILABLE: "unavailable",
|
|
@@ -146,7 +131,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
146
131
|
|
|
147
132
|
// Start the machine, and send a create or find event to get things going
|
|
148
133
|
this.#machine.start()
|
|
149
|
-
this.#machine.send(
|
|
134
|
+
this.#machine.send({ type: BEGIN })
|
|
150
135
|
}
|
|
151
136
|
|
|
152
137
|
// PRIVATE
|
|
@@ -178,13 +163,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
178
163
|
* Called after state transitions. If the document has changed, emits a change event. If we just
|
|
179
164
|
* received the document for the first time, signal that our request has been completed.
|
|
180
165
|
*/
|
|
181
|
-
#checkForChanges(before: T
|
|
182
|
-
const
|
|
183
|
-
|
|
166
|
+
#checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
|
|
167
|
+
const beforeHeads = A.getHeads(before)
|
|
168
|
+
const afterHeads = A.getHeads(after)
|
|
169
|
+
const docChanged = !headsAreSame(afterHeads, beforeHeads)
|
|
184
170
|
if (docChanged) {
|
|
185
171
|
this.emit("heads-changed", { handle: this, doc: after })
|
|
186
172
|
|
|
187
|
-
const patches = A.diff(after,
|
|
173
|
+
const patches = A.diff(after, beforeHeads, afterHeads)
|
|
188
174
|
if (patches.length > 0) {
|
|
189
175
|
this.emit("change", {
|
|
190
176
|
handle: this,
|
|
@@ -306,14 +292,24 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
306
292
|
}
|
|
307
293
|
|
|
308
294
|
/**
|
|
309
|
-
* `update` is called
|
|
310
|
-
*
|
|
295
|
+
* `update` is called any time we have a new document state; could be
|
|
296
|
+
* from a local change, a remote change, or a new document from storage.
|
|
297
|
+
* Does not cause state changes.
|
|
311
298
|
* @hidden
|
|
312
299
|
*/
|
|
313
300
|
update(callback: (doc: A.Doc<T>) => A.Doc<T>) {
|
|
314
301
|
this.#machine.send({ type: UPDATE, payload: { callback } })
|
|
315
302
|
}
|
|
316
303
|
|
|
304
|
+
/**
|
|
305
|
+
* `doneLoading` is called by the repo after it decides it has all the changes
|
|
306
|
+
* it's going to get during setup. This might mean it was created locally,
|
|
307
|
+
* or that it was loaded from storage, or that it was received from a peer.
|
|
308
|
+
*/
|
|
309
|
+
doneLoading() {
|
|
310
|
+
this.#machine.send({ type: DOC_READY })
|
|
311
|
+
}
|
|
312
|
+
|
|
317
313
|
/**
|
|
318
314
|
* Called by the repo either when a doc handle changes or we receive new remote heads.
|
|
319
315
|
* @hidden
|
|
@@ -346,7 +342,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
346
342
|
change(callback: A.ChangeFn<T>, options: A.ChangeOptions<T> = {}) {
|
|
347
343
|
if (!this.isReady()) {
|
|
348
344
|
throw new Error(
|
|
349
|
-
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
345
|
+
`DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
350
346
|
)
|
|
351
347
|
}
|
|
352
348
|
this.#machine.send({
|
|
@@ -425,17 +421,6 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
425
421
|
if (this.#state === "loading") this.#machine.send({ type: REQUEST })
|
|
426
422
|
}
|
|
427
423
|
|
|
428
|
-
/** @hidden */
|
|
429
|
-
awaitNetwork() {
|
|
430
|
-
if (this.#state === "loading") this.#machine.send({ type: AWAIT_NETWORK })
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/** @hidden */
|
|
434
|
-
networkReady() {
|
|
435
|
-
if (this.#state === "awaitingNetwork")
|
|
436
|
-
this.#machine.send({ type: NETWORK_READY })
|
|
437
|
-
}
|
|
438
|
-
|
|
439
424
|
/** Called by the repo when the document is deleted. */
|
|
440
425
|
delete() {
|
|
441
426
|
this.#machine.send({ type: DELETE })
|
|
@@ -550,8 +535,6 @@ export const HandleState = {
|
|
|
550
535
|
IDLE: "idle",
|
|
551
536
|
/** We are waiting for storage to finish loading */
|
|
552
537
|
LOADING: "loading",
|
|
553
|
-
/** We are waiting for the network to be come ready */
|
|
554
|
-
AWAITING_NETWORK: "awaitingNetwork",
|
|
555
538
|
/** We are waiting for someone in the network to respond to a sync request */
|
|
556
539
|
REQUESTING: "requesting",
|
|
557
540
|
/** The document is available */
|
|
@@ -563,15 +546,8 @@ export const HandleState = {
|
|
|
563
546
|
} as const
|
|
564
547
|
export type HandleState = (typeof HandleState)[keyof typeof HandleState]
|
|
565
548
|
|
|
566
|
-
export const {
|
|
567
|
-
|
|
568
|
-
LOADING,
|
|
569
|
-
AWAITING_NETWORK,
|
|
570
|
-
REQUESTING,
|
|
571
|
-
READY,
|
|
572
|
-
DELETED,
|
|
573
|
-
UNAVAILABLE,
|
|
574
|
-
} = HandleState
|
|
549
|
+
export const { IDLE, LOADING, REQUESTING, READY, DELETED, UNAVAILABLE } =
|
|
550
|
+
HandleState
|
|
575
551
|
|
|
576
552
|
// context
|
|
577
553
|
|
|
@@ -584,8 +560,7 @@ interface DocHandleContext<T> {
|
|
|
584
560
|
|
|
585
561
|
/** These are the (internal) events that can be sent to the state machine */
|
|
586
562
|
type DocHandleEvent<T> =
|
|
587
|
-
| { type: typeof
|
|
588
|
-
| { type: typeof FIND }
|
|
563
|
+
| { type: typeof BEGIN }
|
|
589
564
|
| { type: typeof REQUEST }
|
|
590
565
|
| { type: typeof DOC_READY }
|
|
591
566
|
| {
|
|
@@ -595,15 +570,10 @@ type DocHandleEvent<T> =
|
|
|
595
570
|
| { type: typeof TIMEOUT }
|
|
596
571
|
| { type: typeof DELETE }
|
|
597
572
|
| { type: typeof DOC_UNAVAILABLE }
|
|
598
|
-
| { type: typeof AWAIT_NETWORK }
|
|
599
|
-
| { type: typeof NETWORK_READY }
|
|
600
573
|
|
|
601
|
-
const
|
|
602
|
-
const FIND = "FIND"
|
|
574
|
+
const BEGIN = "BEGIN"
|
|
603
575
|
const REQUEST = "REQUEST"
|
|
604
576
|
const DOC_READY = "DOC_READY"
|
|
605
|
-
const AWAIT_NETWORK = "AWAIT_NETWORK"
|
|
606
|
-
const NETWORK_READY = "NETWORK_READY"
|
|
607
577
|
const UPDATE = "UPDATE"
|
|
608
578
|
const DELETE = "DELETE"
|
|
609
579
|
const TIMEOUT = "TIMEOUT"
|