@automerge/automerge-repo 1.2.0 → 2.0.0-alpha.0
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/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/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
|
@@ -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.0",
|
|
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": "16356392fb2ed9245565ae04f6e6b49e61195e65"
|
|
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"
|
package/src/Repo.ts
CHANGED
|
@@ -23,6 +23,10 @@ import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js
|
|
|
23
23
|
import { SyncStatePayload } from "./synchronizer/Synchronizer.js"
|
|
24
24
|
import type { AnyDocumentId, DocumentId, PeerId } from "./types.js"
|
|
25
25
|
|
|
26
|
+
function randomPeerId() {
|
|
27
|
+
return ("peer-" + Math.random().toString(36).slice(4)) as PeerId
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
/** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
|
|
27
31
|
/** The `Repo` is the main entry point of this library
|
|
28
32
|
*
|
|
@@ -61,7 +65,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
61
65
|
constructor({
|
|
62
66
|
storage,
|
|
63
67
|
network = [],
|
|
64
|
-
peerId,
|
|
68
|
+
peerId = randomPeerId(),
|
|
65
69
|
sharePolicy,
|
|
66
70
|
isEphemeral = storage === undefined,
|
|
67
71
|
enableRemoteHeadsGossiping = false,
|
|
@@ -75,7 +79,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
75
79
|
|
|
76
80
|
// The `document` event is fired by the DocCollection any time we create a new document or look
|
|
77
81
|
// up a document by ID. We listen for it in order to wire up storage and network synchronization.
|
|
78
|
-
this.on("document", async ({ handle
|
|
82
|
+
this.on("document", async ({ handle }) => {
|
|
79
83
|
if (storageSubsystem) {
|
|
80
84
|
// Save when the document changes, but no more often than saveDebounceRate.
|
|
81
85
|
const saveFn = ({
|
|
@@ -85,17 +89,6 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
85
89
|
void storageSubsystem.saveDoc(handle.documentId, doc)
|
|
86
90
|
}
|
|
87
91
|
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
|
|
88
|
-
|
|
89
|
-
if (isNew) {
|
|
90
|
-
// this is a new document, immediately save it
|
|
91
|
-
await storageSubsystem.saveDoc(handle.documentId, handle.docSync()!)
|
|
92
|
-
} else {
|
|
93
|
-
// Try to load from disk
|
|
94
|
-
const loadedDoc = await storageSubsystem.loadDoc(handle.documentId)
|
|
95
|
-
if (loadedDoc) {
|
|
96
|
-
handle.update(() => loadedDoc)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
92
|
}
|
|
100
93
|
|
|
101
94
|
handle.on("unavailable", () => {
|
|
@@ -105,20 +98,6 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
105
98
|
})
|
|
106
99
|
})
|
|
107
100
|
|
|
108
|
-
if (this.networkSubsystem.isReady()) {
|
|
109
|
-
handle.request()
|
|
110
|
-
} else {
|
|
111
|
-
handle.awaitNetwork()
|
|
112
|
-
this.networkSubsystem
|
|
113
|
-
.whenReady()
|
|
114
|
-
.then(() => {
|
|
115
|
-
handle.networkReady()
|
|
116
|
-
})
|
|
117
|
-
.catch(err => {
|
|
118
|
-
this.#log("error waiting for network", { err })
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
101
|
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
123
102
|
this.#synchronizer.addDocument(handle.documentId)
|
|
124
103
|
})
|
|
@@ -324,20 +303,16 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
324
303
|
/** Returns an existing handle if we have it; creates one otherwise. */
|
|
325
304
|
#getHandle<T>({
|
|
326
305
|
documentId,
|
|
327
|
-
isNew,
|
|
328
|
-
initialValue,
|
|
329
306
|
}: {
|
|
330
307
|
/** The documentId of the handle to look up or create */
|
|
331
308
|
documentId: DocumentId /** If we know we're creating a new document, specify this so we can have access to it immediately */
|
|
332
|
-
isNew: boolean
|
|
333
|
-
initialValue?: T
|
|
334
309
|
}) {
|
|
335
310
|
// If we have the handle cached, return it
|
|
336
311
|
if (this.#handleCache[documentId]) return this.#handleCache[documentId]
|
|
337
312
|
|
|
338
313
|
// If not, create a new handle, cache it, and return it
|
|
339
314
|
if (!documentId) throw new Error(`Invalid documentId ${documentId}`)
|
|
340
|
-
const handle = new DocHandle<T>(documentId
|
|
315
|
+
const handle = new DocHandle<T>(documentId)
|
|
341
316
|
this.#handleCache[documentId] = handle
|
|
342
317
|
return handle
|
|
343
318
|
}
|
|
@@ -366,10 +341,21 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
366
341
|
const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
|
|
367
342
|
const handle = this.#getHandle<T>({
|
|
368
343
|
documentId,
|
|
369
|
-
isNew: true,
|
|
370
|
-
initialValue,
|
|
371
344
|
}) as DocHandle<T>
|
|
372
|
-
|
|
345
|
+
|
|
346
|
+
this.emit("document", { handle })
|
|
347
|
+
|
|
348
|
+
handle.update(() => {
|
|
349
|
+
let nextDoc: Automerge.Doc<T>
|
|
350
|
+
if (initialValue) {
|
|
351
|
+
nextDoc = Automerge.from(initialValue)
|
|
352
|
+
} else {
|
|
353
|
+
nextDoc = Automerge.emptyChange(Automerge.init())
|
|
354
|
+
}
|
|
355
|
+
return nextDoc
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
handle.doneLoading()
|
|
373
359
|
return handle
|
|
374
360
|
}
|
|
375
361
|
|
|
@@ -434,11 +420,34 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
434
420
|
return this.#handleCache[documentId]
|
|
435
421
|
}
|
|
436
422
|
|
|
423
|
+
// If we don't already have the handle, make an empty one and try loading it
|
|
437
424
|
const handle = this.#getHandle<T>({
|
|
438
425
|
documentId,
|
|
439
|
-
isNew: false,
|
|
440
426
|
}) as DocHandle<T>
|
|
441
|
-
|
|
427
|
+
|
|
428
|
+
// Try to load from disk before telling anyone else about it
|
|
429
|
+
if (this.storageSubsystem) {
|
|
430
|
+
void this.storageSubsystem.loadDoc(handle.documentId).then(loadedDoc => {
|
|
431
|
+
if (loadedDoc) {
|
|
432
|
+
// uhhhh, sorry if you're reading this because we were lying to the type system
|
|
433
|
+
handle.update(() => loadedDoc as Automerge.Doc<T>)
|
|
434
|
+
handle.doneLoading()
|
|
435
|
+
} else {
|
|
436
|
+
this.networkSubsystem
|
|
437
|
+
.whenReady()
|
|
438
|
+
.then(() => {
|
|
439
|
+
handle.request()
|
|
440
|
+
})
|
|
441
|
+
.catch(err => {
|
|
442
|
+
this.#log("error waiting for network", { err })
|
|
443
|
+
})
|
|
444
|
+
this.emit("document", { handle })
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
} else {
|
|
448
|
+
handle.request()
|
|
449
|
+
this.emit("document", { handle })
|
|
450
|
+
}
|
|
442
451
|
return handle
|
|
443
452
|
}
|
|
444
453
|
|
|
@@ -448,7 +457,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
448
457
|
) {
|
|
449
458
|
const documentId = interpretAsDocumentId(id)
|
|
450
459
|
|
|
451
|
-
const handle = this.#getHandle({ documentId
|
|
460
|
+
const handle = this.#getHandle({ documentId })
|
|
452
461
|
handle.delete()
|
|
453
462
|
|
|
454
463
|
delete this.#handleCache[documentId]
|
|
@@ -465,7 +474,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
465
474
|
async export(id: AnyDocumentId): Promise<Uint8Array | undefined> {
|
|
466
475
|
const documentId = interpretAsDocumentId(id)
|
|
467
476
|
|
|
468
|
-
const handle = this.#getHandle({ documentId
|
|
477
|
+
const handle = this.#getHandle({ documentId })
|
|
469
478
|
const doc = await handle.doc()
|
|
470
479
|
if (!doc) return undefined
|
|
471
480
|
return Automerge.save(doc)
|
|
@@ -529,6 +538,13 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
529
538
|
})
|
|
530
539
|
)
|
|
531
540
|
}
|
|
541
|
+
|
|
542
|
+
shutdown(): Promise<void> {
|
|
543
|
+
this.networkSubsystem.adapters.forEach(adapter => {
|
|
544
|
+
adapter.disconnect()
|
|
545
|
+
})
|
|
546
|
+
return this.flush()
|
|
547
|
+
}
|
|
532
548
|
}
|
|
533
549
|
|
|
534
550
|
export interface RepoConfig {
|
|
@@ -582,7 +598,6 @@ export interface RepoEvents {
|
|
|
582
598
|
|
|
583
599
|
export interface DocumentPayload {
|
|
584
600
|
handle: DocHandle<any>
|
|
585
|
-
isNew: boolean
|
|
586
601
|
}
|
|
587
602
|
|
|
588
603
|
export interface DeleteDocumentPayload {
|
|
@@ -2,20 +2,44 @@ import { pause } from "../../src/helpers/pause.js"
|
|
|
2
2
|
import { Message, NetworkAdapter, PeerId } from "../../src/index.js"
|
|
3
3
|
|
|
4
4
|
export class DummyNetworkAdapter extends NetworkAdapter {
|
|
5
|
-
#startReady: boolean
|
|
6
5
|
#sendMessage?: SendMessageFn
|
|
7
6
|
|
|
7
|
+
#ready = false
|
|
8
|
+
#readyResolver?: () => void
|
|
9
|
+
#readyPromise: Promise<void> = new Promise<void>(resolve => {
|
|
10
|
+
this.#readyResolver = resolve
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
isReady() {
|
|
14
|
+
return this.#ready
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
whenReady() {
|
|
18
|
+
return this.#readyPromise
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#forceReady() {
|
|
22
|
+
if (!this.#ready) {
|
|
23
|
+
this.#ready = true
|
|
24
|
+
this.#readyResolver?.()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A public wrapper for use in tests!
|
|
29
|
+
forceReady() {
|
|
30
|
+
this.#forceReady()
|
|
31
|
+
}
|
|
32
|
+
|
|
8
33
|
constructor(opts: Options = { startReady: true }) {
|
|
9
34
|
super()
|
|
10
|
-
|
|
35
|
+
if (opts.startReady) {
|
|
36
|
+
this.#forceReady()
|
|
37
|
+
}
|
|
11
38
|
this.#sendMessage = opts.sendMessage
|
|
12
39
|
}
|
|
13
40
|
|
|
14
41
|
connect(peerId: PeerId) {
|
|
15
42
|
this.peerId = peerId
|
|
16
|
-
if (this.#startReady) {
|
|
17
|
-
this.emit("ready", { network: this })
|
|
18
|
-
}
|
|
19
43
|
}
|
|
20
44
|
|
|
21
45
|
disconnect() {}
|