@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.
- package/dist/DocHandle.d.ts +16 -2
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +34 -2
- package/dist/Repo.d.ts +16 -0
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +34 -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 +19 -39
- package/dist/storage/StorageSubsystem.d.ts +11 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +10 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +1 -0
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +8 -0
- package/dist/synchronizer/Synchronizer.d.ts +8 -0
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/DocHandle.ts +49 -4
- package/src/Repo.ts +65 -2
- package/src/helpers/tests/storage-adapter-tests.ts +31 -62
- package/src/storage/StorageSubsystem.ts +19 -1
- package/src/synchronizer/CollectionSynchronizer.ts +1 -0
- package/src/synchronizer/DocSynchronizer.ts +8 -0
- package/src/synchronizer/Synchronizer.ts +9 -0
- package/test/DocHandle.test.ts +44 -0
- package/test/Repo.test.ts +33 -0
package/dist/DocHandle.d.ts
CHANGED
|
@@ -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
|
package/dist/DocHandle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"DocHandle.d.ts","sourceRoot":"","sources":["../src/DocHandle.ts"],"names":[],"mappings":"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"}
|
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
|
|
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
|
package/dist/Repo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAM5C,OAAO,
|
|
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(
|
|
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;
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
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.
|
|
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": "
|
|
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
|
|
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 {
|
|
638
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/test/DocHandle.test.ts
CHANGED
|
@@ -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", () => {
|