@automerge/automerge-repo 2.0.0-alpha.11 → 2.0.0-alpha.12

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.
@@ -30,6 +30,13 @@ export declare class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
30
30
  * peers. We do not currently have an equivalent `whenSynced()`.
31
31
  */
32
32
  isReady: () => boolean;
33
+ /**
34
+ * @returns true if the document has been unloaded.
35
+ *
36
+ * Unloaded documents are freed from memory but not removed from local storage. It's not currently
37
+ * possible at runtime to reload an unloaded document.
38
+ */
39
+ isUnloaded: () => boolean;
33
40
  /**
34
41
  * @returns true if the document has been marked as deleted.
35
42
  *
@@ -48,7 +55,7 @@ export declare class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
48
55
  */
49
56
  inState: (states: HandleState[]) => boolean;
50
57
  /** @hidden */
51
- get state(): "idle" | "loading" | "requesting" | "ready" | "unavailable" | "deleted";
58
+ get state(): "idle" | "loading" | "requesting" | "ready" | "unavailable" | "unloaded" | "deleted";
52
59
  /**
53
60
  * @returns a promise that resolves when the document is in one of the given states (if no states
54
61
  * are passed, when the document is ready)
@@ -86,6 +93,7 @@ export declare class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
86
93
  * @returns the current document's heads, or undefined if the document is not ready
87
94
  */
88
95
  heads(): A.Heads | undefined;
96
+ begin(): void;
89
97
  /**
90
98
  * Creates a fixed "view" of an automerge document at the given point in time represented
91
99
  * by the `heads` passed in. The return value is the same type as docSync() and will return
@@ -199,6 +207,10 @@ export declare class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
199
207
  * @hidden
200
208
  * */
201
209
  request(): void;
210
+ /** Called by the repo to free memory used by the document. */
211
+ unload(): void;
212
+ /** Called by the repo to reuse an unloaded handle. */
213
+ reload(): void;
202
214
  /** Called by the repo when the document is deleted. */
203
215
  delete(): void;
204
216
  /**
@@ -287,11 +299,13 @@ export declare const HandleState: {
287
299
  readonly REQUESTING: "requesting";
288
300
  /** The document is available */
289
301
  readonly READY: "ready";
302
+ /** The document has been unloaded from the handle, to free memory usage */
303
+ readonly UNLOADED: "unloaded";
290
304
  /** The document has been deleted from the repo */
291
305
  readonly DELETED: "deleted";
292
306
  /** The document was not available in storage or from any connected peers */
293
307
  readonly UNAVAILABLE: "unavailable";
294
308
  };
295
309
  export type HandleState = (typeof HandleState)[keyof typeof HandleState];
296
- export declare const IDLE: "idle", LOADING: "loading", REQUESTING: "requesting", READY: "ready", DELETED: "deleted", UNAVAILABLE: "unavailable";
310
+ export declare const IDLE: "idle", LOADING: "loading", REQUESTING: "requesting", READY: "ready", UNLOADED: "unloaded", DELETED: "deleted", UNAVAILABLE: "unavailable";
297
311
  //# sourceMappingURL=DocHandle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DocHandle.d.ts","sourceRoot":"","sources":["../src/DocHandle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DocHandle.d.ts","sourceRoot":"","sources":["../src/DocHandle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,gCAAgC,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAM5C,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAE9C;;;;;;;;;;;;GAYG;AACH,qBAAa,SAAS,CAAC,CAAC,CAAE,SAAQ,YAAY,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;;IAkBvD,UAAU,EAAE,UAAU;IAF/B,cAAc;gBAEL,UAAU,EAAE,UAAU,EAC7B,OAAO,GAAE,gBAAgB,CAAC,CAAC,CAAM;IA8JnC;OACG;IACH,IAAI,GAAG,IAAI,YAAY,CAEtB;IAED;;;;;OAKG;IACH,OAAO,gBAAgC;IAEvC;;;;;OAKG;IACH,UAAU,gBAAmC;IAE7C;;;;;OAKG;IACH,SAAS,gBAAkC;IAE3C;;;;OAIG;IACH,aAAa,gBAAsC;IAEnD;;OAEG;IACH,OAAO,WAAY,WAAW,EAAE,aAC0B;IAE1D,cAAc;IACd,IAAI,KAAK,yFAER;IAED;;;;;;OAMG;IACG,SAAS,CAAC,WAAW,GAAE,WAAW,EAAc;IAItD;;;;;OAKG;IACG,GAAG;IACP,sEAAsE;IACtE,WAAW,GAAE,WAAW,EAA6B;IAavD;;;;;;;;;;;;OAYG;IACH,OAAO;IAKP;;;;OAIG;IACH,KAAK,IAAI,CAAC,CAAC,KAAK,GAAG,SAAS;IAO5B,KAAK;IAIL;;;;;;;;;;;;;OAaG;IACH,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,GAAG,SAAS;IAShC;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS;IAO1C;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,EAAE,GAAG,SAAS;IAU7D;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,aAAa,GAAG,SAAS;IAWtD;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAI5C;;;;OAIG;IACH,WAAW;IAIX;;;OAGG;IACH,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK;IAKnD,0CAA0C;IAC1C,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,GAAG,SAAS;IAIzD;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAM;IAWhE;;;;OAIG;IACH,QAAQ,CACN,KAAK,EAAE,CAAC,CAAC,KAAK,EACd,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EACvB,OAAO,GAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAM,GAC/B,MAAM,EAAE,GAAG,SAAS;IAsBvB;;;;;;;OAOG;IACH,KAAK;IACH,wDAAwD;IACxD,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;IAe3B;;;OAGG;IACH,WAAW;IAIX;;SAEK;IACL,OAAO;IAIP,8DAA8D;IAC9D,MAAM;IAIN,sDAAsD;IACtD,MAAM;IAIN,uDAAuD;IACvD,MAAM;IAIN;;;;;;OAMG;IACH,SAAS,CAAC,OAAO,EAAE,OAAO;IAO1B,OAAO,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE;CAGlD;AAID,cAAc;AACd,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAE1B;IACE,gGAAgG;IAChG,KAAK,EAAE,IAAI,CAAA;IAEX,yCAAyC;IACzC,YAAY,CAAC,EAAE,CAAC,CAAA;CACjB,GAED;IACE,KAAK,CAAC,EAAE,KAAK,CAAA;IAEb,+HAA+H;IAC/H,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAIL,2EAA2E;AAC3E,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,eAAe,EAAE,CAAC,OAAO,EAAE,6BAA6B,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACpE,MAAM,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACpD,MAAM,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACpD,WAAW,EAAE,CAAC,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAC9D,mBAAmB,EAAE,CAAC,OAAO,EAAE,gCAAgC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAC3E,4BAA4B,EAAE,CAC5B,OAAO,EAAE,wCAAwC,CAAC,CAAC,CAAC,KACjD,IAAI,CAAA;IACT,cAAc,EAAE,CAAC,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAA;CAC/D;AAED,sDAAsD;AACtD,MAAM,WAAW,6BAA6B,CAAC,CAAC;IAC9C,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;CACd;AAED,6CAA6C;AAC7C,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,8BAA8B;IAC9B,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,iDAAiD;IACjD,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACb,wDAAwD;IACxD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,CAAA;IAClB,mCAAmC;IACnC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;CAC1B;AAED,4CAA4C;AAC5C,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;CACrB;AAED,6DAA6D;AAC7D,MAAM,WAAW,2BAA2B,CAAC,CAAC;IAC5C,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;CACrB;AAED,qEAAqE;AACrE,MAAM,WAAW,gCAAgC,CAAC,CAAC;IACjD,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,kEAAkE;AAClE,MAAM,WAAW,wCAAwC,CAAC,CAAC;IACzD,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACpB,IAAI,EAAE,UAAU,CAAA;CACjB;AAED,8DAA8D;AAC9D,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,SAAS,CAAA;IACpB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAA;CACf;AAMD;;GAEG;AACH,eAAO,MAAM,WAAW;IACtB,kEAAkE;;IAElE,mDAAmD;;IAEnD,6EAA6E;;IAE7E,gCAAgC;;IAEhC,2EAA2E;;IAE3E,kDAAkD;;IAElD,4EAA4E;;CAEpE,CAAA;AACV,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAA;AAExE,eAAO,MACL,IAAI,UACJ,OAAO,aACP,UAAU,gBACV,KAAK,WACL,QAAQ,cACR,OAAO,aACP,WAAW,eACE,CAAA"}
package/dist/DocHandle.js CHANGED
@@ -59,6 +59,9 @@ export class DocHandle extends EventEmitter {
59
59
  this.emit("delete", { handle: this });
60
60
  return { doc: A.init() };
61
61
  }),
62
+ onUnload: assign(() => {
63
+ return { doc: A.init() };
64
+ }),
62
65
  onUnavailable: () => {
63
66
  this.emit("unavailable", { handle: this });
64
67
  },
@@ -71,6 +74,7 @@ export class DocHandle extends EventEmitter {
71
74
  context: { documentId, doc },
72
75
  on: {
73
76
  UPDATE: { actions: "onUpdate" },
77
+ UNLOAD: ".unloaded",
74
78
  DELETE: ".deleted",
75
79
  },
76
80
  states: {
@@ -98,6 +102,12 @@ export class DocHandle extends EventEmitter {
98
102
  on: { DOC_READY: "ready" },
99
103
  },
100
104
  ready: {},
105
+ unloaded: {
106
+ entry: "onUnload",
107
+ on: {
108
+ RELOAD: "loading",
109
+ },
110
+ },
101
111
  deleted: { entry: "onDelete", type: "final" },
102
112
  },
103
113
  });
@@ -113,7 +123,7 @@ export class DocHandle extends EventEmitter {
113
123
  });
114
124
  // Start the machine, and send a create or find event to get things going
115
125
  this.#machine.start();
116
- this.#machine.send({ type: BEGIN });
126
+ this.begin();
117
127
  }
118
128
  // PRIVATE
119
129
  /** Returns the current document, regardless of state */
@@ -172,6 +182,13 @@ export class DocHandle extends EventEmitter {
172
182
  * peers. We do not currently have an equivalent `whenSynced()`.
173
183
  */
174
184
  isReady = () => this.inState(["ready"]);
185
+ /**
186
+ * @returns true if the document has been unloaded.
187
+ *
188
+ * Unloaded documents are freed from memory but not removed from local storage. It's not currently
189
+ * possible at runtime to reload an unloaded document.
190
+ */
191
+ isUnloaded = () => this.inState(["unloaded"]);
175
192
  /**
176
193
  * @returns true if the document has been marked as deleted.
177
194
  *
@@ -253,6 +270,9 @@ export class DocHandle extends EventEmitter {
253
270
  }
254
271
  return A.getHeads(this.#doc);
255
272
  }
273
+ begin() {
274
+ this.#machine.send({ type: BEGIN });
275
+ }
256
276
  /**
257
277
  * Creates a fixed "view" of an automerge document at the given point in time represented
258
278
  * by the `heads` passed in. The return value is the same type as docSync() and will return
@@ -444,6 +464,14 @@ export class DocHandle extends EventEmitter {
444
464
  if (this.#state === "loading")
445
465
  this.#machine.send({ type: REQUEST });
446
466
  }
467
+ /** Called by the repo to free memory used by the document. */
468
+ unload() {
469
+ this.#machine.send({ type: UNLOAD });
470
+ }
471
+ /** Called by the repo to reuse an unloaded handle. */
472
+ reload() {
473
+ this.#machine.send({ type: RELOAD });
474
+ }
447
475
  /** Called by the repo when the document is deleted. */
448
476
  delete() {
449
477
  this.#machine.send({ type: DELETE });
@@ -479,16 +507,20 @@ export const HandleState = {
479
507
  REQUESTING: "requesting",
480
508
  /** The document is available */
481
509
  READY: "ready",
510
+ /** The document has been unloaded from the handle, to free memory usage */
511
+ UNLOADED: "unloaded",
482
512
  /** The document has been deleted from the repo */
483
513
  DELETED: "deleted",
484
514
  /** The document was not available in storage or from any connected peers */
485
515
  UNAVAILABLE: "unavailable",
486
516
  };
487
- export const { IDLE, LOADING, REQUESTING, READY, DELETED, UNAVAILABLE } = HandleState;
517
+ export const { IDLE, LOADING, REQUESTING, READY, UNLOADED, DELETED, UNAVAILABLE, } = HandleState;
488
518
  const BEGIN = "BEGIN";
489
519
  const REQUEST = "REQUEST";
490
520
  const DOC_READY = "DOC_READY";
491
521
  const UPDATE = "UPDATE";
522
+ const UNLOAD = "UNLOAD";
523
+ const RELOAD = "RELOAD";
492
524
  const DELETE = "DELETE";
493
525
  const TIMEOUT = "TIMEOUT";
494
526
  const DOC_UNAVAILABLE = "DOC_UNAVAILABLE";
package/dist/Repo.d.ts CHANGED
@@ -6,6 +6,7 @@ import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js";
6
6
  import { StorageSubsystem } from "./storage/StorageSubsystem.js";
7
7
  import { StorageId } from "./storage/types.js";
8
8
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
9
+ import { DocSyncMetrics } from "./synchronizer/Synchronizer.js";
9
10
  import type { AnyDocumentId, DocumentId, PeerId } from "./types.js";
10
11
  /** A Repo is a collection of documents with networking, syncing, and storage capabilities. */
11
12
  /** The `Repo` is the main entry point of this library
@@ -92,6 +93,13 @@ export declare class Repo extends EventEmitter<RepoEvents> {
92
93
  * @returns Promise<void>
93
94
  */
94
95
  flush(documents?: DocumentId[]): Promise<void>;
96
+ /**
97
+ * Removes a DocHandle from the handleCache.
98
+ * @hidden this API is experimental and may change.
99
+ * @param documentId - documentId of the DocHandle to remove from handleCache, if present in cache.
100
+ * @returns Promise<void>
101
+ */
102
+ removeFromCache(documentId: DocumentId): Promise<void>;
95
103
  shutdown(): Promise<void>;
96
104
  metrics(): {
97
105
  documents: {
@@ -135,6 +143,7 @@ export interface RepoEvents {
135
143
  "delete-document": (arg: DeleteDocumentPayload) => void;
136
144
  /** A document was marked as unavailable (we don't have it and none of our peers have it) */
137
145
  "unavailable-document": (arg: DeleteDocumentPayload) => void;
146
+ "doc-metrics": (arg: DocMetrics) => void;
138
147
  }
139
148
  export interface DocumentPayload {
140
149
  handle: DocHandle<any>;
@@ -142,4 +151,11 @@ export interface DocumentPayload {
142
151
  export interface DeleteDocumentPayload {
143
152
  documentId: DocumentId;
144
153
  }
154
+ export type DocMetrics = DocSyncMetrics | {
155
+ type: "doc-loaded";
156
+ documentId: DocumentId;
157
+ durationMillis: number;
158
+ numOps: number;
159
+ numChanges: number;
160
+ };
145
161
  //# sourceMappingURL=Repo.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"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"}
package/dist/Repo.js CHANGED
@@ -2,7 +2,7 @@ import { next as Automerge } from "@automerge/automerge/slim";
2
2
  import debug from "debug";
3
3
  import { EventEmitter } from "eventemitter3";
4
4
  import { generateAutomergeUrl, interpretAsDocumentId, parseAutomergeUrl, } from "./AutomergeUrl.js";
5
- import { DocHandle } from "./DocHandle.js";
5
+ import { DELETED, DocHandle, READY, UNAVAILABLE, UNLOADED, } from "./DocHandle.js";
6
6
  import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js";
7
7
  import { headsAreSame } from "./helpers/headsAreSame.js";
8
8
  import { throttle } from "./helpers/throttle.js";
@@ -62,6 +62,8 @@ export class Repo extends EventEmitter {
62
62
  this.#log(`sending ${message.type} message to ${message.targetId}`);
63
63
  networkSubsystem.send(message);
64
64
  });
65
+ // Forward metrics from doc synchronizers
66
+ this.synchronizer.on("metrics", event => this.emit("doc-metrics", event));
65
67
  if (this.#remoteHeadsGossipingEnabled) {
66
68
  this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
67
69
  this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId);
@@ -70,6 +72,9 @@ export class Repo extends EventEmitter {
70
72
  // STORAGE
71
73
  // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
72
74
  const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined;
75
+ if (storageSubsystem) {
76
+ storageSubsystem.on("document-loaded", event => this.emit("doc-metrics", { type: "doc-loaded", ...event }));
77
+ }
73
78
  this.storageSubsystem = storageSubsystem;
74
79
  // NETWORK
75
80
  // The network subsystem deals with sending and receiving messages to and from peers.
@@ -416,6 +421,34 @@ export class Repo extends EventEmitter {
416
421
  return this.storageSubsystem.saveDoc(handle.documentId, doc);
417
422
  }));
418
423
  }
424
+ /**
425
+ * Removes a DocHandle from the handleCache.
426
+ * @hidden this API is experimental and may change.
427
+ * @param documentId - documentId of the DocHandle to remove from handleCache, if present in cache.
428
+ * @returns Promise<void>
429
+ */
430
+ async removeFromCache(documentId) {
431
+ if (!this.#handleCache[documentId]) {
432
+ this.#log(`WARN: removeFromCache called but handle not found in handleCache for documentId: ${documentId}`);
433
+ return;
434
+ }
435
+ const handle = this.#getHandle({ documentId });
436
+ const doc = await handle.doc([READY, UNLOADED, DELETED, UNAVAILABLE]);
437
+ if (doc) {
438
+ if (handle.isReady()) {
439
+ handle.unload();
440
+ }
441
+ else {
442
+ this.#log(`WARN: removeFromCache called but handle for documentId: ${documentId} in unexpected state: ${handle.state}`);
443
+ }
444
+ delete this.#handleCache[documentId];
445
+ // TODO: remove document from synchronizer when removeDocument is implemented
446
+ // this.synchronizer.removeDocument(documentId)
447
+ }
448
+ else {
449
+ this.#log(`WARN: removeFromCache called but doc undefined for documentId: ${documentId}`);
450
+ }
451
+ }
419
452
  shutdown() {
420
453
  this.networkSubsystem.adapters.forEach(adapter => {
421
454
  adapter.disconnect();
@@ -1,7 +1,7 @@
1
1
  import type { StorageAdapterInterface } from "../../storage/StorageAdapterInterface.js";
2
- export declare function runStorageAdapterTests(_setup: SetupFn, title?: string): void;
2
+ export declare function runStorageAdapterTests(setup: SetupFn, title?: string): void;
3
3
  export type SetupFn = () => Promise<{
4
4
  adapter: StorageAdapterInterface;
5
- teardown?: () => void;
5
+ teardown?: () => void | Promise<void>;
6
6
  }>;
7
7
  //# sourceMappingURL=storage-adapter-tests.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-adapter-tests.d.ts","sourceRoot":"","sources":["../../../src/helpers/tests/storage-adapter-tests.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAA;AAQvF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CA+K5E;AAID,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;IAClC,OAAO,EAAE,uBAAuB,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB,CAAC,CAAA"}
1
+ {"version":3,"file":"storage-adapter-tests.d.ts","sourceRoot":"","sources":["../../../src/helpers/tests/storage-adapter-tests.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAA;AAcvF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CA0I3E;AAID,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;IAClC,OAAO,EAAE,uBAAuB,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC,CAAC,CAAA"}
@@ -1,55 +1,46 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, beforeEach, it as _it } from "vitest";
2
2
  const PAYLOAD_A = () => new Uint8Array([0, 1, 127, 99, 154, 235]);
3
3
  const PAYLOAD_B = () => new Uint8Array([1, 76, 160, 53, 57, 10, 230]);
4
4
  const PAYLOAD_C = () => new Uint8Array([2, 111, 74, 131, 236, 96, 142, 193]);
5
5
  const LARGE_PAYLOAD = new Uint8Array(100000).map(() => Math.random() * 256);
6
- export function runStorageAdapterTests(_setup, title) {
7
- const setup = async () => {
8
- const { adapter, teardown = NO_OP } = await _setup();
9
- return { adapter, teardown };
10
- };
6
+ const it = (_it);
7
+ export function runStorageAdapterTests(setup, title) {
8
+ beforeEach(async (ctx) => {
9
+ const { adapter, teardown = NO_OP } = await setup();
10
+ ctx.adapter = adapter;
11
+ return teardown;
12
+ });
11
13
  describe(`Storage adapter acceptance tests ${title ? `(${title})` : ""}`, () => {
12
14
  describe("load", () => {
13
- it("should return undefined if there is no data", async () => {
14
- const { adapter, teardown } = await setup();
15
+ it("should return undefined if there is no data", async ({ adapter }) => {
15
16
  const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"]);
16
17
  expect(actual).toBeUndefined();
17
- teardown();
18
18
  });
19
19
  });
20
20
  describe("save and load", () => {
21
- it("should return data that was saved", async () => {
22
- const { adapter, teardown } = await setup();
21
+ it("should return data that was saved", async ({ adapter }) => {
23
22
  await adapter.save(["storage-adapter-id"], PAYLOAD_A());
24
23
  const actual = await adapter.load(["storage-adapter-id"]);
25
24
  expect(actual).toStrictEqual(PAYLOAD_A());
26
- teardown();
27
25
  });
28
- it("should work with composite keys", async () => {
29
- const { adapter, teardown } = await setup();
26
+ it("should work with composite keys", async ({ adapter }) => {
30
27
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A());
31
28
  const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"]);
32
29
  expect(actual).toStrictEqual(PAYLOAD_A());
33
- teardown();
34
30
  });
35
- it("should work with a large payload", async () => {
36
- const { adapter, teardown } = await setup();
31
+ it("should work with a large payload", async ({ adapter }) => {
37
32
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], LARGE_PAYLOAD);
38
33
  const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"]);
39
34
  expect(actual).toStrictEqual(LARGE_PAYLOAD);
40
- teardown();
41
35
  });
42
36
  });
43
37
  describe("loadRange", () => {
44
- it("should return an empty array if there is no data", async () => {
45
- const { adapter, teardown } = await setup();
38
+ it("should return an empty array if there is no data", async ({ adapter, }) => {
46
39
  expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([]);
47
- teardown();
48
40
  });
49
41
  });
50
42
  describe("save and loadRange", () => {
51
- it("should return all the data that matches the key", async () => {
52
- const { adapter, teardown } = await setup();
43
+ it("should return all the data that matches the key", async ({ adapter, }) => {
53
44
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A());
54
45
  await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B());
55
46
  await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C());
@@ -62,10 +53,8 @@ export function runStorageAdapterTests(_setup, title) {
62
53
  { key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
63
54
  { key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
64
55
  ]));
65
- teardown();
66
56
  });
67
- it("should only load values that match they key", async () => {
68
- const { adapter, teardown } = await setup();
57
+ it("should only load values that match they key", async ({ adapter }) => {
69
58
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A());
70
59
  await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_C());
71
60
  const actual = await adapter.loadRange(["AAAAA"]);
@@ -75,33 +64,27 @@ export function runStorageAdapterTests(_setup, title) {
75
64
  expect(actual).toStrictEqual(expect.not.arrayContaining([
76
65
  { key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_C() },
77
66
  ]));
78
- teardown();
79
67
  });
80
68
  });
81
69
  describe("save and remove", () => {
82
- it("after removing, should be empty", async () => {
83
- const { adapter, teardown } = await setup();
70
+ it("after removing, should be empty", async ({ adapter }) => {
84
71
  await adapter.save(["AAAAA", "snapshot", "xxxxx"], PAYLOAD_A());
85
72
  await adapter.remove(["AAAAA", "snapshot", "xxxxx"]);
86
73
  expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([]);
87
74
  expect(await adapter.load(["AAAAA", "snapshot", "xxxxx"])).toBeUndefined();
88
- teardown();
89
75
  });
90
76
  });
91
77
  describe("save and save", () => {
92
- it("should overwrite data saved with the same key", async () => {
93
- const { adapter, teardown } = await setup();
78
+ it("should overwrite data saved with the same key", async ({ adapter, }) => {
94
79
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A());
95
80
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_B());
96
81
  expect(await adapter.loadRange(["AAAAA", "sync-state"])).toStrictEqual([
97
82
  { key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_B() },
98
83
  ]);
99
- teardown();
100
84
  });
101
85
  });
102
86
  describe("removeRange", () => {
103
- it("should remove a range of records", async () => {
104
- const { adapter, teardown } = await setup();
87
+ it("should remove a range of records", async ({ adapter }) => {
105
88
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A());
106
89
  await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B());
107
90
  await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C());
@@ -109,10 +92,8 @@ export function runStorageAdapterTests(_setup, title) {
109
92
  expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([
110
93
  { key: ["AAAAA", "snapshot", "yyyyy"], data: PAYLOAD_B() },
111
94
  ]);
112
- teardown();
113
95
  });
114
- it("should not remove records that don't match", async () => {
115
- const { adapter, teardown } = await setup();
96
+ it("should not remove records that don't match", async ({ adapter }) => {
116
97
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A());
117
98
  await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_B());
118
99
  await adapter.removeRange(["AAAAA"]);
@@ -120,7 +101,6 @@ export function runStorageAdapterTests(_setup, title) {
120
101
  expect(actual).toStrictEqual([
121
102
  { key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_B() },
122
103
  ]);
123
- teardown();
124
104
  });
125
105
  });
126
106
  });
@@ -2,11 +2,20 @@ import * as A from "@automerge/automerge/slim/next";
2
2
  import { type DocumentId } from "../types.js";
3
3
  import { StorageAdapterInterface } from "./StorageAdapterInterface.js";
4
4
  import { StorageId } from "./types.js";
5
+ import { EventEmitter } from "eventemitter3";
6
+ type StorageSubsystemEvents = {
7
+ "document-loaded": (arg: {
8
+ documentId: DocumentId;
9
+ durationMillis: number;
10
+ numOps: number;
11
+ numChanges: number;
12
+ }) => void;
13
+ };
5
14
  /**
6
15
  * The storage subsystem is responsible for saving and loading Automerge documents to and from
7
16
  * storage adapter. It also provides a generic key/value storage interface for other uses.
8
17
  */
9
- export declare class StorageSubsystem {
18
+ export declare class StorageSubsystem extends EventEmitter<StorageSubsystemEvents> {
10
19
  #private;
11
20
  constructor(storageAdapter: StorageAdapterInterface);
12
21
  id(): Promise<StorageId>;
@@ -49,4 +58,5 @@ export declare class StorageSubsystem {
49
58
  loadSyncState(documentId: DocumentId, storageId: StorageId): Promise<A.SyncState | undefined>;
50
59
  saveSyncState(documentId: DocumentId, storageId: StorageId, syncState: A.SyncState): Promise<void>;
51
60
  }
61
+ export {};
52
62
  //# sourceMappingURL=StorageSubsystem.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StorageSubsystem.d.ts","sourceRoot":"","sources":["../../src/storage/StorageSubsystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,gCAAgC,CAAA;AAInD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,OAAO,EAAyB,SAAS,EAAE,MAAM,YAAY,CAAA;AAK7D;;;GAGG;AACH,qBAAa,gBAAgB;;gBAef,cAAc,EAAE,uBAAuB;IAI7C,EAAE,IAAI,OAAO,CAAC,SAAS,CAAC;IA2B9B,kCAAkC;IAC5B,IAAI;IACR,iFAAiF;IACjF,SAAS,EAAE,MAAM;IAEjB,yFAAyF;IACzF,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAKlC,gCAAgC;IAC1B,IAAI;IACR,iFAAiF;IACjF,SAAS,EAAE,MAAM;IAEjB,yFAAyF;IACzF,GAAG,EAAE,MAAM;IAEX,sCAAsC;IACtC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,IAAI,CAAC;IAKhB,oCAAoC;IAC9B,MAAM;IACV,iFAAiF;IACjF,SAAS,EAAE,MAAM;IAEjB,2FAA2F;IAC3F,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;IAOhB;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAmClE;;;;;;OAMG;IACG,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAazE;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,UAAU;IAkEhC,aAAa,CACjB,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC;IAW7B,aAAa,CACjB,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,CAAC,CAAC,SAAS,GACrB,OAAO,CAAC,IAAI,CAAC;CA8CjB"}
1
+ {"version":3,"file":"StorageSubsystem.d.ts","sourceRoot":"","sources":["../../src/storage/StorageSubsystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,gCAAgC,CAAA;AAInD,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,OAAO,EAAyB,SAAS,EAAE,MAAM,YAAY,CAAA;AAI7D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,KAAK,sBAAsB,GAAG;IAC5B,iBAAiB,EAAE,CAAC,GAAG,EAAE;QACvB,UAAU,EAAE,UAAU,CAAA;QACtB,cAAc,EAAE,MAAM,CAAA;QACtB,MAAM,EAAE,MAAM,CAAA;QACd,UAAU,EAAE,MAAM,CAAA;KACnB,KAAK,IAAI,CAAA;CACX,CAAA;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;;gBAe5D,cAAc,EAAE,uBAAuB;IAK7C,EAAE,IAAI,OAAO,CAAC,SAAS,CAAC;IA2B9B,kCAAkC;IAC5B,IAAI;IACR,iFAAiF;IACjF,SAAS,EAAE,MAAM;IAEjB,yFAAyF;IACzF,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAKlC,gCAAgC;IAC1B,IAAI;IACR,iFAAiF;IACjF,SAAS,EAAE,MAAM;IAEjB,yFAAyF;IACzF,GAAG,EAAE,MAAM;IAEX,sCAAsC;IACtC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,IAAI,CAAC;IAKhB,oCAAoC;IAC9B,MAAM;IACV,iFAAiF;IACjF,SAAS,EAAE,MAAM;IAEjB,2FAA2F;IAC3F,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;IAOhB;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IA0ClE;;;;;;OAMG;IACG,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAazE;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,UAAU;IAkEhC,aAAa,CACjB,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC;IAW7B,aAAa,CACjB,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,CAAC,CAAC,SAAS,GACrB,OAAO,CAAC,IAAI,CAAC;CA8CjB"}
@@ -5,11 +5,12 @@ 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";
8
9
  /**
9
10
  * The storage subsystem is responsible for saving and loading Automerge documents to and from
10
11
  * storage adapter. It also provides a generic key/value storage interface for other uses.
11
12
  */
12
- export class StorageSubsystem {
13
+ export class StorageSubsystem extends EventEmitter {
13
14
  /** The storage adapter to use for saving and loading documents */
14
15
  #storageAdapter;
15
16
  /** Record of the latest heads we've loaded or saved for each document */
@@ -20,6 +21,7 @@ export class StorageSubsystem {
20
21
  #compacting = false;
21
22
  #log = debug(`automerge-repo:storage-subsystem`);
22
23
  constructor(storageAdapter) {
24
+ super();
23
25
  this.#storageAdapter = storageAdapter;
24
26
  }
25
27
  async id() {
@@ -100,7 +102,14 @@ export class StorageSubsystem {
100
102
  if (binary.length === 0)
101
103
  return null;
102
104
  // Load into an Automerge document
105
+ const start = performance.now();
103
106
  const newDoc = A.loadIncremental(A.init(), binary);
107
+ const end = performance.now();
108
+ this.emit("document-loaded", {
109
+ documentId,
110
+ durationMillis: end - start,
111
+ ...A.stats(newDoc),
112
+ });
104
113
  // Record the latest heads for the document
105
114
  this.#storedHeads.set(documentId, A.getHeads(newDoc));
106
115
  return newDoc;
@@ -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;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAW1C,OAAO,CAAC,IAAI;IAPxB,kDAAkD;IAClD,cAAc;IACd,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAK;gBAKtC,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;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
+ {"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;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAW1C,OAAO,CAAC,IAAI;IAPxB,kDAAkD;IAClD,cAAc;IACd,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAK;gBAKtC,IAAI,EAAE,IAAI;IAsD9B;;;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;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"}
@@ -43,6 +43,7 @@ export class CollectionSynchronizer extends Synchronizer {
43
43
  docSynchronizer.on("message", event => this.emit("message", event));
44
44
  docSynchronizer.on("open-doc", event => this.emit("open-doc", event));
45
45
  docSynchronizer.on("sync-state", event => this.emit("sync-state", event));
46
+ docSynchronizer.on("metrics", event => this.emit("metrics", event));
46
47
  return docSynchronizer;
47
48
  }
48
49
  /** returns an array of peerIds that we share this document generously with */
@@ -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;IA+ExD,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"}
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);
@@ -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,11 @@ 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
+ };
19
27
  //# 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,GAAG;IAC3B,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,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "2.0.0-alpha.11",
3
+ "version": "2.0.0-alpha.12",
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>",
@@ -60,5 +60,5 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  },
63
- "gitHead": "66b09d6f3662e16e0cd8e2cafef39ef5504104d6"
63
+ "gitHead": "8b016e42d2518ebb11eb148f52b9fb9a0b4467ff"
64
64
  }
package/src/DocHandle.ts CHANGED
@@ -72,6 +72,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
72
72
  this.emit("delete", { handle: this })
73
73
  return { doc: A.init() }
74
74
  }),
75
+ onUnload: assign(() => {
76
+ return { doc: A.init() }
77
+ }),
75
78
  onUnavailable: () => {
76
79
  this.emit("unavailable", { handle: this })
77
80
  },
@@ -86,6 +89,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
86
89
  context: { documentId, doc },
87
90
  on: {
88
91
  UPDATE: { actions: "onUpdate" },
92
+ UNLOAD: ".unloaded",
89
93
  DELETE: ".deleted",
90
94
  },
91
95
  states: {
@@ -113,6 +117,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
113
117
  on: { DOC_READY: "ready" },
114
118
  },
115
119
  ready: {},
120
+ unloaded: {
121
+ entry: "onUnload",
122
+ on: {
123
+ RELOAD: "loading",
124
+ },
125
+ },
116
126
  deleted: { entry: "onDelete", type: "final" },
117
127
  },
118
128
  })
@@ -131,7 +141,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
131
141
 
132
142
  // Start the machine, and send a create or find event to get things going
133
143
  this.#machine.start()
134
- this.#machine.send({ type: BEGIN })
144
+ this.begin()
135
145
  }
136
146
 
137
147
  // PRIVATE
@@ -203,6 +213,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
203
213
  */
204
214
  isReady = () => this.inState(["ready"])
205
215
 
216
+ /**
217
+ * @returns true if the document has been unloaded.
218
+ *
219
+ * Unloaded documents are freed from memory but not removed from local storage. It's not currently
220
+ * possible at runtime to reload an unloaded document.
221
+ */
222
+ isUnloaded = () => this.inState(["unloaded"])
223
+
206
224
  /**
207
225
  * @returns true if the document has been marked as deleted.
208
226
  *
@@ -291,6 +309,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
291
309
  return A.getHeads(this.#doc)
292
310
  }
293
311
 
312
+ begin() {
313
+ this.#machine.send({ type: BEGIN })
314
+ }
315
+
294
316
  /**
295
317
  * Creates a fixed "view" of an automerge document at the given point in time represented
296
318
  * by the `heads` passed in. The return value is the same type as docSync() and will return
@@ -505,6 +527,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
505
527
  if (this.#state === "loading") this.#machine.send({ type: REQUEST })
506
528
  }
507
529
 
530
+ /** Called by the repo to free memory used by the document. */
531
+ unload() {
532
+ this.#machine.send({ type: UNLOAD })
533
+ }
534
+
535
+ /** Called by the repo to reuse an unloaded handle. */
536
+ reload() {
537
+ this.#machine.send({ type: RELOAD })
538
+ }
539
+
508
540
  /** Called by the repo when the document is deleted. */
509
541
  delete() {
510
542
  this.#machine.send({ type: DELETE })
@@ -627,6 +659,8 @@ export const HandleState = {
627
659
  REQUESTING: "requesting",
628
660
  /** The document is available */
629
661
  READY: "ready",
662
+ /** The document has been unloaded from the handle, to free memory usage */
663
+ UNLOADED: "unloaded",
630
664
  /** The document has been deleted from the repo */
631
665
  DELETED: "deleted",
632
666
  /** The document was not available in storage or from any connected peers */
@@ -634,8 +668,15 @@ export const HandleState = {
634
668
  } as const
635
669
  export type HandleState = (typeof HandleState)[keyof typeof HandleState]
636
670
 
637
- export const { IDLE, LOADING, REQUESTING, READY, DELETED, UNAVAILABLE } =
638
- HandleState
671
+ export const {
672
+ IDLE,
673
+ LOADING,
674
+ REQUESTING,
675
+ READY,
676
+ UNLOADED,
677
+ DELETED,
678
+ UNAVAILABLE,
679
+ } = HandleState
639
680
 
640
681
  // context
641
682
 
@@ -655,14 +696,18 @@ type DocHandleEvent<T> =
655
696
  type: typeof UPDATE
656
697
  payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
657
698
  }
658
- | { type: typeof TIMEOUT }
699
+ | { type: typeof UNLOAD }
700
+ | { type: typeof RELOAD }
659
701
  | { type: typeof DELETE }
702
+ | { type: typeof TIMEOUT }
660
703
  | { type: typeof DOC_UNAVAILABLE }
661
704
 
662
705
  const BEGIN = "BEGIN"
663
706
  const REQUEST = "REQUEST"
664
707
  const DOC_READY = "DOC_READY"
665
708
  const UPDATE = "UPDATE"
709
+ const UNLOAD = "UNLOAD"
710
+ const RELOAD = "RELOAD"
666
711
  const DELETE = "DELETE"
667
712
  const TIMEOUT = "TIMEOUT"
668
713
  const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
package/src/Repo.ts CHANGED
@@ -6,7 +6,14 @@ import {
6
6
  interpretAsDocumentId,
7
7
  parseAutomergeUrl,
8
8
  } from "./AutomergeUrl.js"
9
- import { DocHandle, DocHandleEncodedChangePayload } from "./DocHandle.js"
9
+ import {
10
+ DELETED,
11
+ DocHandle,
12
+ DocHandleEncodedChangePayload,
13
+ READY,
14
+ UNAVAILABLE,
15
+ UNLOADED,
16
+ } from "./DocHandle.js"
10
17
  import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js"
11
18
  import { headsAreSame } from "./helpers/headsAreSame.js"
12
19
  import { throttle } from "./helpers/throttle.js"
@@ -20,7 +27,10 @@ import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js"
20
27
  import { StorageSubsystem } from "./storage/StorageSubsystem.js"
21
28
  import { StorageId } from "./storage/types.js"
22
29
  import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
23
- import { SyncStatePayload } from "./synchronizer/Synchronizer.js"
30
+ import {
31
+ DocSyncMetrics,
32
+ SyncStatePayload,
33
+ } from "./synchronizer/Synchronizer.js"
24
34
  import type { AnyDocumentId, DocumentId, PeerId } from "./types.js"
25
35
 
26
36
  function randomPeerId() {
@@ -97,6 +107,9 @@ export class Repo extends EventEmitter<RepoEvents> {
97
107
  networkSubsystem.send(message)
98
108
  })
99
109
 
110
+ // Forward metrics from doc synchronizers
111
+ this.synchronizer.on("metrics", event => this.emit("doc-metrics", event))
112
+
100
113
  if (this.#remoteHeadsGossipingEnabled) {
101
114
  this.synchronizer.on("open-doc", ({ peerId, documentId }) => {
102
115
  this.#remoteHeadsSubscriptions.subscribePeerToDoc(peerId, documentId)
@@ -106,6 +119,12 @@ export class Repo extends EventEmitter<RepoEvents> {
106
119
  // STORAGE
107
120
  // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
108
121
  const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined
122
+ if (storageSubsystem) {
123
+ storageSubsystem.on("document-loaded", event =>
124
+ this.emit("doc-metrics", { type: "doc-loaded", ...event })
125
+ )
126
+ }
127
+
109
128
  this.storageSubsystem = storageSubsystem
110
129
 
111
130
  // NETWORK
@@ -539,6 +558,39 @@ export class Repo extends EventEmitter<RepoEvents> {
539
558
  )
540
559
  }
541
560
 
561
+ /**
562
+ * Removes a DocHandle from the handleCache.
563
+ * @hidden this API is experimental and may change.
564
+ * @param documentId - documentId of the DocHandle to remove from handleCache, if present in cache.
565
+ * @returns Promise<void>
566
+ */
567
+ async removeFromCache(documentId: DocumentId) {
568
+ if (!this.#handleCache[documentId]) {
569
+ this.#log(
570
+ `WARN: removeFromCache called but handle not found in handleCache for documentId: ${documentId}`
571
+ )
572
+ return
573
+ }
574
+ const handle = this.#getHandle({ documentId })
575
+ const doc = await handle.doc([READY, UNLOADED, DELETED, UNAVAILABLE])
576
+ if (doc) {
577
+ if (handle.isReady()) {
578
+ handle.unload()
579
+ } else {
580
+ this.#log(
581
+ `WARN: removeFromCache called but handle for documentId: ${documentId} in unexpected state: ${handle.state}`
582
+ )
583
+ }
584
+ delete this.#handleCache[documentId]
585
+ // TODO: remove document from synchronizer when removeDocument is implemented
586
+ // this.synchronizer.removeDocument(documentId)
587
+ } else {
588
+ this.#log(
589
+ `WARN: removeFromCache called but doc undefined for documentId: ${documentId}`
590
+ )
591
+ }
592
+ }
593
+
542
594
  shutdown(): Promise<void> {
543
595
  this.networkSubsystem.adapters.forEach(adapter => {
544
596
  adapter.disconnect()
@@ -598,6 +650,7 @@ export interface RepoEvents {
598
650
  "delete-document": (arg: DeleteDocumentPayload) => void
599
651
  /** A document was marked as unavailable (we don't have it and none of our peers have it) */
600
652
  "unavailable-document": (arg: DeleteDocumentPayload) => void
653
+ "doc-metrics": (arg: DocMetrics) => void
601
654
  }
602
655
 
603
656
  export interface DocumentPayload {
@@ -607,3 +660,13 @@ export interface DocumentPayload {
607
660
  export interface DeleteDocumentPayload {
608
661
  documentId: DocumentId
609
662
  }
663
+
664
+ export type DocMetrics =
665
+ | DocSyncMetrics
666
+ | {
667
+ type: "doc-loaded"
668
+ documentId: DocumentId
669
+ durationMillis: number
670
+ numOps: number
671
+ numChanges: number
672
+ }
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest"
1
+ import { describe, expect, beforeEach, it as _it } from "vitest"
2
2
 
3
3
  import type { StorageAdapterInterface } from "../../storage/StorageAdapterInterface.js"
4
4
 
@@ -8,72 +8,61 @@ const PAYLOAD_C = () => new Uint8Array([2, 111, 74, 131, 236, 96, 142, 193])
8
8
 
9
9
  const LARGE_PAYLOAD = new Uint8Array(100000).map(() => Math.random() * 256)
10
10
 
11
- export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
12
- const setup = async () => {
13
- const { adapter, teardown = NO_OP } = await _setup()
14
- return { adapter, teardown }
15
- }
11
+ type AdapterTestContext = {
12
+ adapter: StorageAdapterInterface
13
+ }
14
+
15
+ const it = _it<AdapterTestContext>
16
+
17
+ export function runStorageAdapterTests(setup: SetupFn, title?: string): void {
18
+ beforeEach<AdapterTestContext>(async ctx => {
19
+ const { adapter, teardown = NO_OP } = await setup()
20
+ ctx.adapter = adapter
21
+ return teardown
22
+ })
16
23
 
17
24
  describe(`Storage adapter acceptance tests ${
18
25
  title ? `(${title})` : ""
19
26
  }`, () => {
20
27
  describe("load", () => {
21
- it("should return undefined if there is no data", async () => {
22
- const { adapter, teardown } = await setup()
23
-
28
+ it("should return undefined if there is no data", async ({ adapter }) => {
24
29
  const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
25
30
  expect(actual).toBeUndefined()
26
-
27
- teardown()
28
31
  })
29
32
  })
30
33
 
31
34
  describe("save and load", () => {
32
- it("should return data that was saved", async () => {
33
- const { adapter, teardown } = await setup()
34
-
35
+ it("should return data that was saved", async ({ adapter }) => {
35
36
  await adapter.save(["storage-adapter-id"], PAYLOAD_A())
36
37
  const actual = await adapter.load(["storage-adapter-id"])
37
38
  expect(actual).toStrictEqual(PAYLOAD_A())
38
-
39
- teardown()
40
39
  })
41
40
 
42
- it("should work with composite keys", async () => {
43
- const { adapter, teardown } = await setup()
44
-
41
+ it("should work with composite keys", async ({ adapter }) => {
45
42
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
46
43
  const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
47
44
  expect(actual).toStrictEqual(PAYLOAD_A())
48
-
49
- teardown()
50
45
  })
51
46
 
52
- it("should work with a large payload", async () => {
53
- const { adapter, teardown } = await setup()
54
-
47
+ it("should work with a large payload", async ({ adapter }) => {
55
48
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], LARGE_PAYLOAD)
56
49
  const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
57
50
  expect(actual).toStrictEqual(LARGE_PAYLOAD)
58
-
59
- teardown()
60
51
  })
61
52
  })
62
53
 
63
54
  describe("loadRange", () => {
64
- it("should return an empty array if there is no data", async () => {
65
- const { adapter, teardown } = await setup()
66
-
55
+ it("should return an empty array if there is no data", async ({
56
+ adapter,
57
+ }) => {
67
58
  expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([])
68
-
69
- teardown()
70
59
  })
71
60
  })
72
61
 
73
62
  describe("save and loadRange", () => {
74
- it("should return all the data that matches the key", async () => {
75
- const { adapter, teardown } = await setup()
76
-
63
+ it("should return all the data that matches the key", async ({
64
+ adapter,
65
+ }) => {
77
66
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
78
67
  await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B())
79
68
  await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C())
@@ -92,13 +81,9 @@ export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
92
81
  { key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
93
82
  ])
94
83
  )
95
-
96
- teardown()
97
84
  })
98
85
 
99
- it("should only load values that match they key", async () => {
100
- const { adapter, teardown } = await setup()
101
-
86
+ it("should only load values that match they key", async ({ adapter }) => {
102
87
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
103
88
  await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_C())
104
89
 
@@ -113,15 +98,11 @@ export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
113
98
  { key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_C() },
114
99
  ])
115
100
  )
116
-
117
- teardown()
118
101
  })
119
102
  })
120
103
 
121
104
  describe("save and remove", () => {
122
- it("after removing, should be empty", async () => {
123
- const { adapter, teardown } = await setup()
124
-
105
+ it("after removing, should be empty", async ({ adapter }) => {
125
106
  await adapter.save(["AAAAA", "snapshot", "xxxxx"], PAYLOAD_A())
126
107
  await adapter.remove(["AAAAA", "snapshot", "xxxxx"])
127
108
 
@@ -129,30 +110,24 @@ export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
129
110
  expect(
130
111
  await adapter.load(["AAAAA", "snapshot", "xxxxx"])
131
112
  ).toBeUndefined()
132
-
133
- teardown()
134
113
  })
135
114
  })
136
115
 
137
116
  describe("save and save", () => {
138
- it("should overwrite data saved with the same key", async () => {
139
- const { adapter, teardown } = await setup()
140
-
117
+ it("should overwrite data saved with the same key", async ({
118
+ adapter,
119
+ }) => {
141
120
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
142
121
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_B())
143
122
 
144
123
  expect(await adapter.loadRange(["AAAAA", "sync-state"])).toStrictEqual([
145
124
  { key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_B() },
146
125
  ])
147
-
148
- teardown()
149
126
  })
150
127
  })
151
128
 
152
129
  describe("removeRange", () => {
153
- it("should remove a range of records", async () => {
154
- const { adapter, teardown } = await setup()
155
-
130
+ it("should remove a range of records", async ({ adapter }) => {
156
131
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
157
132
  await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B())
158
133
  await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C())
@@ -162,13 +137,9 @@ export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
162
137
  expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([
163
138
  { key: ["AAAAA", "snapshot", "yyyyy"], data: PAYLOAD_B() },
164
139
  ])
165
-
166
- teardown()
167
140
  })
168
141
 
169
- it("should not remove records that don't match", async () => {
170
- const { adapter, teardown } = await setup()
171
-
142
+ it("should not remove records that don't match", async ({ adapter }) => {
172
143
  await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
173
144
  await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_B())
174
145
 
@@ -178,8 +149,6 @@ export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
178
149
  expect(actual).toStrictEqual([
179
150
  { key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_B() },
180
151
  ])
181
-
182
- teardown()
183
152
  })
184
153
  })
185
154
  })
@@ -189,5 +158,5 @@ const NO_OP = () => {}
189
158
 
190
159
  export type SetupFn = () => Promise<{
191
160
  adapter: StorageAdapterInterface
192
- teardown?: () => void
161
+ teardown?: () => void | Promise<void>
193
162
  }>
@@ -8,12 +8,22 @@ import { ChunkInfo, StorageKey, StorageId } from "./types.js"
8
8
  import { keyHash, headsHash } from "./keyHash.js"
9
9
  import { chunkTypeFromKey } from "./chunkTypeFromKey.js"
10
10
  import * as Uuid from "uuid"
11
+ import { EventEmitter } from "eventemitter3"
12
+
13
+ type StorageSubsystemEvents = {
14
+ "document-loaded": (arg: {
15
+ documentId: DocumentId
16
+ durationMillis: number
17
+ numOps: number
18
+ numChanges: number
19
+ }) => void
20
+ }
11
21
 
12
22
  /**
13
23
  * The storage subsystem is responsible for saving and loading Automerge documents to and from
14
24
  * storage adapter. It also provides a generic key/value storage interface for other uses.
15
25
  */
16
- export class StorageSubsystem {
26
+ export class StorageSubsystem extends EventEmitter<StorageSubsystemEvents> {
17
27
  /** The storage adapter to use for saving and loading documents */
18
28
  #storageAdapter: StorageAdapterInterface
19
29
 
@@ -29,6 +39,7 @@ export class StorageSubsystem {
29
39
  #log = debug(`automerge-repo:storage-subsystem`)
30
40
 
31
41
  constructor(storageAdapter: StorageAdapterInterface) {
42
+ super()
32
43
  this.#storageAdapter = storageAdapter
33
44
  }
34
45
 
@@ -130,7 +141,14 @@ export class StorageSubsystem {
130
141
  if (binary.length === 0) return null
131
142
 
132
143
  // Load into an Automerge document
144
+ const start = performance.now()
133
145
  const newDoc = A.loadIncremental(A.init(), binary) as A.Doc<T>
146
+ const end = performance.now()
147
+ this.emit("document-loaded", {
148
+ documentId,
149
+ durationMillis: end - start,
150
+ ...A.stats(newDoc),
151
+ })
134
152
 
135
153
  // Record the latest heads for the document
136
154
  this.#storedHeads.set(documentId, A.getHeads(newDoc))
@@ -58,6 +58,7 @@ export class CollectionSynchronizer extends Synchronizer {
58
58
  docSynchronizer.on("message", event => this.emit("message", event))
59
59
  docSynchronizer.on("open-doc", event => this.emit("open-doc", event))
60
60
  docSynchronizer.on("sync-state", event => this.emit("sync-state", event))
61
+ docSynchronizer.on("metrics", event => this.emit("metrics", event))
61
62
  return docSynchronizer
62
63
  }
63
64
 
@@ -351,11 +351,19 @@ export class DocSynchronizer extends Synchronizer {
351
351
 
352
352
  this.#withSyncState(message.senderId, syncState => {
353
353
  this.#handle.update(doc => {
354
+ const start = performance.now()
354
355
  const [newDoc, newSyncState] = A.receiveSyncMessage(
355
356
  doc,
356
357
  syncState,
357
358
  message.data
358
359
  )
360
+ const end = performance.now()
361
+ this.emit("metrics", {
362
+ type: "receive-sync-message",
363
+ documentId: this.#handle.documentId,
364
+ durationMillis: end - start,
365
+ ...A.stats(doc),
366
+ })
359
367
 
360
368
  this.#setSyncState(message.senderId, newSyncState)
361
369
 
@@ -15,6 +15,7 @@ export interface SynchronizerEvents {
15
15
  message: (payload: MessageContents) => void
16
16
  "sync-state": (payload: SyncStatePayload) => void
17
17
  "open-doc": (arg: OpenDocMessage) => void
18
+ metrics: (arg: DocSyncMetrics) => void
18
19
  }
19
20
 
20
21
  /** Notify the repo that the sync state has changed */
@@ -23,3 +24,11 @@ export interface SyncStatePayload {
23
24
  documentId: DocumentId
24
25
  syncState: SyncState
25
26
  }
27
+
28
+ export type DocSyncMetrics = {
29
+ type: "receive-sync-message"
30
+ documentId: DocumentId
31
+ durationMillis: number
32
+ numOps: number
33
+ numChanges: number
34
+ }
@@ -7,6 +7,7 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
7
7
  import { pause } from "../src/helpers/pause.js"
8
8
  import { DocHandle, DocHandleChangePayload } from "../src/index.js"
9
9
  import { TestDoc } from "./types.js"
10
+ import { UNLOADED } from "../src/DocHandle.js"
10
11
 
11
12
  describe("DocHandle", () => {
12
13
  const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
@@ -422,6 +423,49 @@ describe("DocHandle", () => {
422
423
  assert.equal(handle.isDeleted(), true)
423
424
  })
424
425
 
426
+ it("should clear document reference when unloaded", async () => {
427
+ const handle = setup()
428
+
429
+ handle.change(doc => {
430
+ doc.foo = "bar"
431
+ })
432
+ const doc = await handle.doc()
433
+ assert.equal(doc?.foo, "bar")
434
+
435
+ handle.unload()
436
+ assert.equal(handle.isUnloaded(), true)
437
+
438
+ const clearedDoc = await handle.doc([UNLOADED])
439
+ assert.notEqual(clearedDoc?.foo, "bar")
440
+ })
441
+
442
+ it("should allow reloading after unloading", async () => {
443
+ const handle = setup()
444
+
445
+ handle.change(doc => {
446
+ doc.foo = "bar"
447
+ })
448
+ const doc = await handle.doc()
449
+ assert.equal(doc?.foo, "bar")
450
+
451
+ handle.unload()
452
+
453
+ // reload to transition from unloaded to loading
454
+ handle.reload()
455
+
456
+ // simulate requesting from the network
457
+ handle.request()
458
+
459
+ // simulate updating from the network
460
+ handle.update(doc => {
461
+ return A.change(doc, d => (d.foo = "bar"))
462
+ })
463
+
464
+ const reloadedDoc = await handle.doc()
465
+ assert.equal(handle.isReady(), true)
466
+ assert.equal(reloadedDoc?.foo, "bar")
467
+ })
468
+
425
469
  it("should allow changing at old heads", async () => {
426
470
  const handle = setup()
427
471
 
package/test/Repo.test.ts CHANGED
@@ -486,6 +486,39 @@ describe("Repo", () => {
486
486
  const doc = await handle.doc()
487
487
  expect(doc).toEqual({})
488
488
  })
489
+
490
+ describe("handle cache", () => {
491
+ it("contains doc handle", async () => {
492
+ const { repo } = setup()
493
+ const handle = repo.create({ foo: "bar" })
494
+ await handle.doc()
495
+ assert(repo.handles[handle.documentId])
496
+ })
497
+
498
+ it("delete removes doc handle", async () => {
499
+ const { repo } = setup()
500
+ const handle = repo.create({ foo: "bar" })
501
+ await handle.doc()
502
+ await repo.delete(handle.documentId)
503
+ assert(repo.handles[handle.documentId] === undefined)
504
+ })
505
+
506
+ it("removeFromCache removes doc handle", async () => {
507
+ const { repo } = setup()
508
+ const handle = repo.create({ foo: "bar" })
509
+ await handle.doc()
510
+ await repo.removeFromCache(handle.documentId)
511
+ assert(repo.handles[handle.documentId] === undefined)
512
+ })
513
+
514
+ it("removeFromCache for documentId not found", async () => {
515
+ const { repo } = setup()
516
+ const badDocumentId = "badbadbad" as DocumentId
517
+ const handleCacheSize = Object.keys(repo.handles).length
518
+ await repo.removeFromCache(badDocumentId)
519
+ assert(Object.keys(repo.handles).length === handleCacheSize)
520
+ })
521
+ })
489
522
  })
490
523
 
491
524
  describe("flush behaviour", () => {