@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.
@@ -0,0 +1,61 @@
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(
33
+ signal?: AbortSignal,
34
+ cleanup?: () => void
35
+ ): Promise<never> {
36
+ if (signal?.aborted) {
37
+ throw new DOMException("Operation aborted", "AbortError")
38
+ }
39
+
40
+ if (!signal) {
41
+ return new Promise(() => {}) // Never resolves
42
+ }
43
+
44
+ return new Promise((_, reject) => {
45
+ signal.addEventListener(
46
+ "abort",
47
+ () => {
48
+ cleanup?.()
49
+ reject(new DOMException("Operation aborted", "AbortError"))
50
+ },
51
+ { once: true }
52
+ )
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Include this type in an options object to pass an AbortSignal to a function.
58
+ */
59
+ export interface AbortOptions {
60
+ signal?: AbortSignal
61
+ }
@@ -49,9 +49,10 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
49
49
  // Alice creates a document
50
50
  const aliceHandle = aliceRepo.create<TestDoc>()
51
51
 
52
- // Bob receives the document
53
- await eventPromise(bobRepo, "document")
54
- const bobHandle = bobRepo.find<TestDoc>(aliceHandle.url)
52
+ // TODO: ... let connections complete. this shouldn't be necessary.
53
+ await pause(50)
54
+
55
+ const bobHandle = await bobRepo.find<TestDoc>(aliceHandle.url)
55
56
 
56
57
  // Alice changes the document
57
58
  aliceHandle.change(d => {
@@ -60,7 +61,7 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
60
61
 
61
62
  // Bob receives the change
62
63
  await eventPromise(bobHandle, "change")
63
- assert.equal((await bobHandle.doc())?.foo, "bar")
64
+ assert.equal((await bobHandle).doc()?.foo, "bar")
64
65
 
65
66
  // Bob changes the document
66
67
  bobHandle.change(d => {
@@ -69,7 +70,7 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
69
70
 
70
71
  // Alice receives the change
71
72
  await eventPromise(aliceHandle, "change")
72
- assert.equal((await aliceHandle.doc())?.foo, "baz")
73
+ assert.equal(aliceHandle.doc().foo, "baz")
73
74
  }
74
75
 
75
76
  // Run the test in both directions, in case they're different types of adapters
@@ -100,9 +101,9 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
100
101
  const docUrl = aliceHandle.url
101
102
 
102
103
  // Bob and Charlie receive the document
103
- await eventPromises([bobRepo, charlieRepo], "document")
104
- const bobHandle = bobRepo.find<TestDoc>(docUrl)
105
- const charlieHandle = charlieRepo.find<TestDoc>(docUrl)
104
+ await pause(50)
105
+ const bobHandle = await bobRepo.find<TestDoc>(docUrl)
106
+ const charlieHandle = await charlieRepo.find<TestDoc>(docUrl)
106
107
 
107
108
  // Alice changes the document
108
109
  aliceHandle.change(d => {
@@ -111,8 +112,8 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
111
112
 
112
113
  // Bob and Charlie receive the change
113
114
  await eventPromises([bobHandle, charlieHandle], "change")
114
- assert.equal((await bobHandle.doc())?.foo, "bar")
115
- assert.equal((await charlieHandle.doc())?.foo, "bar")
115
+ assert.equal(bobHandle.doc().foo, "bar")
116
+ assert.equal(charlieHandle.doc().foo, "bar")
116
117
 
117
118
  // Charlie changes the document
118
119
  charlieHandle.change(d => {
@@ -121,8 +122,8 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
121
122
 
122
123
  // Alice and Bob receive the change
123
124
  await eventPromises([aliceHandle, bobHandle], "change")
124
- assert.equal((await bobHandle.doc())?.foo, "baz")
125
- assert.equal((await charlieHandle.doc())?.foo, "baz")
125
+ assert.equal(bobHandle.doc().foo, "baz")
126
+ assert.equal(charlieHandle.doc().foo, "baz")
126
127
 
127
128
  teardown()
128
129
  })
@@ -141,7 +142,7 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
141
142
  )
142
143
 
143
144
  const aliceHandle = aliceRepo.create<TestDoc>()
144
- const charlieHandle = charlieRepo.find(aliceHandle.url)
145
+ const charlieHandle = await charlieRepo.find(aliceHandle.url)
145
146
 
146
147
  // pause to give charlie a chance to let alice know it wants the doc
147
148
  await pause(100)
@@ -1,6 +1,6 @@
1
1
  import debug from "debug"
2
2
  import { DocHandle } from "../DocHandle.js"
3
- import { parseAutomergeUrl, stringifyAutomergeUrl } from "../AutomergeUrl.js"
3
+ import { parseAutomergeUrl } from "../AutomergeUrl.js"
4
4
  import { Repo } from "../Repo.js"
5
5
  import { DocMessage } from "../network/messages.js"
6
6
  import { AutomergeUrl, DocumentId, PeerId } from "../types.js"
@@ -29,18 +29,19 @@ export class CollectionSynchronizer extends Synchronizer {
29
29
  }
30
30
 
31
31
  /** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
32
- #fetchDocSynchronizer(documentId: DocumentId) {
33
- if (!this.docSynchronizers[documentId]) {
34
- const handle = this.repo.find(stringifyAutomergeUrl({ documentId }))
35
- this.docSynchronizers[documentId] = this.#initDocSynchronizer(handle)
32
+ #fetchDocSynchronizer(handle: DocHandle<unknown>) {
33
+ if (!this.docSynchronizers[handle.documentId]) {
34
+ this.docSynchronizers[handle.documentId] =
35
+ this.#initDocSynchronizer(handle)
36
36
  }
37
- return this.docSynchronizers[documentId]
37
+ return this.docSynchronizers[handle.documentId]
38
38
  }
39
39
 
40
40
  /** Creates a new docSynchronizer and sets it up to propagate messages */
41
41
  #initDocSynchronizer(handle: DocHandle<unknown>): DocSynchronizer {
42
42
  const docSynchronizer = new DocSynchronizer({
43
43
  handle,
44
+ peerId: this.repo.networkSubsystem.peerId,
44
45
  onLoadSyncState: async peerId => {
45
46
  if (!this.repo.storageSubsystem) {
46
47
  return
@@ -109,13 +110,16 @@ export class CollectionSynchronizer extends Synchronizer {
109
110
 
110
111
  this.#docSetUp[documentId] = true
111
112
 
112
- const docSynchronizer = this.#fetchDocSynchronizer(documentId)
113
+ const handle = await this.repo.find(documentId, {
114
+ allowableStates: ["ready", "unavailable", "requesting"],
115
+ })
116
+ const docSynchronizer = this.#fetchDocSynchronizer(handle)
113
117
 
114
118
  docSynchronizer.receiveMessage(message)
115
119
 
116
120
  // Initiate sync with any new peers
117
121
  const peers = await this.#documentGenerousPeers(documentId)
118
- docSynchronizer.beginSync(
122
+ void docSynchronizer.beginSync(
119
123
  peers.filter(peerId => !docSynchronizer.hasPeer(peerId))
120
124
  )
121
125
  }
@@ -123,14 +127,14 @@ export class CollectionSynchronizer extends Synchronizer {
123
127
  /**
124
128
  * Starts synchronizing the given document with all peers that we share it generously with.
125
129
  */
126
- addDocument(documentId: DocumentId) {
130
+ addDocument(handle: DocHandle<unknown>) {
127
131
  // HACK: this is a hack to prevent us from adding the same document twice
128
- if (this.#docSetUp[documentId]) {
132
+ if (this.#docSetUp[handle.documentId]) {
129
133
  return
130
134
  }
131
- const docSynchronizer = this.#fetchDocSynchronizer(documentId)
132
- void this.#documentGenerousPeers(documentId).then(peers => {
133
- docSynchronizer.beginSync(peers)
135
+ const docSynchronizer = this.#fetchDocSynchronizer(handle)
136
+ void this.#documentGenerousPeers(handle.documentId).then(peers => {
137
+ void docSynchronizer.beginSync(peers)
134
138
  })
135
139
  }
136
140
 
@@ -152,7 +156,7 @@ export class CollectionSynchronizer extends Synchronizer {
152
156
  for (const docSynchronizer of Object.values(this.docSynchronizers)) {
153
157
  const { documentId } = docSynchronizer
154
158
  void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
155
- if (okToShare) docSynchronizer.beginSync([peerId])
159
+ if (okToShare) void docSynchronizer.beginSync([peerId])
156
160
  })
157
161
  }
158
162
  }
@@ -30,6 +30,7 @@ type PendingMessage = {
30
30
 
31
31
  interface DocSynchronizerConfig {
32
32
  handle: DocHandle<unknown>
33
+ peerId: PeerId
33
34
  onLoadSyncState?: (peerId: PeerId) => Promise<A.SyncState | undefined>
34
35
  }
35
36
 
@@ -56,13 +57,17 @@ export class DocSynchronizer extends Synchronizer {
56
57
 
57
58
  #pendingSyncMessages: Array<PendingMessage> = []
58
59
 
60
+ // We keep this around at least in part for debugging.
61
+ // eslint-disable-next-line no-unused-private-class-members
62
+ #peerId: PeerId
59
63
  #syncStarted = false
60
64
 
61
65
  #handle: DocHandle<unknown>
62
66
  #onLoadSyncState: (peerId: PeerId) => Promise<A.SyncState | undefined>
63
67
 
64
- constructor({ handle, onLoadSyncState }: DocSynchronizerConfig) {
68
+ constructor({ handle, peerId, onLoadSyncState }: DocSynchronizerConfig) {
65
69
  super()
70
+ this.#peerId = peerId
66
71
  this.#handle = handle
67
72
  this.#onLoadSyncState =
68
73
  onLoadSyncState ?? (() => Promise.resolve(undefined))
@@ -81,7 +86,6 @@ export class DocSynchronizer extends Synchronizer {
81
86
 
82
87
  // Process pending sync messages immediately after the handle becomes ready.
83
88
  void (async () => {
84
- await handle.doc([READY, REQUESTING])
85
89
  this.#processAllPendingSyncMessages()
86
90
  })()
87
91
  }
@@ -97,10 +101,13 @@ export class DocSynchronizer extends Synchronizer {
97
101
  /// PRIVATE
98
102
 
99
103
  async #syncWithPeers() {
100
- this.#log(`syncWithPeers`)
101
- const doc = await this.#handle.doc()
102
- if (doc === undefined) return
103
- this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
104
+ try {
105
+ await this.#handle.whenReady()
106
+ const doc = this.#handle.doc() // XXX THIS ONE IS WEIRD
107
+ this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
108
+ } catch (e) {
109
+ console.log("sync with peers threw an exception")
110
+ }
104
111
  }
105
112
 
106
113
  async #broadcastToPeers({
@@ -226,32 +233,26 @@ export class DocSynchronizer extends Synchronizer {
226
233
  return this.#peers.includes(peerId)
227
234
  }
228
235
 
229
- beginSync(peerIds: PeerId[]) {
230
- const noPeersWithDocument = peerIds.every(
231
- peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
232
- )
233
-
234
- // At this point if we don't have anything in our storage, we need to use an empty doc to sync
235
- // with; but we don't want to surface that state to the front end
236
-
237
- const docPromise = this.#handle
238
- .doc([READY, REQUESTING, UNAVAILABLE])
239
- .then(doc => {
240
- // we register out peers first, then say that sync has started
236
+ async beginSync(peerIds: PeerId[]) {
237
+ void this.#handle
238
+ .whenReady([READY, REQUESTING, UNAVAILABLE])
239
+ .then(() => {
240
+ this.#syncStarted = true
241
+ this.#checkDocUnavailable()
242
+ })
243
+ .catch(e => {
244
+ console.log("caught whenready", e)
241
245
  this.#syncStarted = true
242
246
  this.#checkDocUnavailable()
243
-
244
- const wasUnavailable = doc === undefined
245
- if (wasUnavailable && noPeersWithDocument) {
246
- return
247
- }
248
-
249
- // If the doc is unavailable we still need a blank document to generate
250
- // the sync message from
251
- return doc ?? A.init<unknown>()
252
247
  })
253
248
 
254
- this.#log(`beginSync: ${peerIds.join(", ")}`)
249
+ const peersWithDocument = this.#peers.some(peerId => {
250
+ return this.#peerDocumentStatuses[peerId] == "has"
251
+ })
252
+
253
+ if (peersWithDocument) {
254
+ await this.#handle.whenReady()
255
+ }
255
256
 
256
257
  peerIds.forEach(peerId => {
257
258
  this.#withSyncState(peerId, syncState => {
@@ -264,11 +265,28 @@ export class DocSynchronizer extends Synchronizer {
264
265
  )
265
266
  this.#setSyncState(peerId, reparsedSyncState)
266
267
 
267
- docPromise
268
- .then(doc => {
269
- if (doc) {
270
- this.#sendSyncMessage(peerId, doc)
268
+ // At this point if we don't have anything in our storage, we need to use an empty doc to sync
269
+ // with; but we don't want to surface that state to the front end
270
+ this.#handle
271
+ .whenReady([READY, REQUESTING, UNAVAILABLE])
272
+ .then(() => {
273
+ const doc = this.#handle.isReady()
274
+ ? this.#handle.doc()
275
+ : A.init<unknown>()
276
+
277
+ const noPeersWithDocument = peerIds.every(
278
+ peerId =>
279
+ this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
280
+ )
281
+
282
+ const wasUnavailable = doc === undefined
283
+ if (wasUnavailable && noPeersWithDocument) {
284
+ return
271
285
  }
286
+
287
+ // If the doc is unavailable we still need a blank document to generate
288
+ // the sync message from
289
+ this.#sendSyncMessage(peerId, doc ?? A.init<unknown>())
272
290
  })
273
291
  .catch(err => {
274
292
  this.#log(`Error loading doc for ${peerId}: ${err}`)
@@ -352,6 +370,7 @@ export class DocSynchronizer extends Synchronizer {
352
370
  this.#withSyncState(message.senderId, syncState => {
353
371
  this.#handle.update(doc => {
354
372
  const start = performance.now()
373
+
355
374
  const [newDoc, newSyncState] = A.receiveSyncMessage(
356
375
  doc,
357
376
  syncState,
@@ -28,13 +28,13 @@ describe("CollectionSynchronizer", () => {
28
28
  done()
29
29
  })
30
30
 
31
- synchronizer.addDocument(handle.documentId)
31
+ synchronizer.addDocument(handle)
32
32
  }))
33
33
 
34
34
  it("starts synchronizing existing documents when a peer is added", () =>
35
35
  new Promise<void>(done => {
36
36
  const handle = repo.create()
37
- synchronizer.addDocument(handle.documentId)
37
+ synchronizer.addDocument(handle)
38
38
  synchronizer.once("message", event => {
39
39
  const { targetId, documentId } = event as SyncMessage
40
40
  assert(targetId === "peer1")
@@ -50,7 +50,7 @@ describe("CollectionSynchronizer", () => {
50
50
 
51
51
  repo.sharePolicy = async (peerId: PeerId) => peerId !== "peer1"
52
52
 
53
- synchronizer.addDocument(handle.documentId)
53
+ synchronizer.addDocument(handle)
54
54
  synchronizer.once("message", () => {
55
55
  reject(new Error("Should not have sent a message"))
56
56
  })
@@ -71,7 +71,7 @@ describe("CollectionSynchronizer", () => {
71
71
  reject(new Error("Should not have sent a message"))
72
72
  })
73
73
 
74
- synchronizer.addDocument(handle.documentId)
74
+ synchronizer.addDocument(handle)
75
75
 
76
76
  setTimeout(done)
77
77
  }))
@@ -1,7 +1,7 @@
1
1
  import * as A from "@automerge/automerge/next"
2
2
  import assert from "assert"
3
3
  import { decode } from "cbor-x"
4
- import { describe, it, vi } from "vitest"
4
+ import { describe, expect, it, vi } from "vitest"
5
5
  import {
6
6
  encodeHeads,
7
7
  generateAutomergeUrl,
@@ -11,7 +11,6 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
11
11
  import { pause } from "../src/helpers/pause.js"
12
12
  import { DocHandle, DocHandleChangePayload } from "../src/index.js"
13
13
  import { TestDoc } from "./types.js"
14
- import { UNLOADED } from "../src/DocHandle.js"
15
14
 
16
15
  describe("DocHandle", () => {
17
16
  const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
@@ -39,7 +38,7 @@ describe("DocHandle", () => {
39
38
  handle.update(doc => docFromMockStorage(doc))
40
39
 
41
40
  assert.equal(handle.isReady(), true)
42
- const doc = await handle.doc()
41
+ const doc = handle.doc()
43
42
  assert.equal(doc?.foo, "bar")
44
43
  })
45
44
 
@@ -51,13 +50,13 @@ describe("DocHandle", () => {
51
50
  handle.update(doc => docFromMockStorage(doc))
52
51
 
53
52
  assert.equal(handle.isReady(), true)
54
- const doc = await handle.doc()
55
- assert.deepEqual(doc, handle.docSync())
53
+ const doc = handle.doc()
54
+ assert.deepEqual(doc, handle.doc())
56
55
  })
57
56
 
58
- it("should return undefined if we access the doc before ready", async () => {
57
+ it("should throw an exception if we access the doc before ready", async () => {
59
58
  const handle = new DocHandle<TestDoc>(TEST_ID)
60
- assert.equal(handle.docSync(), undefined)
59
+ assert.throws(() => handle.doc())
61
60
  })
62
61
 
63
62
  it("should not return a doc until ready", async () => {
@@ -67,7 +66,7 @@ describe("DocHandle", () => {
67
66
  // simulate loading from storage
68
67
  handle.update(doc => docFromMockStorage(doc))
69
68
 
70
- const doc = await handle.doc()
69
+ const doc = handle.doc()
71
70
 
72
71
  assert.equal(handle.isReady(), true)
73
72
  assert.equal(doc?.foo, "bar")
@@ -87,15 +86,15 @@ describe("DocHandle", () => {
87
86
  handle.change(d => (d.foo = "bar"))
88
87
  assert.equal(handle.isReady(), true)
89
88
 
90
- const heads = encodeHeads(A.getHeads(handle.docSync()))
89
+ const heads = encodeHeads(A.getHeads(handle.doc()))
91
90
  assert.notDeepEqual(handle.heads(), [])
92
91
  assert.deepEqual(heads, handle.heads())
93
92
  })
94
93
 
95
- it("should return undefined if the heads aren't loaded", async () => {
94
+ it("should throw an if the heads aren't loaded", async () => {
96
95
  const handle = new DocHandle<TestDoc>(TEST_ID)
97
96
  assert.equal(handle.isReady(), false)
98
- assert.deepEqual(handle.heads(), undefined)
97
+ expect(() => handle.heads()).toThrow("DocHandle is not ready")
99
98
  })
100
99
 
101
100
  it("should return the history when requested", async () => {
@@ -128,7 +127,7 @@ describe("DocHandle", () => {
128
127
 
129
128
  const history = handle.history()
130
129
  const viewHandle = new DocHandle<TestDoc>(TEST_ID, { heads: history[0] })
131
- viewHandle.update(() => A.clone(handle.docSync()!))
130
+ viewHandle.update(() => A.clone(handle.doc()!))
132
131
  viewHandle.doneLoading()
133
132
 
134
133
  assert.deepEqual(await viewHandle.doc(), { foo: "zero" })
@@ -260,8 +259,6 @@ describe("DocHandle", () => {
260
259
  const handle = new DocHandle<TestDoc>(TEST_ID)
261
260
  assert.equal(handle.isReady(), false)
262
261
 
263
- handle.doc()
264
-
265
262
  assert(vi.getTimerCount() > timerCount)
266
263
 
267
264
  // simulate loading from storage
@@ -286,7 +283,7 @@ describe("DocHandle", () => {
286
283
  assert.equal(handle.isReady(), true)
287
284
  handle.change(d => (d.foo = "pizza"))
288
285
 
289
- const doc = await handle.doc()
286
+ const doc = handle.doc()
290
287
  assert.equal(doc?.foo, "pizza")
291
288
  })
292
289
 
@@ -296,7 +293,9 @@ describe("DocHandle", () => {
296
293
  // we don't have it in storage, so we request it from the network
297
294
  handle.request()
298
295
 
299
- assert.equal(handle.docSync(), undefined)
296
+ await expect(() => {
297
+ handle.doc()
298
+ }).toThrowError("DocHandle is not ready")
300
299
  assert.equal(handle.isReady(), false)
301
300
  assert.throws(() => handle.change(_ => {}))
302
301
  })
@@ -312,7 +311,7 @@ describe("DocHandle", () => {
312
311
  return A.change(doc, d => (d.foo = "bar"))
313
312
  })
314
313
 
315
- const doc = await handle.doc()
314
+ const doc = handle.doc()
316
315
  assert.equal(handle.isReady(), true)
317
316
  assert.equal(doc?.foo, "bar")
318
317
  })
@@ -328,7 +327,7 @@ describe("DocHandle", () => {
328
327
  doc.foo = "bar"
329
328
  })
330
329
 
331
- const doc = await handle.doc()
330
+ const doc = handle.doc()
332
331
  assert.equal(doc?.foo, "bar")
333
332
 
334
333
  const changePayload = await p
@@ -353,7 +352,7 @@ describe("DocHandle", () => {
353
352
 
354
353
  const p = new Promise<void>(resolve =>
355
354
  handle.once("change", ({ handle, doc }) => {
356
- assert.equal(handle.docSync()?.foo, doc.foo)
355
+ assert.equal(handle.doc()?.foo, doc.foo)
357
356
 
358
357
  resolve()
359
358
  })
@@ -390,7 +389,7 @@ describe("DocHandle", () => {
390
389
  doc.foo = "baz"
391
390
  })
392
391
 
393
- const doc = await handle.doc()
392
+ const doc = handle.doc()
394
393
  assert.equal(doc?.foo, "baz")
395
394
 
396
395
  return p
@@ -405,7 +404,7 @@ describe("DocHandle", () => {
405
404
  })
406
405
 
407
406
  await p
408
- const doc = await handle.doc()
407
+ const doc = handle.doc()
409
408
  assert.equal(doc?.foo, "bar")
410
409
  })
411
410
 
@@ -425,11 +424,7 @@ describe("DocHandle", () => {
425
424
  // set docHandle time out after 5 ms
426
425
  const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
427
426
 
428
- const doc = await handle.doc()
429
-
430
- assert.equal(doc, undefined)
431
-
432
- assert.equal(handle.state, "unavailable")
427
+ expect(() => handle.doc()).toThrowError("DocHandle is not ready")
433
428
  })
434
429
 
435
430
  it("should not time out if the document is loaded in time", async () => {
@@ -440,11 +435,11 @@ describe("DocHandle", () => {
440
435
  handle.update(doc => docFromMockStorage(doc))
441
436
 
442
437
  // now it should not time out
443
- const doc = await handle.doc()
438
+ const doc = handle.doc()
444
439
  assert.equal(doc?.foo, "bar")
445
440
  })
446
441
 
447
- it("should be undefined if loading from the network times out", async () => {
442
+ it("should throw an exception if loading from the network times out", async () => {
448
443
  // set docHandle time out after 5 ms
449
444
  const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
450
445
 
@@ -454,8 +449,7 @@ describe("DocHandle", () => {
454
449
  // there's no update
455
450
  await pause(10)
456
451
 
457
- const doc = await handle.doc()
458
- assert.equal(doc, undefined)
452
+ expect(() => handle.doc()).toThrowError("DocHandle is not ready")
459
453
  })
460
454
 
461
455
  it("should not time out if the document is updated in time", async () => {
@@ -473,7 +467,7 @@ describe("DocHandle", () => {
473
467
  // now it should not time out
474
468
  await pause(5)
475
469
 
476
- const doc = await handle.doc()
470
+ const doc = handle.doc()
477
471
  assert.equal(doc?.foo, "bar")
478
472
  })
479
473
 
@@ -489,49 +483,6 @@ describe("DocHandle", () => {
489
483
  assert.equal(handle.isDeleted(), true)
490
484
  })
491
485
 
492
- it("should clear document reference when unloaded", async () => {
493
- const handle = setup()
494
-
495
- handle.change(doc => {
496
- doc.foo = "bar"
497
- })
498
- const doc = await handle.doc()
499
- assert.equal(doc?.foo, "bar")
500
-
501
- handle.unload()
502
- assert.equal(handle.isUnloaded(), true)
503
-
504
- const clearedDoc = await handle.doc([UNLOADED])
505
- assert.notEqual(clearedDoc?.foo, "bar")
506
- })
507
-
508
- it("should allow reloading after unloading", async () => {
509
- const handle = setup()
510
-
511
- handle.change(doc => {
512
- doc.foo = "bar"
513
- })
514
- const doc = await handle.doc()
515
- assert.equal(doc?.foo, "bar")
516
-
517
- handle.unload()
518
-
519
- // reload to transition from unloaded to loading
520
- handle.reload()
521
-
522
- // simulate requesting from the network
523
- handle.request()
524
-
525
- // simulate updating from the network
526
- handle.update(doc => {
527
- return A.change(doc, d => (d.foo = "bar"))
528
- })
529
-
530
- const reloadedDoc = await handle.doc()
531
- assert.equal(handle.isReady(), true)
532
- assert.equal(reloadedDoc?.foo, "bar")
533
- })
534
-
535
486
  it("should allow changing at old heads", async () => {
536
487
  const handle = setup()
537
488