@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.
@@ -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
- this.#log(`syncWithPeers`);
49
- const doc = await this.#handle.doc();
50
- if (doc === undefined)
51
- return;
52
- this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc));
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
- const noPeersWithDocument = peerIds.every(peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]);
156
- // At this point if we don't have anything in our storage, we need to use an empty doc to sync
157
- // with; but we don't want to surface that state to the front end
158
- const docPromise = this.#handle
159
- .doc([READY, REQUESTING, UNAVAILABLE])
160
- .then(doc => {
161
- // we register out peers first, then say that sync has started
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.#log(`beginSync: ${peerIds.join(", ")}`);
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
- docPromise
182
- .then(doc => {
183
- if (doc) {
184
- this.#sendSyncMessage(peerId, doc);
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.20",
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": "d53bc37be0fd923ff40f3cf7e2bd06a0496ddb73"
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
- * @returns the current state of this handle's Automerge document.
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
- async doc(
285
- /** states to wait for, such as "LOADING". mostly for internal use. */
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
- const doc = this.#doc
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
- // Return the document
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
- * Not to be confused with the SyncState of the document, which describes the state of the
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
- if (!this.isReady()) return undefined
320
- if (this.#fixedHeads) {
321
- const doc = this.#doc
322
- return doc ? A.view(doc, decodeHeads(this.#fixedHeads)) : undefined
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 | undefined {
333
- if (!this.isReady()) return undefined
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 new DocHandle with a fixed "view" at the given point in time represented
369
- * by the `heads` passed in. The return value is the same type as docSync() and will return
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.docSync()!)
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.docSync()
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.documentId)
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.docSync()
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
- * Retrieves a document by id. It gets data from the local system, but also emits a `document`
433
- * event to advertise interest in the document.
434
- */
435
- find<T>(
436
- /** The url or documentId of the handle to retrieve */
437
- id: AnyDocumentId
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
- const cachedHandle = this.#handleCache[documentId]
444
- if (cachedHandle) {
445
- if (cachedHandle.isUnavailable()) {
446
- // this ensures that the event fires after the handle has been returned
447
- setTimeout(() => {
448
- cachedHandle.emit("unavailable", {
449
- handle: cachedHandle,
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
- // If we don't already have the handle, make an empty one and try loading it
458
- const handle = this.#getHandle<T>({
459
- documentId,
460
- }) as DocHandle<T>
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
- // Loading & network is going to be asynchronous no matter what,
463
- // but we want to return the handle immediately.
464
- const attemptLoad = this.storageSubsystem
465
- ? this.storageSubsystem.loadDoc(handle.documentId)
466
- : Promise.resolve(null)
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
- // we want to wait for the network subsystem to be ready before
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
- // If we already have the handle, return it immediately (or a view of the handle if heads are specified)
487
- return heads ? handle.view(heads) : handle
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 = await handle.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
- const doc = handle.docSync()
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
- const doc = await handle.doc([READY, UNLOADED, DELETED, UNAVAILABLE])
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
  }