@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.
Files changed (61) hide show
  1. package/dist/AutomergeUrl.d.ts +17 -5
  2. package/dist/AutomergeUrl.d.ts.map +1 -1
  3. package/dist/AutomergeUrl.js +71 -24
  4. package/dist/DocHandle.d.ts +80 -8
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +181 -10
  7. package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
  8. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
  9. package/dist/RemoteHeadsSubscriptions.js +4 -1
  10. package/dist/Repo.d.ts +35 -2
  11. package/dist/Repo.d.ts.map +1 -1
  12. package/dist/Repo.js +112 -70
  13. package/dist/entrypoints/fullfat.d.ts +1 -0
  14. package/dist/entrypoints/fullfat.d.ts.map +1 -1
  15. package/dist/entrypoints/fullfat.js +1 -2
  16. package/dist/helpers/bufferFromHex.d.ts +3 -0
  17. package/dist/helpers/bufferFromHex.d.ts.map +1 -0
  18. package/dist/helpers/bufferFromHex.js +13 -0
  19. package/dist/helpers/headsAreSame.d.ts +2 -2
  20. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  21. package/dist/helpers/mergeArrays.d.ts +1 -1
  22. package/dist/helpers/mergeArrays.d.ts.map +1 -1
  23. package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
  24. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  25. package/dist/helpers/tests/storage-adapter-tests.js +25 -48
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/storage/StorageSubsystem.d.ts +11 -1
  30. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  31. package/dist/storage/StorageSubsystem.js +20 -4
  32. package/dist/synchronizer/CollectionSynchronizer.d.ts +15 -2
  33. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  34. package/dist/synchronizer/CollectionSynchronizer.js +29 -8
  35. package/dist/synchronizer/DocSynchronizer.d.ts +7 -0
  36. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  37. package/dist/synchronizer/DocSynchronizer.js +14 -0
  38. package/dist/synchronizer/Synchronizer.d.ts +11 -0
  39. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  40. package/dist/types.d.ts +4 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +3 -3
  43. package/src/AutomergeUrl.ts +101 -26
  44. package/src/DocHandle.ts +245 -20
  45. package/src/RemoteHeadsSubscriptions.ts +11 -9
  46. package/src/Repo.ts +163 -68
  47. package/src/entrypoints/fullfat.ts +1 -2
  48. package/src/helpers/bufferFromHex.ts +14 -0
  49. package/src/helpers/headsAreSame.ts +2 -2
  50. package/src/helpers/tests/storage-adapter-tests.ts +44 -86
  51. package/src/index.ts +2 -0
  52. package/src/storage/StorageSubsystem.ts +29 -4
  53. package/src/synchronizer/CollectionSynchronizer.ts +42 -9
  54. package/src/synchronizer/DocSynchronizer.ts +15 -0
  55. package/src/synchronizer/Synchronizer.ts +14 -0
  56. package/src/types.ts +4 -1
  57. package/test/AutomergeUrl.test.ts +130 -0
  58. package/test/DocHandle.test.ts +209 -2
  59. package/test/DocSynchronizer.test.ts +10 -3
  60. package/test/Repo.test.ts +228 -3
  61. 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
- const loaded = await this.#storageAdapter.load(key);
182
- return loaded ? A.decodeSyncState(loaded) : undefined;
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
- constructor(repo: Repo);
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;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAU1C,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAqD9B;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU;IAyBxC;;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;CACF"}
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
- #docSynchronizers = {};
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
- constructor(repo) {
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.#docSynchronizers[documentId]) {
24
+ if (!this.docSynchronizers[documentId]) {
22
25
  const handle = this.repo.find(stringifyAutomergeUrl({ documentId }));
23
- this.#docSynchronizers[documentId] = this.#initDocSynchronizer(handle);
26
+ this.docSynchronizers[documentId] = this.#initDocSynchronizer(handle);
24
27
  }
25
- return this.#docSynchronizers[documentId];
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.#docSynchronizers)) {
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.#docSynchronizers)) {
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;CA8EzD"}
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;CAC1C;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;CACrB"}
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
- __SessionId: true;
38
+ __sessionId: true;
36
39
  };
37
40
  //# sourceMappingURL=types.d.ts.map
@@ -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;AAEpB,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"}
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.2",
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.7",
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": "373ce97a1c3153b476926290942e8a55cde7874b"
63
+ "gitHead": "d53bc37be0fd923ff40f3cf7e2bd06a0496ddb73"
64
64
  }
@@ -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] = url.match(regex) || []
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
- /** unencoded DocumentId */
23
- binaryDocumentId,
24
- /** encoded DocumentId */
25
- documentId,
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
- const documentId =
37
- arg instanceof Uint8Array || typeof arg === "string"
38
- ? arg
39
- : "documentId" in arg
40
- ? arg.documentId
41
- : undefined
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
- : typeof documentId === "string"
47
- ? documentId
48
- : undefined
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
- if (encodedDocumentId === undefined)
51
- throw new Error("Invalid documentId: " + documentId)
94
+ return url as AutomergeUrl
95
+ }
52
96
 
53
- return (urlPrefix + encodedDocumentId) as AutomergeUrl
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") return false
62
- if (!str || !str.startsWith(urlPrefix)) return false
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(automergeUrl)
66
- return isValidDocumentId(documentId)
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
  }