@automerge/automerge-repo 2.0.0-alpha.20 → 2.0.0-alpha.23
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/README.md +8 -8
- package/dist/DocHandle.d.ts +10 -22
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +21 -51
- package/dist/FindProgress.d.ts +30 -0
- package/dist/FindProgress.d.ts.map +1 -0
- package/dist/FindProgress.js +1 -0
- package/dist/Repo.d.ts +9 -4
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +166 -69
- package/dist/helpers/abortable.d.ts +39 -0
- package/dist/helpers/abortable.d.ts.map +1 -0
- package/dist/helpers/abortable.js +45 -0
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +13 -13
- package/dist/synchronizer/CollectionSynchronizer.d.ts +2 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +18 -14
- package/dist/synchronizer/DocSynchronizer.d.ts +3 -2
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +43 -27
- package/fuzz/fuzz.ts +3 -3
- package/package.json +3 -4
- package/src/DocHandle.ts +23 -51
- package/src/FindProgress.ts +48 -0
- package/src/Repo.ts +187 -67
- package/src/helpers/abortable.ts +61 -0
- package/src/helpers/tests/network-adapter-tests.ts +14 -13
- package/src/synchronizer/CollectionSynchronizer.ts +18 -14
- package/src/synchronizer/DocSynchronizer.ts +51 -32
- package/test/CollectionSynchronizer.test.ts +4 -4
- package/test/DocHandle.test.ts +25 -74
- package/test/Repo.test.ts +169 -216
- package/test/remoteHeads.test.ts +27 -12
package/dist/Repo.js
CHANGED
|
@@ -9,6 +9,7 @@ import { throttle } from "./helpers/throttle.js";
|
|
|
9
9
|
import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
|
|
10
10
|
import { StorageSubsystem } from "./storage/StorageSubsystem.js";
|
|
11
11
|
import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
|
|
12
|
+
import { abortable } from "./helpers/abortable.js";
|
|
12
13
|
function randomPeerId() {
|
|
13
14
|
return ("peer-" + Math.random().toString(36).slice(4));
|
|
14
15
|
}
|
|
@@ -170,16 +171,8 @@ export class Repo extends EventEmitter {
|
|
|
170
171
|
};
|
|
171
172
|
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate));
|
|
172
173
|
}
|
|
173
|
-
handle.on("unavailable", () => {
|
|
174
|
-
this.#log("document unavailable", { documentId: handle.documentId });
|
|
175
|
-
this.emit("unavailable-document", {
|
|
176
|
-
documentId: handle.documentId,
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
174
|
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
180
|
-
this.synchronizer.addDocument(handle
|
|
181
|
-
// Preserve the old event in case anyone was using it.
|
|
182
|
-
this.emit("document", { handle });
|
|
175
|
+
this.synchronizer.addDocument(handle);
|
|
183
176
|
}
|
|
184
177
|
#receiveMessage(message) {
|
|
185
178
|
switch (message.type) {
|
|
@@ -280,18 +273,13 @@ export class Repo extends EventEmitter {
|
|
|
280
273
|
* Any peers this `Repo` is connected to for whom `sharePolicy` returns `true` will
|
|
281
274
|
* be notified of the newly created DocHandle.
|
|
282
275
|
*
|
|
283
|
-
* @throws if the cloned handle is not yet ready or if
|
|
284
|
-
* `clonedHandle.docSync()` returns `undefined` (i.e. the handle is unavailable).
|
|
285
276
|
*/
|
|
286
277
|
clone(clonedHandle) {
|
|
287
278
|
if (!clonedHandle.isReady()) {
|
|
288
279
|
throw new Error(`Cloned handle is not yet in ready state.
|
|
289
280
|
(Try await handle.whenReady() first.)`);
|
|
290
281
|
}
|
|
291
|
-
const sourceDoc = clonedHandle.
|
|
292
|
-
if (!sourceDoc) {
|
|
293
|
-
throw new Error("Cloned handle doesn't have a document.");
|
|
294
|
-
}
|
|
282
|
+
const sourceDoc = clonedHandle.doc();
|
|
295
283
|
const handle = this.create();
|
|
296
284
|
handle.update(() => {
|
|
297
285
|
// we replace the document with the new cloned one
|
|
@@ -299,58 +287,171 @@ export class Repo extends EventEmitter {
|
|
|
299
287
|
});
|
|
300
288
|
return handle;
|
|
301
289
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
*/
|
|
306
|
-
find(
|
|
307
|
-
/** The url or documentId of the handle to retrieve */
|
|
308
|
-
id) {
|
|
290
|
+
findWithProgress(id, options = {}) {
|
|
291
|
+
const { signal } = options;
|
|
292
|
+
const abortPromise = abortable(signal);
|
|
309
293
|
const { documentId, heads } = isValidAutomergeUrl(id)
|
|
310
294
|
? parseAutomergeUrl(id)
|
|
311
295
|
: { documentId: interpretAsDocumentId(id), heads: undefined };
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
296
|
+
// Check cache first - return plain FindStep for terminal states
|
|
297
|
+
if (this.#handleCache[documentId]) {
|
|
298
|
+
const handle = this.#handleCache[documentId];
|
|
299
|
+
if (handle.state === UNAVAILABLE) {
|
|
300
|
+
const result = {
|
|
301
|
+
state: "unavailable",
|
|
302
|
+
error: new Error(`Document ${id} is unavailable`),
|
|
303
|
+
handle,
|
|
304
|
+
};
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
if (handle.state === DELETED) {
|
|
308
|
+
return {
|
|
309
|
+
state: "failed",
|
|
310
|
+
error: new Error(`Document ${id} was deleted`),
|
|
311
|
+
handle,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (handle.state === READY) {
|
|
315
|
+
// If we already have the handle, return it immediately (or a view of the handle if heads are specified)
|
|
316
|
+
return {
|
|
317
|
+
state: "ready",
|
|
318
|
+
// TODO: this handle needs to be cached (or at least avoid running clone)
|
|
319
|
+
handle: heads ? handle.view(heads) : handle,
|
|
320
|
+
};
|
|
321
321
|
}
|
|
322
|
-
// If we already have the handle, return it immediately (or a view of the handle if heads are specified)
|
|
323
|
-
return heads ? cachedHandle.view(heads) : cachedHandle;
|
|
324
322
|
}
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
323
|
+
// the generator takes over `this`, so we need an alias to the repo this
|
|
324
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
325
|
+
const that = this;
|
|
326
|
+
async function* progressGenerator() {
|
|
327
|
+
try {
|
|
328
|
+
const handle = that.#getHandle({ documentId });
|
|
329
|
+
yield { state: "loading", progress: 25, handle };
|
|
330
|
+
const loadingPromise = await (that.storageSubsystem
|
|
331
|
+
? that.storageSubsystem.loadDoc(handle.documentId)
|
|
332
|
+
: Promise.resolve(null));
|
|
333
|
+
const loadedDoc = await Promise.race([loadingPromise, abortPromise]);
|
|
334
|
+
if (loadedDoc) {
|
|
335
|
+
handle.update(() => loadedDoc);
|
|
336
|
+
handle.doneLoading();
|
|
337
|
+
yield { state: "loading", progress: 50, handle };
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
await Promise.race([that.networkSubsystem.whenReady(), abortPromise]);
|
|
341
|
+
handle.request();
|
|
342
|
+
yield { state: "loading", progress: 75, handle };
|
|
343
|
+
}
|
|
344
|
+
that.#registerHandleWithSubsystems(handle);
|
|
345
|
+
await Promise.race([
|
|
346
|
+
handle.whenReady([READY, UNAVAILABLE]),
|
|
347
|
+
abortPromise,
|
|
348
|
+
]);
|
|
349
|
+
if (handle.state === UNAVAILABLE) {
|
|
350
|
+
yield { state: "unavailable", handle };
|
|
351
|
+
}
|
|
352
|
+
if (handle.state === DELETED) {
|
|
353
|
+
throw new Error(`Document ${id} was deleted`);
|
|
354
|
+
}
|
|
355
|
+
yield { state: "ready", handle };
|
|
340
356
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
357
|
+
catch (error) {
|
|
358
|
+
yield {
|
|
359
|
+
state: "failed",
|
|
360
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
361
|
+
handle,
|
|
362
|
+
};
|
|
346
363
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
}
|
|
365
|
+
const iterator = progressGenerator();
|
|
366
|
+
const next = async () => {
|
|
367
|
+
const result = await iterator.next();
|
|
368
|
+
return { ...result.value, next };
|
|
369
|
+
};
|
|
370
|
+
const untilReady = async (allowableStates) => {
|
|
371
|
+
for await (const state of iterator) {
|
|
372
|
+
if (allowableStates.includes(state.handle.state)) {
|
|
373
|
+
return state.handle;
|
|
374
|
+
}
|
|
375
|
+
if (state.state === "unavailable") {
|
|
376
|
+
throw new Error(`Document ${id} is unavailable`);
|
|
377
|
+
}
|
|
378
|
+
if (state.state === "ready")
|
|
379
|
+
return state.handle;
|
|
380
|
+
if (state.state === "failed")
|
|
381
|
+
throw state.error;
|
|
382
|
+
}
|
|
383
|
+
throw new Error("Iterator completed without reaching ready state");
|
|
384
|
+
};
|
|
385
|
+
const handle = this.#getHandle({ documentId });
|
|
386
|
+
const initial = { state: "loading", progress: 0, handle };
|
|
387
|
+
return { ...initial, next, untilReady };
|
|
388
|
+
}
|
|
389
|
+
async find(id, options = {}) {
|
|
390
|
+
const { allowableStates = ["ready"], signal } = options;
|
|
391
|
+
const progress = this.findWithProgress(id, { signal });
|
|
392
|
+
/*if (allowableStates.includes(progress.state)) {
|
|
393
|
+
console.log("returning early")
|
|
394
|
+
return progress.handle
|
|
395
|
+
}*/
|
|
396
|
+
if ("untilReady" in progress) {
|
|
397
|
+
this.#registerHandleWithSubsystems(progress.handle);
|
|
398
|
+
return progress.untilReady(allowableStates);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
return progress.handle;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Loads a document without waiting for ready state
|
|
406
|
+
*/
|
|
407
|
+
async #loadDocument(documentId) {
|
|
408
|
+
// If we have the handle cached, return it
|
|
409
|
+
if (this.#handleCache[documentId]) {
|
|
410
|
+
return this.#handleCache[documentId];
|
|
411
|
+
}
|
|
412
|
+
// If we don't already have the handle, make an empty one and try loading it
|
|
413
|
+
const handle = this.#getHandle({ documentId });
|
|
414
|
+
const loadedDoc = await (this.storageSubsystem
|
|
415
|
+
? this.storageSubsystem.loadDoc(handle.documentId)
|
|
416
|
+
: Promise.resolve(null));
|
|
417
|
+
if (loadedDoc) {
|
|
418
|
+
// We need to cast this to <T> because loadDoc operates in <unknowns>.
|
|
419
|
+
// This is really where we ought to be validating the input matches <T>.
|
|
420
|
+
handle.update(() => loadedDoc);
|
|
421
|
+
handle.doneLoading();
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Because the network subsystem might still be booting up, we wait
|
|
425
|
+
// here so that we don't immediately give up loading because we're still
|
|
426
|
+
// making our initial connection to a sync server.
|
|
427
|
+
await this.networkSubsystem.whenReady();
|
|
428
|
+
handle.request();
|
|
429
|
+
}
|
|
430
|
+
this.#registerHandleWithSubsystems(handle);
|
|
431
|
+
return handle;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Retrieves a document by id. It gets data from the local system, but also emits a `document`
|
|
435
|
+
* event to advertise interest in the document.
|
|
436
|
+
*/
|
|
437
|
+
async findClassic(
|
|
438
|
+
/** The url or documentId of the handle to retrieve */
|
|
439
|
+
id, options = {}) {
|
|
440
|
+
const documentId = interpretAsDocumentId(id);
|
|
441
|
+
const { allowableStates, signal } = options;
|
|
442
|
+
return Promise.race([
|
|
443
|
+
(async () => {
|
|
444
|
+
const handle = await this.#loadDocument(documentId);
|
|
445
|
+
if (!allowableStates) {
|
|
446
|
+
await handle.whenReady([READY, UNAVAILABLE]);
|
|
447
|
+
if (handle.state === UNAVAILABLE && !signal?.aborted) {
|
|
448
|
+
throw new Error(`Document ${id} is unavailable`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return handle;
|
|
452
|
+
})(),
|
|
453
|
+
abortable(signal),
|
|
454
|
+
]);
|
|
354
455
|
}
|
|
355
456
|
delete(
|
|
356
457
|
/** The url or documentId of the handle to delete */
|
|
@@ -371,9 +472,7 @@ export class Repo extends EventEmitter {
|
|
|
371
472
|
async export(id) {
|
|
372
473
|
const documentId = interpretAsDocumentId(id);
|
|
373
474
|
const handle = this.#getHandle({ documentId });
|
|
374
|
-
const doc =
|
|
375
|
-
if (!doc)
|
|
376
|
-
return undefined;
|
|
475
|
+
const doc = handle.doc();
|
|
377
476
|
return Automerge.save(doc);
|
|
378
477
|
}
|
|
379
478
|
/**
|
|
@@ -419,11 +518,7 @@ export class Repo extends EventEmitter {
|
|
|
419
518
|
? documents.map(id => this.#handleCache[id])
|
|
420
519
|
: Object.values(this.#handleCache);
|
|
421
520
|
await Promise.all(handles.map(async (handle) => {
|
|
422
|
-
|
|
423
|
-
if (!doc) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
return this.storageSubsystem.saveDoc(handle.documentId, doc);
|
|
521
|
+
return this.storageSubsystem.saveDoc(handle.documentId, handle.doc());
|
|
427
522
|
}));
|
|
428
523
|
}
|
|
429
524
|
/**
|
|
@@ -438,7 +533,9 @@ export class Repo extends EventEmitter {
|
|
|
438
533
|
return;
|
|
439
534
|
}
|
|
440
535
|
const handle = this.#getHandle({ documentId });
|
|
441
|
-
|
|
536
|
+
await handle.whenReady([READY, UNLOADED, DELETED, UNAVAILABLE]);
|
|
537
|
+
const doc = handle.doc();
|
|
538
|
+
// because this is an internal-ish function, we'll be extra careful about undefined docs here
|
|
442
539
|
if (doc) {
|
|
443
540
|
if (handle.isReady()) {
|
|
444
541
|
handle.unload();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a promise that rejects when the signal is aborted.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This utility creates a promise that rejects when the provided AbortSignal is aborted.
|
|
6
|
+
* It's designed to be used with Promise.race() to make operations abortable.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const controller = new AbortController();
|
|
11
|
+
*
|
|
12
|
+
* try {
|
|
13
|
+
* const result = await Promise.race([
|
|
14
|
+
* fetch('https://api.example.com/data'),
|
|
15
|
+
* abortable(controller.signal)
|
|
16
|
+
* ]);
|
|
17
|
+
* } catch (err) {
|
|
18
|
+
* if (err.name === 'AbortError') {
|
|
19
|
+
* console.log('The operation was aborted');
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* // Later, to abort:
|
|
24
|
+
* controller.abort();
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param signal - An AbortSignal that can be used to abort the operation
|
|
28
|
+
* @param cleanup - Optional cleanup function that will be called if aborted
|
|
29
|
+
* @returns A promise that rejects with AbortError when the signal is aborted
|
|
30
|
+
* @throws {DOMException} With name "AbortError" when aborted
|
|
31
|
+
*/
|
|
32
|
+
export declare function abortable(signal?: AbortSignal, cleanup?: () => void): Promise<never>;
|
|
33
|
+
/**
|
|
34
|
+
* Include this type in an options object to pass an AbortSignal to a function.
|
|
35
|
+
*/
|
|
36
|
+
export interface AbortOptions {
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=abortable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abortable.d.ts","sourceRoot":"","sources":["../../src/helpers/abortable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,SAAS,CACvB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,MAAM,IAAI,GACnB,OAAO,CAAC,KAAK,CAAC,CAmBhB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a promise that rejects when the signal is aborted.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This utility creates a promise that rejects when the provided AbortSignal is aborted.
|
|
6
|
+
* It's designed to be used with Promise.race() to make operations abortable.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const controller = new AbortController();
|
|
11
|
+
*
|
|
12
|
+
* try {
|
|
13
|
+
* const result = await Promise.race([
|
|
14
|
+
* fetch('https://api.example.com/data'),
|
|
15
|
+
* abortable(controller.signal)
|
|
16
|
+
* ]);
|
|
17
|
+
* } catch (err) {
|
|
18
|
+
* if (err.name === 'AbortError') {
|
|
19
|
+
* console.log('The operation was aborted');
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* // Later, to abort:
|
|
24
|
+
* controller.abort();
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param signal - An AbortSignal that can be used to abort the operation
|
|
28
|
+
* @param cleanup - Optional cleanup function that will be called if aborted
|
|
29
|
+
* @returns A promise that rejects with AbortError when the signal is aborted
|
|
30
|
+
* @throws {DOMException} With name "AbortError" when aborted
|
|
31
|
+
*/
|
|
32
|
+
export function abortable(signal, cleanup) {
|
|
33
|
+
if (signal?.aborted) {
|
|
34
|
+
throw new DOMException("Operation aborted", "AbortError");
|
|
35
|
+
}
|
|
36
|
+
if (!signal) {
|
|
37
|
+
return new Promise(() => { }); // Never resolves
|
|
38
|
+
}
|
|
39
|
+
return new Promise((_, reject) => {
|
|
40
|
+
signal.addEventListener("abort", () => {
|
|
41
|
+
cleanup?.();
|
|
42
|
+
reject(new DOMException("Operation aborted", "AbortError"));
|
|
43
|
+
}, { once: true });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network-adapter-tests.d.ts","sourceRoot":"","sources":["../../../src/helpers/tests/network-adapter-tests.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAA;AAIvF;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"network-adapter-tests.d.ts","sourceRoot":"","sources":["../../../src/helpers/tests/network-adapter-tests.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAA;AAIvF;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CA2Q5E;AAID,KAAK,OAAO,GAAG,uBAAuB,GAAG,uBAAuB,EAAE,CAAA;AAElE,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;IAClC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB,CAAC,CAAA"}
|
|
@@ -30,23 +30,23 @@ export function runNetworkAdapterTests(_setup, title) {
|
|
|
30
30
|
const bobRepo = new Repo({ network: b, peerId: bob });
|
|
31
31
|
// Alice creates a document
|
|
32
32
|
const aliceHandle = aliceRepo.create();
|
|
33
|
-
//
|
|
34
|
-
await
|
|
35
|
-
const bobHandle = bobRepo.find(aliceHandle.url);
|
|
33
|
+
// TODO: ... let connections complete. this shouldn't be necessary.
|
|
34
|
+
await pause(50);
|
|
35
|
+
const bobHandle = await bobRepo.find(aliceHandle.url);
|
|
36
36
|
// Alice changes the document
|
|
37
37
|
aliceHandle.change(d => {
|
|
38
38
|
d.foo = "bar";
|
|
39
39
|
});
|
|
40
40
|
// Bob receives the change
|
|
41
41
|
await eventPromise(bobHandle, "change");
|
|
42
|
-
assert.equal((await bobHandle.doc()
|
|
42
|
+
assert.equal((await bobHandle).doc()?.foo, "bar");
|
|
43
43
|
// Bob changes the document
|
|
44
44
|
bobHandle.change(d => {
|
|
45
45
|
d.foo = "baz";
|
|
46
46
|
});
|
|
47
47
|
// Alice receives the change
|
|
48
48
|
await eventPromise(aliceHandle, "change");
|
|
49
|
-
assert.equal(
|
|
49
|
+
assert.equal(aliceHandle.doc().foo, "baz");
|
|
50
50
|
};
|
|
51
51
|
// Run the test in both directions, in case they're different types of adapters
|
|
52
52
|
{
|
|
@@ -72,25 +72,25 @@ export function runNetworkAdapterTests(_setup, title) {
|
|
|
72
72
|
const aliceHandle = aliceRepo.create();
|
|
73
73
|
const docUrl = aliceHandle.url;
|
|
74
74
|
// Bob and Charlie receive the document
|
|
75
|
-
await
|
|
76
|
-
const bobHandle = bobRepo.find(docUrl);
|
|
77
|
-
const charlieHandle = charlieRepo.find(docUrl);
|
|
75
|
+
await pause(50);
|
|
76
|
+
const bobHandle = await bobRepo.find(docUrl);
|
|
77
|
+
const charlieHandle = await charlieRepo.find(docUrl);
|
|
78
78
|
// Alice changes the document
|
|
79
79
|
aliceHandle.change(d => {
|
|
80
80
|
d.foo = "bar";
|
|
81
81
|
});
|
|
82
82
|
// Bob and Charlie receive the change
|
|
83
83
|
await eventPromises([bobHandle, charlieHandle], "change");
|
|
84
|
-
assert.equal(
|
|
85
|
-
assert.equal(
|
|
84
|
+
assert.equal(bobHandle.doc().foo, "bar");
|
|
85
|
+
assert.equal(charlieHandle.doc().foo, "bar");
|
|
86
86
|
// Charlie changes the document
|
|
87
87
|
charlieHandle.change(d => {
|
|
88
88
|
d.foo = "baz";
|
|
89
89
|
});
|
|
90
90
|
// Alice and Bob receive the change
|
|
91
91
|
await eventPromises([aliceHandle, bobHandle], "change");
|
|
92
|
-
assert.equal(
|
|
93
|
-
assert.equal(
|
|
92
|
+
assert.equal(bobHandle.doc().foo, "baz");
|
|
93
|
+
assert.equal(charlieHandle.doc().foo, "baz");
|
|
94
94
|
teardown();
|
|
95
95
|
});
|
|
96
96
|
it("can broadcast a message", async () => {
|
|
@@ -101,7 +101,7 @@ export function runNetworkAdapterTests(_setup, title) {
|
|
|
101
101
|
const charlieRepo = new Repo({ network: c, peerId: charlie });
|
|
102
102
|
await eventPromises([aliceRepo, bobRepo, charlieRepo].map(r => r.networkSubsystem), "peer");
|
|
103
103
|
const aliceHandle = aliceRepo.create();
|
|
104
|
-
const charlieHandle = charlieRepo.find(aliceHandle.url);
|
|
104
|
+
const charlieHandle = await charlieRepo.find(aliceHandle.url);
|
|
105
105
|
// pause to give charlie a chance to let alice know it wants the doc
|
|
106
106
|
await pause(100);
|
|
107
107
|
const alicePresenceData = { presence: "alice" };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DocHandle } from "../DocHandle.js";
|
|
1
2
|
import { Repo } from "../Repo.js";
|
|
2
3
|
import { DocMessage } from "../network/messages.js";
|
|
3
4
|
import { AutomergeUrl, DocumentId, PeerId } from "../types.js";
|
|
@@ -19,7 +20,7 @@ export declare class CollectionSynchronizer extends Synchronizer {
|
|
|
19
20
|
/**
|
|
20
21
|
* Starts synchronizing the given document with all peers that we share it generously with.
|
|
21
22
|
*/
|
|
22
|
-
addDocument(
|
|
23
|
+
addDocument(handle: DocHandle<unknown>): void;
|
|
23
24
|
removeDocument(documentId: DocumentId): void;
|
|
24
25
|
/** Adds a peer and maybe starts synchronizing with them */
|
|
25
26
|
addPeer(peerId: PeerId): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CollectionSynchronizer.d.ts","sourceRoot":"","sources":["../../src/synchronizer/CollectionSynchronizer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIhD,4FAA4F;AAC5F,qBAAa,sBAAuB,SAAQ,YAAY;;IAa1C,OAAO,CAAC,IAAI;IATxB,kDAAkD;IAClD,cAAc;IACd,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAK;gBAOtC,IAAI,EAAE,IAAI,EAAE,QAAQ,GAAE,YAAY,EAAO;IAwD7D;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,UAAU;IAyCxC;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC;IAatC,cAAc,CAAC,UAAU,EAAE,UAAU;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM;IAgBtB,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM;IASzB,+CAA+C;IAC/C,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED,OAAO,IAAI;QACT,CAAC,GAAG,EAAE,MAAM,GAAG;YACb,KAAK,EAAE,MAAM,EAAE,CAAA;YACf,IAAI,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,UAAU,EAAE,MAAM,CAAA;aAAE,CAAA;SAC7C,CAAA;KACF;CASF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import debug from "debug";
|
|
2
|
-
import { parseAutomergeUrl
|
|
2
|
+
import { parseAutomergeUrl } from "../AutomergeUrl.js";
|
|
3
3
|
import { DocSynchronizer } from "./DocSynchronizer.js";
|
|
4
4
|
import { Synchronizer } from "./Synchronizer.js";
|
|
5
5
|
const log = debug("automerge-repo:collectionsync");
|
|
@@ -20,17 +20,18 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
20
20
|
this.#denylist = denylist.map(url => parseAutomergeUrl(url).documentId);
|
|
21
21
|
}
|
|
22
22
|
/** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
|
|
23
|
-
#fetchDocSynchronizer(
|
|
24
|
-
if (!this.docSynchronizers[documentId]) {
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
#fetchDocSynchronizer(handle) {
|
|
24
|
+
if (!this.docSynchronizers[handle.documentId]) {
|
|
25
|
+
this.docSynchronizers[handle.documentId] =
|
|
26
|
+
this.#initDocSynchronizer(handle);
|
|
27
27
|
}
|
|
28
|
-
return this.docSynchronizers[documentId];
|
|
28
|
+
return this.docSynchronizers[handle.documentId];
|
|
29
29
|
}
|
|
30
30
|
/** Creates a new docSynchronizer and sets it up to propagate messages */
|
|
31
31
|
#initDocSynchronizer(handle) {
|
|
32
32
|
const docSynchronizer = new DocSynchronizer({
|
|
33
33
|
handle,
|
|
34
|
+
peerId: this.repo.networkSubsystem.peerId,
|
|
34
35
|
onLoadSyncState: async (peerId) => {
|
|
35
36
|
if (!this.repo.storageSubsystem) {
|
|
36
37
|
return;
|
|
@@ -83,23 +84,26 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
this.#docSetUp[documentId] = true;
|
|
86
|
-
const
|
|
87
|
+
const handle = await this.repo.find(documentId, {
|
|
88
|
+
allowableStates: ["ready", "unavailable", "requesting"],
|
|
89
|
+
});
|
|
90
|
+
const docSynchronizer = this.#fetchDocSynchronizer(handle);
|
|
87
91
|
docSynchronizer.receiveMessage(message);
|
|
88
92
|
// Initiate sync with any new peers
|
|
89
93
|
const peers = await this.#documentGenerousPeers(documentId);
|
|
90
|
-
docSynchronizer.beginSync(peers.filter(peerId => !docSynchronizer.hasPeer(peerId)));
|
|
94
|
+
void docSynchronizer.beginSync(peers.filter(peerId => !docSynchronizer.hasPeer(peerId)));
|
|
91
95
|
}
|
|
92
96
|
/**
|
|
93
97
|
* Starts synchronizing the given document with all peers that we share it generously with.
|
|
94
98
|
*/
|
|
95
|
-
addDocument(
|
|
99
|
+
addDocument(handle) {
|
|
96
100
|
// HACK: this is a hack to prevent us from adding the same document twice
|
|
97
|
-
if (this.#docSetUp[documentId]) {
|
|
101
|
+
if (this.#docSetUp[handle.documentId]) {
|
|
98
102
|
return;
|
|
99
103
|
}
|
|
100
|
-
const docSynchronizer = this.#fetchDocSynchronizer(
|
|
101
|
-
void this.#documentGenerousPeers(documentId).then(peers => {
|
|
102
|
-
docSynchronizer.beginSync(peers);
|
|
104
|
+
const docSynchronizer = this.#fetchDocSynchronizer(handle);
|
|
105
|
+
void this.#documentGenerousPeers(handle.documentId).then(peers => {
|
|
106
|
+
void docSynchronizer.beginSync(peers);
|
|
103
107
|
});
|
|
104
108
|
}
|
|
105
109
|
// TODO: implement this
|
|
@@ -118,7 +122,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
118
122
|
const { documentId } = docSynchronizer;
|
|
119
123
|
void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
120
124
|
if (okToShare)
|
|
121
|
-
docSynchronizer.beginSync([peerId]);
|
|
125
|
+
void docSynchronizer.beginSync([peerId]);
|
|
122
126
|
});
|
|
123
127
|
}
|
|
124
128
|
}
|
|
@@ -6,6 +6,7 @@ import { Synchronizer } from "./Synchronizer.js";
|
|
|
6
6
|
type PeerDocumentStatus = "unknown" | "has" | "unavailable" | "wants";
|
|
7
7
|
interface DocSynchronizerConfig {
|
|
8
8
|
handle: DocHandle<unknown>;
|
|
9
|
+
peerId: PeerId;
|
|
9
10
|
onLoadSyncState?: (peerId: PeerId) => Promise<A.SyncState | undefined>;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
@@ -15,11 +16,11 @@ interface DocSynchronizerConfig {
|
|
|
15
16
|
export declare class DocSynchronizer extends Synchronizer {
|
|
16
17
|
#private;
|
|
17
18
|
syncDebounceRate: number;
|
|
18
|
-
constructor({ handle, onLoadSyncState }: DocSynchronizerConfig);
|
|
19
|
+
constructor({ handle, peerId, onLoadSyncState }: DocSynchronizerConfig);
|
|
19
20
|
get peerStates(): Record<PeerId, PeerDocumentStatus>;
|
|
20
21
|
get documentId(): import("../types.js").DocumentId;
|
|
21
22
|
hasPeer(peerId: PeerId): boolean;
|
|
22
|
-
beginSync(peerIds: PeerId[]): void
|
|
23
|
+
beginSync(peerIds: PeerId[]): Promise<void>;
|
|
23
24
|
endSync(peerId: PeerId): void;
|
|
24
25
|
receiveMessage(message: RepoMessage): void;
|
|
25
26
|
receiveEphemeralMessage(message: EphemeralMessage): void;
|
|
@@ -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;
|
|
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,MAAM,EAAE,MAAM,CAAA;IACd,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;gBAyBV,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE,qBAAqB;IAyBtE,IAAI,UAAU,uCAEb;IAED,IAAI,UAAU,qCAEb;IAqID,OAAO,CAAC,MAAM,EAAE,MAAM;IAIhB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE;IA8DjC,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;IAwFxD,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"}
|