@automerge/automerge-repo 2.0.0-alpha.2 → 2.0.0-alpha.20
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 +17 -5
- package/dist/AutomergeUrl.d.ts.map +1 -1
- package/dist/AutomergeUrl.js +71 -24
- package/dist/DocHandle.d.ts +80 -8
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +181 -10
- package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
- package/dist/RemoteHeadsSubscriptions.js +4 -1
- package/dist/Repo.d.ts +35 -2
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +112 -70
- package/dist/entrypoints/fullfat.d.ts +1 -0
- package/dist/entrypoints/fullfat.d.ts.map +1 -1
- package/dist/entrypoints/fullfat.js +1 -2
- package/dist/helpers/bufferFromHex.d.ts +3 -0
- package/dist/helpers/bufferFromHex.d.ts.map +1 -0
- package/dist/helpers/bufferFromHex.js +13 -0
- package/dist/helpers/headsAreSame.d.ts +2 -2
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/mergeArrays.d.ts +1 -1
- package/dist/helpers/mergeArrays.d.ts.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.js +25 -48
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/storage/StorageSubsystem.d.ts +11 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +20 -4
- package/dist/synchronizer/CollectionSynchronizer.d.ts +15 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +29 -8
- package/dist/synchronizer/DocSynchronizer.d.ts +7 -0
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +14 -0
- package/dist/synchronizer/Synchronizer.d.ts +11 -0
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/AutomergeUrl.ts +101 -26
- package/src/DocHandle.ts +245 -20
- package/src/RemoteHeadsSubscriptions.ts +11 -9
- package/src/Repo.ts +163 -68
- package/src/entrypoints/fullfat.ts +1 -2
- package/src/helpers/bufferFromHex.ts +14 -0
- package/src/helpers/headsAreSame.ts +2 -2
- package/src/helpers/tests/storage-adapter-tests.ts +44 -86
- package/src/index.ts +2 -0
- package/src/storage/StorageSubsystem.ts +29 -4
- package/src/synchronizer/CollectionSynchronizer.ts +42 -9
- package/src/synchronizer/DocSynchronizer.ts +15 -0
- package/src/synchronizer/Synchronizer.ts +14 -0
- package/src/types.ts +4 -1
- package/test/AutomergeUrl.test.ts +130 -0
- package/test/DocHandle.test.ts +209 -2
- package/test/DocSynchronizer.test.ts +10 -3
- package/test/Repo.test.ts +228 -3
- package/test/StorageSubsystem.test.ts +17 -0
|
@@ -5,11 +5,13 @@ import { mergeArrays } from "../helpers/mergeArrays.js";
|
|
|
5
5
|
import { keyHash, headsHash } from "./keyHash.js";
|
|
6
6
|
import { chunkTypeFromKey } from "./chunkTypeFromKey.js";
|
|
7
7
|
import * as Uuid from "uuid";
|
|
8
|
+
import { EventEmitter } from "eventemitter3";
|
|
9
|
+
import { encodeHeads } from "../AutomergeUrl.js";
|
|
8
10
|
/**
|
|
9
11
|
* The storage subsystem is responsible for saving and loading Automerge documents to and from
|
|
10
12
|
* storage adapter. It also provides a generic key/value storage interface for other uses.
|
|
11
13
|
*/
|
|
12
|
-
export class StorageSubsystem {
|
|
14
|
+
export class StorageSubsystem extends EventEmitter {
|
|
13
15
|
/** The storage adapter to use for saving and loading documents */
|
|
14
16
|
#storageAdapter;
|
|
15
17
|
/** Record of the latest heads we've loaded or saved for each document */
|
|
@@ -20,6 +22,7 @@ export class StorageSubsystem {
|
|
|
20
22
|
#compacting = false;
|
|
21
23
|
#log = debug(`automerge-repo:storage-subsystem`);
|
|
22
24
|
constructor(storageAdapter) {
|
|
25
|
+
super();
|
|
23
26
|
this.#storageAdapter = storageAdapter;
|
|
24
27
|
}
|
|
25
28
|
async id() {
|
|
@@ -100,7 +103,14 @@ export class StorageSubsystem {
|
|
|
100
103
|
if (binary.length === 0)
|
|
101
104
|
return null;
|
|
102
105
|
// Load into an Automerge document
|
|
106
|
+
const start = performance.now();
|
|
103
107
|
const newDoc = A.loadIncremental(A.init(), binary);
|
|
108
|
+
const end = performance.now();
|
|
109
|
+
this.emit("document-loaded", {
|
|
110
|
+
documentId,
|
|
111
|
+
durationMillis: end - start,
|
|
112
|
+
...A.stats(newDoc),
|
|
113
|
+
});
|
|
104
114
|
// Record the latest heads for the document
|
|
105
115
|
this.#storedHeads.set(documentId, A.getHeads(newDoc));
|
|
106
116
|
return newDoc;
|
|
@@ -178,8 +188,14 @@ export class StorageSubsystem {
|
|
|
178
188
|
}
|
|
179
189
|
async loadSyncState(documentId, storageId) {
|
|
180
190
|
const key = [documentId, "sync-state", storageId];
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
try {
|
|
192
|
+
const loaded = await this.#storageAdapter.load(key);
|
|
193
|
+
return loaded ? A.decodeSyncState(loaded) : undefined;
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
this.#log(`Error loading sync state for ${documentId} from ${storageId}`);
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
183
199
|
}
|
|
184
200
|
async saveSyncState(documentId, storageId, syncState) {
|
|
185
201
|
const key = [documentId, "sync-state", storageId];
|
|
@@ -195,7 +211,7 @@ export class StorageSubsystem {
|
|
|
195
211
|
return true;
|
|
196
212
|
}
|
|
197
213
|
const newHeads = A.getHeads(doc);
|
|
198
|
-
if (headsAreSame(newHeads, oldHeads)) {
|
|
214
|
+
if (headsAreSame(encodeHeads(newHeads), encodeHeads(oldHeads))) {
|
|
199
215
|
// the document hasn't changed
|
|
200
216
|
return false;
|
|
201
217
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Repo } from "../Repo.js";
|
|
2
2
|
import { DocMessage } from "../network/messages.js";
|
|
3
|
-
import { DocumentId, PeerId } from "../types.js";
|
|
3
|
+
import { AutomergeUrl, DocumentId, PeerId } from "../types.js";
|
|
4
|
+
import { DocSynchronizer } from "./DocSynchronizer.js";
|
|
4
5
|
import { Synchronizer } from "./Synchronizer.js";
|
|
5
6
|
/** A CollectionSynchronizer is responsible for synchronizing a DocCollection with peers. */
|
|
6
7
|
export declare class CollectionSynchronizer extends Synchronizer {
|
|
7
8
|
#private;
|
|
8
9
|
private repo;
|
|
9
|
-
|
|
10
|
+
/** A map of documentIds to their synchronizers */
|
|
11
|
+
/** @hidden */
|
|
12
|
+
docSynchronizers: Record<DocumentId, DocSynchronizer>;
|
|
13
|
+
constructor(repo: Repo, denylist?: AutomergeUrl[]);
|
|
10
14
|
/**
|
|
11
15
|
* When we receive a sync message for a document we haven't got in memory, we
|
|
12
16
|
* register it with the repo and start synchronizing
|
|
@@ -23,5 +27,14 @@ export declare class CollectionSynchronizer extends Synchronizer {
|
|
|
23
27
|
removePeer(peerId: PeerId): void;
|
|
24
28
|
/** Returns a list of all connected peer ids */
|
|
25
29
|
get peers(): PeerId[];
|
|
30
|
+
metrics(): {
|
|
31
|
+
[key: string]: {
|
|
32
|
+
peers: PeerId[];
|
|
33
|
+
size: {
|
|
34
|
+
numOps: number;
|
|
35
|
+
numChanges: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
26
39
|
}
|
|
27
40
|
//# sourceMappingURL=CollectionSynchronizer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAa1C,OAAO,CAAC,IAAI;IATxB,kDAAkD;IAClD,cAAc;IACd,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAK;gBAOtC,IAAI,EAAE,IAAI,EAAE,QAAQ,GAAE,YAAY,EAAO;IAuD7D;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU;IAsCxC;;OAEG;IACH,WAAW,CAAC,UAAU,EAAE,UAAU;IAalC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAgBtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;IASzB,+CAA+C;IAC/C,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED,OAAO,IAAI;QACT,CAAC,GAAG,EAAE,MAAM,GAAG;YACb,KAAK,EAAE,MAAM,EAAE,CAAA;YACf,IAAI,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,UAAU,EAAE,MAAM,CAAA;aAAE,CAAA;SAC7C,CAAA;KACF;CASF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import debug from "debug";
|
|
2
|
-
import { stringifyAutomergeUrl } from "../AutomergeUrl.js";
|
|
2
|
+
import { parseAutomergeUrl, stringifyAutomergeUrl } from "../AutomergeUrl.js";
|
|
3
3
|
import { DocSynchronizer } from "./DocSynchronizer.js";
|
|
4
4
|
import { Synchronizer } from "./Synchronizer.js";
|
|
5
5
|
const log = debug("automerge-repo:collectionsync");
|
|
@@ -9,20 +9,23 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
9
9
|
/** The set of peers we are connected with */
|
|
10
10
|
#peers = new Set();
|
|
11
11
|
/** A map of documentIds to their synchronizers */
|
|
12
|
-
|
|
12
|
+
/** @hidden */
|
|
13
|
+
docSynchronizers = {};
|
|
13
14
|
/** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
|
|
14
15
|
#docSetUp = {};
|
|
15
|
-
|
|
16
|
+
#denylist;
|
|
17
|
+
constructor(repo, denylist = []) {
|
|
16
18
|
super();
|
|
17
19
|
this.repo = repo;
|
|
20
|
+
this.#denylist = denylist.map(url => parseAutomergeUrl(url).documentId);
|
|
18
21
|
}
|
|
19
22
|
/** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
|
|
20
23
|
#fetchDocSynchronizer(documentId) {
|
|
21
|
-
if (!this
|
|
24
|
+
if (!this.docSynchronizers[documentId]) {
|
|
22
25
|
const handle = this.repo.find(stringifyAutomergeUrl({ documentId }));
|
|
23
|
-
this
|
|
26
|
+
this.docSynchronizers[documentId] = this.#initDocSynchronizer(handle);
|
|
24
27
|
}
|
|
25
|
-
return this
|
|
28
|
+
return this.docSynchronizers[documentId];
|
|
26
29
|
}
|
|
27
30
|
/** Creates a new docSynchronizer and sets it up to propagate messages */
|
|
28
31
|
#initDocSynchronizer(handle) {
|
|
@@ -42,6 +45,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
42
45
|
docSynchronizer.on("message", event => this.emit("message", event));
|
|
43
46
|
docSynchronizer.on("open-doc", event => this.emit("open-doc", event));
|
|
44
47
|
docSynchronizer.on("sync-state", event => this.emit("sync-state", event));
|
|
48
|
+
docSynchronizer.on("metrics", event => this.emit("metrics", event));
|
|
45
49
|
return docSynchronizer;
|
|
46
50
|
}
|
|
47
51
|
/** returns an array of peerIds that we share this document generously with */
|
|
@@ -66,6 +70,18 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
66
70
|
if (!documentId) {
|
|
67
71
|
throw new Error("received a message with an invalid documentId");
|
|
68
72
|
}
|
|
73
|
+
if (this.#denylist.includes(documentId)) {
|
|
74
|
+
this.emit("metrics", {
|
|
75
|
+
type: "doc-denied",
|
|
76
|
+
documentId,
|
|
77
|
+
});
|
|
78
|
+
this.emit("message", {
|
|
79
|
+
type: "doc-unavailable",
|
|
80
|
+
documentId,
|
|
81
|
+
targetId: message.senderId,
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
69
85
|
this.#docSetUp[documentId] = true;
|
|
70
86
|
const docSynchronizer = this.#fetchDocSynchronizer(documentId);
|
|
71
87
|
docSynchronizer.receiveMessage(message);
|
|
@@ -98,7 +114,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
98
114
|
return;
|
|
99
115
|
}
|
|
100
116
|
this.#peers.add(peerId);
|
|
101
|
-
for (const docSynchronizer of Object.values(this
|
|
117
|
+
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
102
118
|
const { documentId } = docSynchronizer;
|
|
103
119
|
void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
104
120
|
if (okToShare)
|
|
@@ -110,7 +126,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
110
126
|
removePeer(peerId) {
|
|
111
127
|
log(`removing peer ${peerId}`);
|
|
112
128
|
this.#peers.delete(peerId);
|
|
113
|
-
for (const docSynchronizer of Object.values(this
|
|
129
|
+
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
114
130
|
docSynchronizer.endSync(peerId);
|
|
115
131
|
}
|
|
116
132
|
}
|
|
@@ -118,4 +134,9 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
118
134
|
get peers() {
|
|
119
135
|
return Array.from(this.#peers);
|
|
120
136
|
}
|
|
137
|
+
metrics() {
|
|
138
|
+
return Object.fromEntries(Object.entries(this.docSynchronizers).map(([documentId, synchronizer]) => {
|
|
139
|
+
return [documentId, synchronizer.metrics()];
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
121
142
|
}
|
|
@@ -24,6 +24,13 @@ export declare class DocSynchronizer extends Synchronizer {
|
|
|
24
24
|
receiveMessage(message: RepoMessage): void;
|
|
25
25
|
receiveEphemeralMessage(message: EphemeralMessage): void;
|
|
26
26
|
receiveSyncMessage(message: SyncMessage | RequestMessage): void;
|
|
27
|
+
metrics(): {
|
|
28
|
+
peers: PeerId[];
|
|
29
|
+
size: {
|
|
30
|
+
numOps: number;
|
|
31
|
+
numChanges: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
27
34
|
}
|
|
28
35
|
export {};
|
|
29
36
|
//# sourceMappingURL=DocSynchronizer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,gCAAgC,CAAA;AAGnD,OAAO,EACL,SAAS,EAKV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEL,gBAAgB,EAEhB,WAAW,EACX,cAAc,EACd,WAAW,EAEZ,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,KAAK,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,CAAA;AAOrE,UAAU,qBAAqB;IAC7B,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC,CAAA;CACvE;AAED;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,YAAY;;IAE/C,gBAAgB,SAAM;gBAsBV,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,qBAAqB;IAyB9D,IAAI,UAAU,uCAEb;IAED,IAAI,UAAU,qCAEb;IAkID,OAAO,CAAC,MAAM,EAAE,MAAM;IAItB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE;IAmD3B,OAAO,CAAC,MAAM,EAAE,MAAM;IAKtB,cAAc,CAAC,OAAO,EAAE,WAAW;IAkBnC,uBAAuB,CAAC,OAAO,EAAE,gBAAgB;IAuBjD,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc;
|
|
1
|
+
{"version":3,"file":"DocSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/DocSynchronizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,gCAAgC,CAAA;AAGnD,OAAO,EACL,SAAS,EAKV,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEL,gBAAgB,EAEhB,WAAW,EACX,cAAc,EACd,WAAW,EAEZ,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGhD,KAAK,kBAAkB,GAAG,SAAS,GAAG,KAAK,GAAG,aAAa,GAAG,OAAO,CAAA;AAOrE,UAAU,qBAAqB;IAC7B,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC1B,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC,CAAA;CACvE;AAED;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,YAAY;;IAE/C,gBAAgB,SAAM;gBAsBV,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,qBAAqB;IAyB9D,IAAI,UAAU,uCAEb;IAED,IAAI,UAAU,qCAEb;IAkID,OAAO,CAAC,MAAM,EAAE,MAAM;IAItB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE;IAmD3B,OAAO,CAAC,MAAM,EAAE,MAAM;IAKtB,cAAc,CAAC,OAAO,EAAE,WAAW;IAkBnC,uBAAuB,CAAC,OAAO,EAAE,gBAAgB;IAuBjD,kBAAkB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc;IAuFxD,OAAO,IAAI;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE;CAM7E"}
|
|
@@ -252,7 +252,15 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
252
252
|
}
|
|
253
253
|
this.#withSyncState(message.senderId, syncState => {
|
|
254
254
|
this.#handle.update(doc => {
|
|
255
|
+
const start = performance.now();
|
|
255
256
|
const [newDoc, newSyncState] = A.receiveSyncMessage(doc, syncState, message.data);
|
|
257
|
+
const end = performance.now();
|
|
258
|
+
this.emit("metrics", {
|
|
259
|
+
type: "receive-sync-message",
|
|
260
|
+
documentId: this.#handle.documentId,
|
|
261
|
+
durationMillis: end - start,
|
|
262
|
+
...A.stats(doc),
|
|
263
|
+
});
|
|
256
264
|
this.#setSyncState(message.senderId, newSyncState);
|
|
257
265
|
// respond to just this peer (as required)
|
|
258
266
|
this.#sendSyncMessage(message.senderId, doc);
|
|
@@ -286,4 +294,10 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
286
294
|
}
|
|
287
295
|
this.#pendingSyncMessages = [];
|
|
288
296
|
}
|
|
297
|
+
metrics() {
|
|
298
|
+
return {
|
|
299
|
+
peers: this.#peers,
|
|
300
|
+
size: this.#handle.metrics(),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
289
303
|
}
|
|
@@ -9,6 +9,7 @@ export interface SynchronizerEvents {
|
|
|
9
9
|
message: (payload: MessageContents) => void;
|
|
10
10
|
"sync-state": (payload: SyncStatePayload) => void;
|
|
11
11
|
"open-doc": (arg: OpenDocMessage) => void;
|
|
12
|
+
metrics: (arg: DocSyncMetrics) => void;
|
|
12
13
|
}
|
|
13
14
|
/** Notify the repo that the sync state has changed */
|
|
14
15
|
export interface SyncStatePayload {
|
|
@@ -16,4 +17,14 @@ export interface SyncStatePayload {
|
|
|
16
17
|
documentId: DocumentId;
|
|
17
18
|
syncState: SyncState;
|
|
18
19
|
}
|
|
20
|
+
export type DocSyncMetrics = {
|
|
21
|
+
type: "receive-sync-message";
|
|
22
|
+
documentId: DocumentId;
|
|
23
|
+
durationMillis: number;
|
|
24
|
+
numOps: number;
|
|
25
|
+
numChanges: number;
|
|
26
|
+
} | {
|
|
27
|
+
type: "doc-denied";
|
|
28
|
+
documentId: DocumentId;
|
|
29
|
+
};
|
|
19
30
|
//# sourceMappingURL=Synchronizer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
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,EACL,eAAe,EACf,cAAc,EACd,WAAW,EACZ,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEhD,8BAAsB,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;CACpD;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;IAC3C,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACjD,UAAU,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,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,EACL,eAAe,EACf,cAAc,EACd,WAAW,EACZ,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AACrD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEhD,8BAAsB,YAAa,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IACzE,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;CACpD;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;IAC3C,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACjD,UAAU,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAA;CACvC;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;CACrB;AAED,MAAM,MAAM,cAAc,GACtB;IACE,IAAI,EAAE,sBAAsB,CAAA;IAC5B,UAAU,EAAE,UAAU,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB,GACD;IACE,IAAI,EAAE,YAAY,CAAA;IAClB,UAAU,EAAE,UAAU,CAAA;CACvB,CAAA"}
|
package/dist/types.d.ts
CHANGED
|
@@ -26,12 +26,15 @@ export type LegacyDocumentId = string & {
|
|
|
26
26
|
__legacyDocumentId: true;
|
|
27
27
|
};
|
|
28
28
|
export type AnyDocumentId = AutomergeUrl | DocumentId | BinaryDocumentId | LegacyDocumentId;
|
|
29
|
+
export type UrlHeads = string[] & {
|
|
30
|
+
__automergeUrlHeads: unknown;
|
|
31
|
+
};
|
|
29
32
|
/** A branded type for peer IDs */
|
|
30
33
|
export type PeerId = string & {
|
|
31
34
|
__peerId: true;
|
|
32
35
|
};
|
|
33
36
|
/** A randomly generated string created when the {@link Repo} starts up */
|
|
34
37
|
export type SessionId = string & {
|
|
35
|
-
|
|
38
|
+
__sessionId: true;
|
|
36
39
|
};
|
|
37
40
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AAExD,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAEpE,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AAExD,iGAAiG;AACjG,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAExE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG;IAAE,kBAAkB,EAAE,IAAI,CAAA;CAAE,CAAA;AAEpE,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,gBAAgB,CAAA;AAGpB,MAAM,MAAM,QAAQ,GAAG,MAAM,EAAE,GAAG;IAAE,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAAA;AAElE,kCAAkC;AAClC,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAA;AAEhD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.20",
|
|
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.8",
|
|
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": "d53bc37be0fd923ff40f3cf7e2bd06a0496ddb73"
|
|
64
64
|
}
|
package/src/AutomergeUrl.ts
CHANGED
|
@@ -4,26 +4,54 @@ import type {
|
|
|
4
4
|
BinaryDocumentId,
|
|
5
5
|
DocumentId,
|
|
6
6
|
AnyDocumentId,
|
|
7
|
+
UrlHeads,
|
|
7
8
|
} from "./types.js"
|
|
9
|
+
|
|
8
10
|
import * as Uuid from "uuid"
|
|
9
11
|
import bs58check from "bs58check"
|
|
12
|
+
import {
|
|
13
|
+
uint8ArrayFromHexString,
|
|
14
|
+
uint8ArrayToHexString,
|
|
15
|
+
} from "./helpers/bufferFromHex.js"
|
|
16
|
+
|
|
17
|
+
import type { Heads as AutomergeHeads } from "@automerge/automerge/slim"
|
|
10
18
|
|
|
11
19
|
export const urlPrefix = "automerge:"
|
|
12
20
|
|
|
21
|
+
interface ParsedAutomergeUrl {
|
|
22
|
+
/** unencoded DocumentId */
|
|
23
|
+
binaryDocumentId: BinaryDocumentId
|
|
24
|
+
/** bs58 encoded DocumentId */
|
|
25
|
+
documentId: DocumentId
|
|
26
|
+
/** Optional array of heads, if specified in URL */
|
|
27
|
+
heads?: UrlHeads
|
|
28
|
+
/** Optional hex array of heads, in Automerge core format */
|
|
29
|
+
hexHeads?: string[] // AKA: heads
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/** Given an Automerge URL, returns the DocumentId in both base58check-encoded form and binary form */
|
|
14
|
-
export const parseAutomergeUrl = (url: AutomergeUrl) => {
|
|
33
|
+
export const parseAutomergeUrl = (url: AutomergeUrl): ParsedAutomergeUrl => {
|
|
34
|
+
const [baseUrl, headsSection, ...rest] = url.split("#")
|
|
35
|
+
if (rest.length > 0) {
|
|
36
|
+
throw new Error("Invalid URL: contains multiple heads sections")
|
|
37
|
+
}
|
|
15
38
|
const regex = new RegExp(`^${urlPrefix}(\\w+)$`)
|
|
16
|
-
const [, docMatch] =
|
|
39
|
+
const [, docMatch] = baseUrl.match(regex) || []
|
|
17
40
|
const documentId = docMatch as DocumentId
|
|
18
41
|
const binaryDocumentId = documentIdToBinary(documentId)
|
|
19
42
|
|
|
20
43
|
if (!binaryDocumentId) throw new Error("Invalid document URL: " + url)
|
|
21
|
-
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
if (headsSection === undefined) return { binaryDocumentId, documentId }
|
|
45
|
+
|
|
46
|
+
const heads = (headsSection === "" ? [] : headsSection.split("|")) as UrlHeads
|
|
47
|
+
const hexHeads = heads.map(head => {
|
|
48
|
+
try {
|
|
49
|
+
return uint8ArrayToHexString(bs58check.decode(head))
|
|
50
|
+
} catch (e) {
|
|
51
|
+
throw new Error(`Invalid head in URL: ${head}`)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return { binaryDocumentId, hexHeads, documentId, heads }
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
/**
|
|
@@ -32,38 +60,78 @@ export const parseAutomergeUrl = (url: AutomergeUrl) => {
|
|
|
32
60
|
*/
|
|
33
61
|
export const stringifyAutomergeUrl = (
|
|
34
62
|
arg: UrlOptions | DocumentId | BinaryDocumentId
|
|
35
|
-
) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
): AutomergeUrl => {
|
|
64
|
+
if (arg instanceof Uint8Array || typeof arg === "string") {
|
|
65
|
+
return (urlPrefix +
|
|
66
|
+
(arg instanceof Uint8Array
|
|
67
|
+
? binaryToDocumentId(arg)
|
|
68
|
+
: arg)) as AutomergeUrl
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { documentId, heads = undefined } = arg
|
|
72
|
+
|
|
73
|
+
if (documentId === undefined)
|
|
74
|
+
throw new Error("Invalid documentId: " + documentId)
|
|
42
75
|
|
|
43
76
|
const encodedDocumentId =
|
|
44
77
|
documentId instanceof Uint8Array
|
|
45
78
|
? binaryToDocumentId(documentId)
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
: documentId
|
|
80
|
+
|
|
81
|
+
let url = `${urlPrefix}${encodedDocumentId}`
|
|
82
|
+
|
|
83
|
+
if (heads !== undefined) {
|
|
84
|
+
heads.forEach(head => {
|
|
85
|
+
try {
|
|
86
|
+
bs58check.decode(head)
|
|
87
|
+
} catch (e) {
|
|
88
|
+
throw new Error(`Invalid head: ${head}`)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
url += "#" + heads.join("|")
|
|
92
|
+
}
|
|
49
93
|
|
|
50
|
-
|
|
51
|
-
|
|
94
|
+
return url as AutomergeUrl
|
|
95
|
+
}
|
|
52
96
|
|
|
53
|
-
|
|
97
|
+
/** Helper to extract just the heads from a URL if they exist */
|
|
98
|
+
export const getHeadsFromUrl = (url: AutomergeUrl): string[] | undefined => {
|
|
99
|
+
const { heads } = parseAutomergeUrl(url)
|
|
100
|
+
return heads
|
|
54
101
|
}
|
|
55
102
|
|
|
103
|
+
export const anyDocumentIdToAutomergeUrl = (id: AnyDocumentId) =>
|
|
104
|
+
isValidAutomergeUrl(id)
|
|
105
|
+
? id
|
|
106
|
+
: isValidDocumentId(id)
|
|
107
|
+
? stringifyAutomergeUrl({ documentId: id })
|
|
108
|
+
: isValidUuid(id)
|
|
109
|
+
? parseLegacyUUID(id)
|
|
110
|
+
: undefined
|
|
111
|
+
|
|
56
112
|
/**
|
|
57
113
|
* Given a string, returns true if it is a valid Automerge URL. This function also acts as a type
|
|
58
114
|
* discriminator in Typescript.
|
|
59
115
|
*/
|
|
60
116
|
export const isValidAutomergeUrl = (str: unknown): str is AutomergeUrl => {
|
|
61
|
-
if (typeof str !== "string"
|
|
62
|
-
|
|
63
|
-
const automergeUrl = str as AutomergeUrl
|
|
117
|
+
if (typeof str !== "string" || !str || !str.startsWith(urlPrefix))
|
|
118
|
+
return false
|
|
64
119
|
try {
|
|
65
|
-
const { documentId } = parseAutomergeUrl(
|
|
66
|
-
|
|
120
|
+
const { documentId, heads } = parseAutomergeUrl(str as AutomergeUrl)
|
|
121
|
+
if (!isValidDocumentId(documentId)) return false
|
|
122
|
+
if (
|
|
123
|
+
heads &&
|
|
124
|
+
!heads.every(head => {
|
|
125
|
+
try {
|
|
126
|
+
bs58check.decode(head)
|
|
127
|
+
return true
|
|
128
|
+
} catch {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
return false
|
|
134
|
+
return true
|
|
67
135
|
} catch {
|
|
68
136
|
return false
|
|
69
137
|
}
|
|
@@ -97,6 +165,12 @@ export const documentIdToBinary = (docId: DocumentId) =>
|
|
|
97
165
|
export const binaryToDocumentId = (docId: BinaryDocumentId) =>
|
|
98
166
|
bs58check.encode(docId) as DocumentId
|
|
99
167
|
|
|
168
|
+
export const encodeHeads = (heads: AutomergeHeads): UrlHeads =>
|
|
169
|
+
heads.map(h => bs58check.encode(uint8ArrayFromHexString(h))) as UrlHeads
|
|
170
|
+
|
|
171
|
+
export const decodeHeads = (heads: UrlHeads): AutomergeHeads =>
|
|
172
|
+
heads.map(h => uint8ArrayToHexString(bs58check.decode(h))) as AutomergeHeads
|
|
173
|
+
|
|
100
174
|
export const parseLegacyUUID = (str: string) => {
|
|
101
175
|
if (!Uuid.validate(str)) return undefined
|
|
102
176
|
const documentId = Uuid.parse(str) as BinaryDocumentId
|
|
@@ -141,4 +215,5 @@ export const interpretAsDocumentId = (id: AnyDocumentId) => {
|
|
|
141
215
|
|
|
142
216
|
type UrlOptions = {
|
|
143
217
|
documentId: DocumentId | BinaryDocumentId
|
|
218
|
+
heads?: UrlHeads
|
|
144
219
|
}
|