@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
|
@@ -19,11 +19,15 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
19
19
|
/** Sync state for each peer we've communicated with (including inactive peers) */
|
|
20
20
|
#syncStates = {};
|
|
21
21
|
#pendingSyncMessages = [];
|
|
22
|
+
// We keep this around at least in part for debugging.
|
|
23
|
+
// eslint-disable-next-line no-unused-private-class-members
|
|
24
|
+
#peerId;
|
|
22
25
|
#syncStarted = false;
|
|
23
26
|
#handle;
|
|
24
27
|
#onLoadSyncState;
|
|
25
|
-
constructor({ handle, onLoadSyncState }) {
|
|
28
|
+
constructor({ handle, peerId, onLoadSyncState }) {
|
|
26
29
|
super();
|
|
30
|
+
this.#peerId = peerId;
|
|
27
31
|
this.#handle = handle;
|
|
28
32
|
this.#onLoadSyncState =
|
|
29
33
|
onLoadSyncState ?? (() => Promise.resolve(undefined));
|
|
@@ -33,7 +37,6 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
33
37
|
handle.on("ephemeral-message-outbound", payload => this.#broadcastToPeers(payload));
|
|
34
38
|
// Process pending sync messages immediately after the handle becomes ready.
|
|
35
39
|
void (async () => {
|
|
36
|
-
await handle.doc([READY, REQUESTING]);
|
|
37
40
|
this.#processAllPendingSyncMessages();
|
|
38
41
|
})();
|
|
39
42
|
}
|
|
@@ -45,11 +48,14 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
45
48
|
}
|
|
46
49
|
/// PRIVATE
|
|
47
50
|
async #syncWithPeers() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
try {
|
|
52
|
+
await this.#handle.whenReady();
|
|
53
|
+
const doc = this.#handle.doc(); // XXX THIS ONE IS WEIRD
|
|
54
|
+
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc));
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.log("sync with peers threw an exception");
|
|
58
|
+
}
|
|
53
59
|
}
|
|
54
60
|
async #broadcastToPeers({ data, }) {
|
|
55
61
|
this.#log(`broadcastToPeers`, this.#peers);
|
|
@@ -151,25 +157,24 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
151
157
|
hasPeer(peerId) {
|
|
152
158
|
return this.#peers.includes(peerId);
|
|
153
159
|
}
|
|
154
|
-
beginSync(peerIds) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
async beginSync(peerIds) {
|
|
161
|
+
void this.#handle
|
|
162
|
+
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
163
|
+
.then(() => {
|
|
164
|
+
this.#syncStarted = true;
|
|
165
|
+
this.#checkDocUnavailable();
|
|
166
|
+
})
|
|
167
|
+
.catch(e => {
|
|
168
|
+
console.log("caught whenready", e);
|
|
162
169
|
this.#syncStarted = true;
|
|
163
170
|
this.#checkDocUnavailable();
|
|
164
|
-
const wasUnavailable = doc === undefined;
|
|
165
|
-
if (wasUnavailable && noPeersWithDocument) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
// If the doc is unavailable we still need a blank document to generate
|
|
169
|
-
// the sync message from
|
|
170
|
-
return doc ?? A.init();
|
|
171
171
|
});
|
|
172
|
-
this.#
|
|
172
|
+
const peersWithDocument = this.#peers.some(peerId => {
|
|
173
|
+
return this.#peerDocumentStatuses[peerId] == "has";
|
|
174
|
+
});
|
|
175
|
+
if (peersWithDocument) {
|
|
176
|
+
await this.#handle.whenReady();
|
|
177
|
+
}
|
|
173
178
|
peerIds.forEach(peerId => {
|
|
174
179
|
this.#withSyncState(peerId, syncState => {
|
|
175
180
|
// HACK: if we have a sync state already, we round-trip it through the encoding system to make
|
|
@@ -178,11 +183,22 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
178
183
|
// TODO: cover that case with a test and remove this hack
|
|
179
184
|
const reparsedSyncState = A.decodeSyncState(A.encodeSyncState(syncState));
|
|
180
185
|
this.#setSyncState(peerId, reparsedSyncState);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
187
|
+
// with; but we don't want to surface that state to the front end
|
|
188
|
+
this.#handle
|
|
189
|
+
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
190
|
+
.then(() => {
|
|
191
|
+
const doc = this.#handle.isReady()
|
|
192
|
+
? this.#handle.doc()
|
|
193
|
+
: A.init();
|
|
194
|
+
const noPeersWithDocument = peerIds.every(peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]);
|
|
195
|
+
const wasUnavailable = doc === undefined;
|
|
196
|
+
if (wasUnavailable && noPeersWithDocument) {
|
|
197
|
+
return;
|
|
185
198
|
}
|
|
199
|
+
// If the doc is unavailable we still need a blank document to generate
|
|
200
|
+
// the sync message from
|
|
201
|
+
this.#sendSyncMessage(peerId, doc ?? A.init());
|
|
186
202
|
})
|
|
187
203
|
.catch(err => {
|
|
188
204
|
this.#log(`Error loading doc for ${peerId}: ${err}`);
|
package/fuzz/fuzz.ts
CHANGED
|
@@ -107,9 +107,9 @@ for (let i = 0; i < 100000; i++) {
|
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
await pause(0)
|
|
110
|
-
const a = await aliceRepo.find(doc.url).doc()
|
|
111
|
-
const b = await bobRepo.find(doc.url).doc()
|
|
112
|
-
const c = await charlieRepo.find(doc.url).doc()
|
|
110
|
+
const a = (await aliceRepo.find(doc.url)).doc()
|
|
111
|
+
const b = (await bobRepo.find(doc.url)).doc()
|
|
112
|
+
const c = (await charlieRepo.find(doc.url)).doc()
|
|
113
113
|
assert.deepStrictEqual(a, b, "A and B should be equal")
|
|
114
114
|
assert.deepStrictEqual(b, c, "B and C should be equal")
|
|
115
115
|
|
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.23",
|
|
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>",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"http-server": "^14.1.0",
|
|
23
|
+
"ts-node": "^10.9.2",
|
|
23
24
|
"vite": "^5.0.8"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
@@ -29,8 +30,6 @@
|
|
|
29
30
|
"debug": "^4.3.4",
|
|
30
31
|
"eventemitter3": "^5.0.1",
|
|
31
32
|
"fast-sha256": "^1.3.0",
|
|
32
|
-
"tiny-typed-emitter": "^2.1.0",
|
|
33
|
-
"ts-node": "^10.9.1",
|
|
34
33
|
"uuid": "^9.0.0",
|
|
35
34
|
"xstate": "^5.9.1"
|
|
36
35
|
},
|
|
@@ -60,5 +59,5 @@
|
|
|
60
59
|
"publishConfig": {
|
|
61
60
|
"access": "public"
|
|
62
61
|
},
|
|
63
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "82a9bed7cc6a92940f3aa68167f6990123dda58e"
|
|
64
63
|
}
|
package/src/DocHandle.ts
CHANGED
|
@@ -83,12 +83,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
83
83
|
this.emit("delete", { handle: this })
|
|
84
84
|
return { doc: A.init() }
|
|
85
85
|
}),
|
|
86
|
+
onUnavailable: assign(() => {
|
|
87
|
+
return { doc: A.init() }
|
|
88
|
+
}),
|
|
86
89
|
onUnload: assign(() => {
|
|
87
90
|
return { doc: A.init() }
|
|
88
91
|
}),
|
|
89
|
-
onUnavailable: () => {
|
|
90
|
-
this.emit("unavailable", { handle: this })
|
|
91
|
-
},
|
|
92
92
|
},
|
|
93
93
|
}).createMachine({
|
|
94
94
|
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
|
|
@@ -276,52 +276,28 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
*
|
|
279
|
+
* Returns the current state of the Automerge document this handle manages.
|
|
280
|
+
*
|
|
281
|
+
* @returns the current document
|
|
282
|
+
* @throws on deleted and unavailable documents
|
|
280
283
|
*
|
|
281
|
-
* This is the recommended way to access a handle's document. Note that this waits for the handle
|
|
282
|
-
* to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
|
|
283
284
|
*/
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
awaitStates: HandleState[] = ["ready", "unavailable"]
|
|
287
|
-
) {
|
|
288
|
-
try {
|
|
289
|
-
// wait for the document to enter one of the desired states
|
|
290
|
-
await this.#statePromise(awaitStates)
|
|
291
|
-
} catch (error) {
|
|
292
|
-
// if we timed out, return undefined
|
|
293
|
-
return undefined
|
|
294
|
-
}
|
|
295
|
-
// If we have fixed heads, return a view at those heads
|
|
285
|
+
doc() {
|
|
286
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
296
287
|
if (this.#fixedHeads) {
|
|
297
|
-
|
|
298
|
-
if (!doc || this.isUnavailable()) return undefined
|
|
299
|
-
return A.view(doc, decodeHeads(this.#fixedHeads))
|
|
288
|
+
return A.view(this.#doc, decodeHeads(this.#fixedHeads))
|
|
300
289
|
}
|
|
301
|
-
|
|
302
|
-
return !this.isUnavailable() ? this.#doc : undefined
|
|
290
|
+
return this.#doc
|
|
303
291
|
}
|
|
304
292
|
|
|
305
293
|
/**
|
|
306
|
-
* Synchronously returns the current state of the Automerge document this handle manages, or
|
|
307
|
-
* undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
|
|
308
|
-
* if you want to make sure loading is complete first.
|
|
309
294
|
*
|
|
310
|
-
*
|
|
311
|
-
* synchronization process.
|
|
312
|
-
*
|
|
313
|
-
* Note that `undefined` is not a valid Automerge document, so the return from this function is
|
|
314
|
-
* unambigous.
|
|
315
|
-
*
|
|
316
|
-
* @returns the current document, or undefined if the document is not ready.
|
|
317
|
-
*/
|
|
295
|
+
* @deprecated */
|
|
318
296
|
docSync() {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
return this.#doc
|
|
297
|
+
console.warn(
|
|
298
|
+
"docSync is deprecated. Use doc() instead. This function will be removed as part of the 2.0 release."
|
|
299
|
+
)
|
|
300
|
+
return this.doc()
|
|
325
301
|
}
|
|
326
302
|
|
|
327
303
|
/**
|
|
@@ -329,8 +305,8 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
329
305
|
* This precisely defines the state of a document.
|
|
330
306
|
* @returns the current document's heads, or undefined if the document is not ready
|
|
331
307
|
*/
|
|
332
|
-
heads(): UrlHeads
|
|
333
|
-
if (!this.isReady())
|
|
308
|
+
heads(): UrlHeads {
|
|
309
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
334
310
|
if (this.#fixedHeads) {
|
|
335
311
|
return this.#fixedHeads
|
|
336
312
|
}
|
|
@@ -365,8 +341,8 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
365
341
|
}
|
|
366
342
|
|
|
367
343
|
/**
|
|
368
|
-
* Creates a
|
|
369
|
-
* by the `heads` passed in. The return value is the same type as
|
|
344
|
+
* Creates a fixed "view" of an automerge document at the given point in time represented
|
|
345
|
+
* by the `heads` passed in. The return value is the same type as doc() and will return
|
|
370
346
|
* undefined if the object hasn't finished loading.
|
|
371
347
|
*
|
|
372
348
|
* @remarks
|
|
@@ -426,7 +402,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
426
402
|
if (!otherHeads) throw new Error("Other document's heads not available")
|
|
427
403
|
|
|
428
404
|
// Create a temporary merged doc to verify shared history and compute diff
|
|
429
|
-
const mergedDoc = A.merge(A.clone(doc), first.
|
|
405
|
+
const mergedDoc = A.merge(A.clone(doc), first.doc()!)
|
|
430
406
|
// Use the merged doc to compute the diff
|
|
431
407
|
return A.diff(
|
|
432
408
|
mergedDoc,
|
|
@@ -591,10 +567,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
591
567
|
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
592
568
|
)
|
|
593
569
|
}
|
|
594
|
-
const mergingDoc = otherHandle.
|
|
595
|
-
if (!mergingDoc) {
|
|
596
|
-
throw new Error("The document to be merged in is falsy, aborting.")
|
|
597
|
-
}
|
|
570
|
+
const mergingDoc = otherHandle.doc()
|
|
598
571
|
|
|
599
572
|
this.update(doc => {
|
|
600
573
|
return A.merge(doc, mergingDoc)
|
|
@@ -641,7 +614,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
641
614
|
broadcast(message: unknown) {
|
|
642
615
|
this.emit("ephemeral-message-outbound", {
|
|
643
616
|
handle: this,
|
|
644
|
-
data: encode(message),
|
|
617
|
+
data: new Uint8Array(encode(message)),
|
|
645
618
|
})
|
|
646
619
|
}
|
|
647
620
|
|
|
@@ -680,7 +653,6 @@ export interface DocHandleEvents<T> {
|
|
|
680
653
|
"heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
|
|
681
654
|
change: (payload: DocHandleChangePayload<T>) => void
|
|
682
655
|
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
683
|
-
unavailable: (payload: DocHandleUnavailablePayload<T>) => void
|
|
684
656
|
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
685
657
|
"ephemeral-message-outbound": (
|
|
686
658
|
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DocHandle } from "./DocHandle.js"
|
|
2
|
+
|
|
3
|
+
export type FindProgressState =
|
|
4
|
+
| "loading"
|
|
5
|
+
| "ready"
|
|
6
|
+
| "failed"
|
|
7
|
+
| "aborted"
|
|
8
|
+
| "unavailable"
|
|
9
|
+
|
|
10
|
+
interface FindProgressBase<T> {
|
|
11
|
+
state: FindProgressState
|
|
12
|
+
handle: DocHandle<T>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FindProgressLoading<T> extends FindProgressBase<T> {
|
|
16
|
+
state: "loading"
|
|
17
|
+
progress: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FindProgressReady<T> extends FindProgressBase<T> {
|
|
21
|
+
state: "ready"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FindProgressFailed<T> extends FindProgressBase<T> {
|
|
25
|
+
state: "failed"
|
|
26
|
+
error: Error
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FindProgressUnavailable<T> extends FindProgressBase<T> {
|
|
30
|
+
state: "unavailable"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FindProgressAborted<T> extends FindProgressBase<T> {
|
|
34
|
+
state: "aborted"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type FindProgress<T> =
|
|
38
|
+
| FindProgressLoading<T>
|
|
39
|
+
| FindProgressReady<T>
|
|
40
|
+
| FindProgressFailed<T>
|
|
41
|
+
| FindProgressUnavailable<T>
|
|
42
|
+
| FindProgressAborted<T>
|
|
43
|
+
|
|
44
|
+
export type FindProgressWithMethods<T> = FindProgress<T> & {
|
|
45
|
+
next: () => Promise<FindProgressWithMethods<T>>
|
|
46
|
+
// TODO: i don't like this allowableStates
|
|
47
|
+
untilReady: (allowableStates: string[]) => Promise<DocHandle<T>>
|
|
48
|
+
}
|
package/src/Repo.ts
CHANGED
|
@@ -39,6 +39,8 @@ import type {
|
|
|
39
39
|
DocumentId,
|
|
40
40
|
PeerId,
|
|
41
41
|
} from "./types.js"
|
|
42
|
+
import { abortable, AbortOptions } from "./helpers/abortable.js"
|
|
43
|
+
import { FindProgress, FindProgressWithMethods } from "./FindProgress.js"
|
|
42
44
|
|
|
43
45
|
function randomPeerId() {
|
|
44
46
|
return ("peer-" + Math.random().toString(36).slice(4)) as PeerId
|
|
@@ -260,18 +262,8 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
260
262
|
handle.on("heads-changed", throttle(saveFn, this.saveDebounceRate))
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
handle.on("unavailable", () => {
|
|
264
|
-
this.#log("document unavailable", { documentId: handle.documentId })
|
|
265
|
-
this.emit("unavailable-document", {
|
|
266
|
-
documentId: handle.documentId,
|
|
267
|
-
})
|
|
268
|
-
})
|
|
269
|
-
|
|
270
265
|
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
271
|
-
this.synchronizer.addDocument(handle
|
|
272
|
-
|
|
273
|
-
// Preserve the old event in case anyone was using it.
|
|
274
|
-
this.emit("document", { handle })
|
|
266
|
+
this.synchronizer.addDocument(handle)
|
|
275
267
|
}
|
|
276
268
|
|
|
277
269
|
#receiveMessage(message: RepoMessage) {
|
|
@@ -402,8 +394,6 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
402
394
|
* Any peers this `Repo` is connected to for whom `sharePolicy` returns `true` will
|
|
403
395
|
* be notified of the newly created DocHandle.
|
|
404
396
|
*
|
|
405
|
-
* @throws if the cloned handle is not yet ready or if
|
|
406
|
-
* `clonedHandle.docSync()` returns `undefined` (i.e. the handle is unavailable).
|
|
407
397
|
*/
|
|
408
398
|
clone<T>(clonedHandle: DocHandle<T>) {
|
|
409
399
|
if (!clonedHandle.isReady()) {
|
|
@@ -413,11 +403,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
413
403
|
)
|
|
414
404
|
}
|
|
415
405
|
|
|
416
|
-
const sourceDoc = clonedHandle.
|
|
417
|
-
if (!sourceDoc) {
|
|
418
|
-
throw new Error("Cloned handle doesn't have a document.")
|
|
419
|
-
}
|
|
420
|
-
|
|
406
|
+
const sourceDoc = clonedHandle.doc()
|
|
421
407
|
const handle = this.create<T>()
|
|
422
408
|
|
|
423
409
|
handle.update(() => {
|
|
@@ -428,63 +414,196 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
428
414
|
return handle
|
|
429
415
|
}
|
|
430
416
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
): DocHandle<T> {
|
|
417
|
+
findWithProgress<T>(
|
|
418
|
+
id: AnyDocumentId,
|
|
419
|
+
options: AbortOptions = {}
|
|
420
|
+
): FindProgressWithMethods<T> | FindProgress<T> {
|
|
421
|
+
const { signal } = options
|
|
422
|
+
const abortPromise = abortable(signal)
|
|
423
|
+
|
|
439
424
|
const { documentId, heads } = isValidAutomergeUrl(id)
|
|
440
425
|
? parseAutomergeUrl(id)
|
|
441
426
|
: { documentId: interpretAsDocumentId(id), heads: undefined }
|
|
442
427
|
|
|
443
|
-
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
428
|
+
// Check cache first - return plain FindStep for terminal states
|
|
429
|
+
if (this.#handleCache[documentId]) {
|
|
430
|
+
const handle = this.#handleCache[documentId]
|
|
431
|
+
if (handle.state === UNAVAILABLE) {
|
|
432
|
+
const result = {
|
|
433
|
+
state: "unavailable" as const,
|
|
434
|
+
error: new Error(`Document ${id} is unavailable`),
|
|
435
|
+
handle,
|
|
436
|
+
}
|
|
437
|
+
return result
|
|
438
|
+
}
|
|
439
|
+
if (handle.state === DELETED) {
|
|
440
|
+
return {
|
|
441
|
+
state: "failed",
|
|
442
|
+
error: new Error(`Document ${id} was deleted`),
|
|
443
|
+
handle,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (handle.state === READY) {
|
|
447
|
+
// If we already have the handle, return it immediately (or a view of the handle if heads are specified)
|
|
448
|
+
return {
|
|
449
|
+
state: "ready",
|
|
450
|
+
// TODO: this handle needs to be cached (or at least avoid running clone)
|
|
451
|
+
handle: heads ? handle.view(heads) : handle,
|
|
452
|
+
}
|
|
452
453
|
}
|
|
453
|
-
// If we already have the handle, return it immediately (or a view of the handle if heads are specified)
|
|
454
|
-
return heads ? cachedHandle.view(heads) : cachedHandle
|
|
455
454
|
}
|
|
456
455
|
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
456
|
+
// the generator takes over `this`, so we need an alias to the repo this
|
|
457
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
458
|
+
const that = this
|
|
459
|
+
async function* progressGenerator(): AsyncGenerator<FindProgress<T>> {
|
|
460
|
+
try {
|
|
461
|
+
const handle = that.#getHandle<T>({ documentId })
|
|
462
|
+
yield { state: "loading", progress: 25, handle }
|
|
461
463
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
464
|
+
const loadingPromise = await (that.storageSubsystem
|
|
465
|
+
? that.storageSubsystem.loadDoc(handle.documentId)
|
|
466
|
+
: Promise.resolve(null))
|
|
467
|
+
|
|
468
|
+
const loadedDoc = await Promise.race([loadingPromise, abortPromise])
|
|
467
469
|
|
|
468
|
-
attemptLoad
|
|
469
|
-
.then(async loadedDoc => {
|
|
470
470
|
if (loadedDoc) {
|
|
471
|
-
// uhhhh, sorry if you're reading this because we were lying to the type system
|
|
472
471
|
handle.update(() => loadedDoc as Automerge.Doc<T>)
|
|
473
472
|
handle.doneLoading()
|
|
473
|
+
yield { state: "loading", progress: 50, handle }
|
|
474
474
|
} else {
|
|
475
|
-
|
|
476
|
-
// we request the document. this prevents entering unavailable during initialization.
|
|
477
|
-
await this.networkSubsystem.whenReady()
|
|
475
|
+
await Promise.race([that.networkSubsystem.whenReady(), abortPromise])
|
|
478
476
|
handle.request()
|
|
477
|
+
yield { state: "loading", progress: 75, handle }
|
|
479
478
|
}
|
|
480
|
-
this.#registerHandleWithSubsystems(handle)
|
|
481
|
-
})
|
|
482
|
-
.catch(err => {
|
|
483
|
-
this.#log("error waiting for network", { err })
|
|
484
|
-
})
|
|
485
479
|
|
|
486
|
-
|
|
487
|
-
|
|
480
|
+
that.#registerHandleWithSubsystems(handle)
|
|
481
|
+
|
|
482
|
+
await Promise.race([
|
|
483
|
+
handle.whenReady([READY, UNAVAILABLE]),
|
|
484
|
+
abortPromise,
|
|
485
|
+
])
|
|
486
|
+
|
|
487
|
+
if (handle.state === UNAVAILABLE) {
|
|
488
|
+
yield { state: "unavailable", handle }
|
|
489
|
+
}
|
|
490
|
+
if (handle.state === DELETED) {
|
|
491
|
+
throw new Error(`Document ${id} was deleted`)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
yield { state: "ready", handle }
|
|
495
|
+
} catch (error) {
|
|
496
|
+
yield {
|
|
497
|
+
state: "failed",
|
|
498
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
499
|
+
handle,
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const iterator = progressGenerator()
|
|
505
|
+
|
|
506
|
+
const next = async () => {
|
|
507
|
+
const result = await iterator.next()
|
|
508
|
+
return { ...result.value, next }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const untilReady = async (allowableStates: string[]) => {
|
|
512
|
+
for await (const state of iterator) {
|
|
513
|
+
if (allowableStates.includes(state.handle.state)) {
|
|
514
|
+
return state.handle
|
|
515
|
+
}
|
|
516
|
+
if (state.state === "unavailable") {
|
|
517
|
+
throw new Error(`Document ${id} is unavailable`)
|
|
518
|
+
}
|
|
519
|
+
if (state.state === "ready") return state.handle
|
|
520
|
+
if (state.state === "failed") throw state.error
|
|
521
|
+
}
|
|
522
|
+
throw new Error("Iterator completed without reaching ready state")
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const handle = this.#getHandle<T>({ documentId })
|
|
526
|
+
const initial = { state: "loading" as const, progress: 0, handle }
|
|
527
|
+
return { ...initial, next, untilReady }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async find<T>(
|
|
531
|
+
id: AnyDocumentId,
|
|
532
|
+
options: RepoFindOptions & AbortOptions = {}
|
|
533
|
+
): Promise<DocHandle<T>> {
|
|
534
|
+
const { allowableStates = ["ready"], signal } = options
|
|
535
|
+
const progress = this.findWithProgress<T>(id, { signal })
|
|
536
|
+
|
|
537
|
+
/*if (allowableStates.includes(progress.state)) {
|
|
538
|
+
console.log("returning early")
|
|
539
|
+
return progress.handle
|
|
540
|
+
}*/
|
|
541
|
+
|
|
542
|
+
if ("untilReady" in progress) {
|
|
543
|
+
this.#registerHandleWithSubsystems(progress.handle)
|
|
544
|
+
return progress.untilReady(allowableStates)
|
|
545
|
+
} else {
|
|
546
|
+
return progress.handle
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Loads a document without waiting for ready state
|
|
552
|
+
*/
|
|
553
|
+
async #loadDocument<T>(documentId: DocumentId): Promise<DocHandle<T>> {
|
|
554
|
+
// If we have the handle cached, return it
|
|
555
|
+
if (this.#handleCache[documentId]) {
|
|
556
|
+
return this.#handleCache[documentId]
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// If we don't already have the handle, make an empty one and try loading it
|
|
560
|
+
const handle = this.#getHandle<T>({ documentId })
|
|
561
|
+
const loadedDoc = await (this.storageSubsystem
|
|
562
|
+
? this.storageSubsystem.loadDoc(handle.documentId)
|
|
563
|
+
: Promise.resolve(null))
|
|
564
|
+
|
|
565
|
+
if (loadedDoc) {
|
|
566
|
+
// We need to cast this to <T> because loadDoc operates in <unknowns>.
|
|
567
|
+
// This is really where we ought to be validating the input matches <T>.
|
|
568
|
+
handle.update(() => loadedDoc as Automerge.Doc<T>)
|
|
569
|
+
handle.doneLoading()
|
|
570
|
+
} else {
|
|
571
|
+
// Because the network subsystem might still be booting up, we wait
|
|
572
|
+
// here so that we don't immediately give up loading because we're still
|
|
573
|
+
// making our initial connection to a sync server.
|
|
574
|
+
await this.networkSubsystem.whenReady()
|
|
575
|
+
handle.request()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.#registerHandleWithSubsystems(handle)
|
|
579
|
+
return handle
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Retrieves a document by id. It gets data from the local system, but also emits a `document`
|
|
584
|
+
* event to advertise interest in the document.
|
|
585
|
+
*/
|
|
586
|
+
async findClassic<T>(
|
|
587
|
+
/** The url or documentId of the handle to retrieve */
|
|
588
|
+
id: AnyDocumentId,
|
|
589
|
+
options: RepoFindOptions & AbortOptions = {}
|
|
590
|
+
): Promise<DocHandle<T>> {
|
|
591
|
+
const documentId = interpretAsDocumentId(id)
|
|
592
|
+
const { allowableStates, signal } = options
|
|
593
|
+
|
|
594
|
+
return Promise.race([
|
|
595
|
+
(async () => {
|
|
596
|
+
const handle = await this.#loadDocument<T>(documentId)
|
|
597
|
+
if (!allowableStates) {
|
|
598
|
+
await handle.whenReady([READY, UNAVAILABLE])
|
|
599
|
+
if (handle.state === UNAVAILABLE && !signal?.aborted) {
|
|
600
|
+
throw new Error(`Document ${id} is unavailable`)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return handle
|
|
604
|
+
})(),
|
|
605
|
+
abortable(signal),
|
|
606
|
+
])
|
|
488
607
|
}
|
|
489
608
|
|
|
490
609
|
delete(
|
|
@@ -511,8 +630,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
511
630
|
const documentId = interpretAsDocumentId(id)
|
|
512
631
|
|
|
513
632
|
const handle = this.#getHandle({ documentId })
|
|
514
|
-
const doc =
|
|
515
|
-
if (!doc) return undefined
|
|
633
|
+
const doc = handle.doc()
|
|
516
634
|
return Automerge.save(doc)
|
|
517
635
|
}
|
|
518
636
|
|
|
@@ -566,11 +684,7 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
566
684
|
: Object.values(this.#handleCache)
|
|
567
685
|
await Promise.all(
|
|
568
686
|
handles.map(async handle => {
|
|
569
|
-
|
|
570
|
-
if (!doc) {
|
|
571
|
-
return
|
|
572
|
-
}
|
|
573
|
-
return this.storageSubsystem!.saveDoc(handle.documentId, doc)
|
|
687
|
+
return this.storageSubsystem!.saveDoc(handle.documentId, handle.doc())
|
|
574
688
|
})
|
|
575
689
|
)
|
|
576
690
|
}
|
|
@@ -589,7 +703,9 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
589
703
|
return
|
|
590
704
|
}
|
|
591
705
|
const handle = this.#getHandle({ documentId })
|
|
592
|
-
|
|
706
|
+
await handle.whenReady([READY, UNLOADED, DELETED, UNAVAILABLE])
|
|
707
|
+
const doc = handle.doc()
|
|
708
|
+
// because this is an internal-ish function, we'll be extra careful about undefined docs here
|
|
593
709
|
if (doc) {
|
|
594
710
|
if (handle.isReady()) {
|
|
595
711
|
handle.unload()
|
|
@@ -677,6 +793,10 @@ export interface RepoEvents {
|
|
|
677
793
|
"doc-metrics": (arg: DocMetrics) => void
|
|
678
794
|
}
|
|
679
795
|
|
|
796
|
+
export interface RepoFindOptions {
|
|
797
|
+
allowableStates?: string[]
|
|
798
|
+
}
|
|
799
|
+
|
|
680
800
|
export interface DocumentPayload {
|
|
681
801
|
handle: DocHandle<any>
|
|
682
802
|
}
|