@automerge/automerge-repo 2.0.0-alpha.7 → 2.0.0-beta.2

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.
Files changed (79) hide show
  1. package/README.md +8 -8
  2. package/dist/AutomergeUrl.d.ts +17 -5
  3. package/dist/AutomergeUrl.d.ts.map +1 -1
  4. package/dist/AutomergeUrl.js +71 -24
  5. package/dist/DocHandle.d.ts +68 -45
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +166 -69
  8. package/dist/FindProgress.d.ts +30 -0
  9. package/dist/FindProgress.d.ts.map +1 -0
  10. package/dist/FindProgress.js +1 -0
  11. package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
  12. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
  13. package/dist/RemoteHeadsSubscriptions.js +4 -1
  14. package/dist/Repo.d.ts +46 -6
  15. package/dist/Repo.d.ts.map +1 -1
  16. package/dist/Repo.js +252 -67
  17. package/dist/helpers/abortable.d.ts +36 -0
  18. package/dist/helpers/abortable.d.ts.map +1 -0
  19. package/dist/helpers/abortable.js +47 -0
  20. package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
  21. package/dist/helpers/bufferFromHex.d.ts +3 -0
  22. package/dist/helpers/bufferFromHex.d.ts.map +1 -0
  23. package/dist/helpers/bufferFromHex.js +13 -0
  24. package/dist/helpers/debounce.d.ts.map +1 -1
  25. package/dist/helpers/eventPromise.d.ts.map +1 -1
  26. package/dist/helpers/headsAreSame.d.ts +2 -2
  27. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  28. package/dist/helpers/mergeArrays.d.ts +1 -1
  29. package/dist/helpers/mergeArrays.d.ts.map +1 -1
  30. package/dist/helpers/pause.d.ts.map +1 -1
  31. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  32. package/dist/helpers/tests/network-adapter-tests.js +13 -13
  33. package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
  34. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  35. package/dist/helpers/tests/storage-adapter-tests.js +25 -48
  36. package/dist/helpers/throttle.d.ts.map +1 -1
  37. package/dist/helpers/withTimeout.d.ts.map +1 -1
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/network/messages.d.ts.map +1 -1
  42. package/dist/storage/StorageSubsystem.d.ts +15 -1
  43. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  44. package/dist/storage/StorageSubsystem.js +50 -14
  45. package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -3
  46. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  47. package/dist/synchronizer/CollectionSynchronizer.js +34 -15
  48. package/dist/synchronizer/DocSynchronizer.d.ts +3 -2
  49. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  50. package/dist/synchronizer/DocSynchronizer.js +51 -27
  51. package/dist/synchronizer/Synchronizer.d.ts +11 -0
  52. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  53. package/dist/types.d.ts +4 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/fuzz/fuzz.ts +3 -3
  56. package/package.json +3 -4
  57. package/src/AutomergeUrl.ts +101 -26
  58. package/src/DocHandle.ts +235 -82
  59. package/src/FindProgress.ts +48 -0
  60. package/src/RemoteHeadsSubscriptions.ts +11 -9
  61. package/src/Repo.ts +368 -74
  62. package/src/helpers/abortable.ts +62 -0
  63. package/src/helpers/bufferFromHex.ts +14 -0
  64. package/src/helpers/headsAreSame.ts +2 -2
  65. package/src/helpers/tests/network-adapter-tests.ts +14 -13
  66. package/src/helpers/tests/storage-adapter-tests.ts +44 -86
  67. package/src/index.ts +7 -0
  68. package/src/storage/StorageSubsystem.ts +66 -16
  69. package/src/synchronizer/CollectionSynchronizer.ts +37 -16
  70. package/src/synchronizer/DocSynchronizer.ts +59 -32
  71. package/src/synchronizer/Synchronizer.ts +14 -0
  72. package/src/types.ts +4 -1
  73. package/test/AutomergeUrl.test.ts +130 -0
  74. package/test/CollectionSynchronizer.test.ts +4 -4
  75. package/test/DocHandle.test.ts +181 -38
  76. package/test/DocSynchronizer.test.ts +10 -3
  77. package/test/Repo.test.ts +376 -203
  78. package/test/StorageSubsystem.test.ts +80 -1
  79. package/test/remoteHeads.test.ts +27 -12
package/test/Repo.test.ts CHANGED
@@ -3,8 +3,11 @@ import { MessageChannelNetworkAdapter } from "../../automerge-repo-network-messa
3
3
  import assert from "assert"
4
4
  import * as Uuid from "uuid"
5
5
  import { describe, expect, it } from "vitest"
6
- import { parseAutomergeUrl } from "../src/AutomergeUrl.js"
7
6
  import {
7
+ encodeHeads,
8
+ getHeadsFromUrl,
9
+ isValidAutomergeUrl,
10
+ parseAutomergeUrl,
8
11
  generateAutomergeUrl,
9
12
  stringifyAutomergeUrl,
10
13
  } from "../src/AutomergeUrl.js"
@@ -13,6 +16,7 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
13
16
  import { pause } from "../src/helpers/pause.js"
14
17
  import {
15
18
  AnyDocumentId,
19
+ UrlHeads,
16
20
  AutomergeUrl,
17
21
  DocHandle,
18
22
  DocumentId,
@@ -29,6 +33,7 @@ import {
29
33
  import { getRandomItem } from "./helpers/getRandomItem.js"
30
34
  import { TestDoc } from "./types.js"
31
35
  import { StorageId, StorageKey } from "../src/storage/types.js"
36
+ import { chunkTypeFromKey } from "../src/storage/chunkTypeFromKey.js"
32
37
 
33
38
  describe("Repo", () => {
34
39
  describe("constructor", () => {
@@ -72,35 +77,34 @@ describe("Repo", () => {
72
77
  it("can create a document with an initial value", async () => {
73
78
  const { repo } = setup()
74
79
  const handle = repo.create({ foo: "bar" })
75
- await handle.doc()
76
- assert.equal(handle.docSync().foo, "bar")
80
+ assert.equal(handle.doc().foo, "bar")
77
81
  })
78
82
 
79
- it("can find a document by url", () => {
83
+ it("can find a document by url", async () => {
80
84
  const { repo } = setup()
81
85
  const handle = repo.create<TestDoc>()
82
86
  handle.change((d: TestDoc) => {
83
87
  d.foo = "bar"
84
88
  })
85
89
 
86
- const handle2 = repo.find(handle.url)
90
+ const handle2 = await repo.find(handle.url)
87
91
  assert.equal(handle, handle2)
88
- assert.deepEqual(handle2.docSync(), { foo: "bar" })
92
+ assert.deepEqual(handle2.doc(), { foo: "bar" })
89
93
  })
90
94
 
91
- it("can find a document by its unprefixed document ID", () => {
95
+ it("can find a document by its unprefixed document ID", async () => {
92
96
  const { repo } = setup()
93
97
  const handle = repo.create<TestDoc>()
94
98
  handle.change((d: TestDoc) => {
95
99
  d.foo = "bar"
96
100
  })
97
101
 
98
- const handle2 = repo.find(handle.documentId)
102
+ const handle2 = await repo.find(handle.documentId)
99
103
  assert.equal(handle, handle2)
100
- assert.deepEqual(handle2.docSync(), { foo: "bar" })
104
+ assert.deepEqual(handle2.doc(), { foo: "bar" })
101
105
  })
102
106
 
103
- it("can find a document by legacy UUID (for now)", () => {
107
+ it("can find a document by legacy UUID (for now)", async () => {
104
108
  disableConsoleWarn()
105
109
 
106
110
  const { repo } = setup()
@@ -113,9 +117,9 @@ describe("Repo", () => {
113
117
  const { binaryDocumentId } = parseAutomergeUrl(url)
114
118
  const legacyDocId = Uuid.stringify(binaryDocumentId) as LegacyDocumentId
115
119
 
116
- const handle2 = repo.find(legacyDocId)
120
+ const handle2 = await repo.find(legacyDocId)
117
121
  assert.equal(handle, handle2)
118
- assert.deepEqual(handle2.docSync(), { foo: "bar" })
122
+ assert.deepEqual(handle2.doc(), { foo: "bar" })
119
123
 
120
124
  reenableConsoleWarn()
121
125
  })
@@ -126,7 +130,7 @@ describe("Repo", () => {
126
130
  handle.change(d => {
127
131
  d.foo = "bar"
128
132
  })
129
- const v = await handle.doc()
133
+ const v = handle.doc()
130
134
  assert.equal(handle.isReady(), true)
131
135
  assert.equal(v.foo, "bar")
132
136
  })
@@ -140,8 +144,8 @@ describe("Repo", () => {
140
144
  const handle2 = repo.clone(handle)
141
145
  assert.equal(handle2.isReady(), true)
142
146
  assert.notEqual(handle.documentId, handle2.documentId)
143
- assert.deepStrictEqual(handle.docSync(), handle2.docSync())
144
- assert.deepStrictEqual(handle2.docSync(), { foo: "bar" })
147
+ assert.deepStrictEqual(handle.doc(), handle2.doc())
148
+ assert.deepStrictEqual(handle2.doc(), { foo: "bar" })
145
149
  })
146
150
 
147
151
  it("the cloned documents are distinct", () => {
@@ -159,9 +163,9 @@ describe("Repo", () => {
159
163
  d.baz = "baz"
160
164
  })
161
165
 
162
- assert.notDeepStrictEqual(handle.docSync(), handle2.docSync())
163
- assert.deepStrictEqual(handle.docSync(), { foo: "bar", bar: "bif" })
164
- assert.deepStrictEqual(handle2.docSync(), { foo: "bar", baz: "baz" })
166
+ assert.notDeepStrictEqual(handle.doc(), handle2.doc())
167
+ assert.deepStrictEqual(handle.doc(), { foo: "bar", bar: "bif" })
168
+ assert.deepStrictEqual(handle2.doc(), { foo: "bar", baz: "baz" })
165
169
  })
166
170
 
167
171
  it("the cloned documents can merge", () => {
@@ -181,59 +185,47 @@ describe("Repo", () => {
181
185
 
182
186
  handle.merge(handle2)
183
187
 
184
- assert.deepStrictEqual(handle.docSync(), {
188
+ assert.deepStrictEqual(handle.doc(), {
185
189
  foo: "bar",
186
190
  bar: "bif",
187
191
  baz: "baz",
188
192
  })
189
193
  // only the one handle should be changed
190
- assert.deepStrictEqual(handle2.docSync(), { foo: "bar", baz: "baz" })
194
+ assert.deepStrictEqual(handle2.doc(), { foo: "bar", baz: "baz" })
191
195
  })
192
196
 
193
197
  it("throws an error if we try to find a handle with an invalid AutomergeUrl", async () => {
194
198
  const { repo } = setup()
195
- try {
196
- repo.find<TestDoc>("invalid-url" as unknown as AutomergeUrl)
197
- } catch (e: any) {
198
- assert.equal(e.message, "Invalid AutomergeUrl: 'invalid-url'")
199
- }
199
+ await expect(async () => {
200
+ await repo.find<TestDoc>("invalid-url" as unknown as AutomergeUrl)
201
+ }).rejects.toThrow("Invalid AutomergeUrl: 'invalid-url'")
200
202
  })
201
203
 
202
204
  it("doesn't find a document that doesn't exist", async () => {
203
205
  const { repo } = setup()
204
- const handle = repo.find<TestDoc>(generateAutomergeUrl())
205
-
206
- await handle.whenReady(["ready", "unavailable"])
207
-
208
- assert.equal(handle.isReady(), false)
209
- assert.equal(handle.state, "unavailable")
210
- const doc = await handle.doc()
211
- assert.equal(doc, undefined)
212
- })
213
-
214
- it("emits an unavailable event when you don't have the document locally and are not connected to anyone", async () => {
215
- const { repo } = setup()
216
- const url = generateAutomergeUrl()
217
- const handle = repo.find<TestDoc>(url)
218
- assert.equal(handle.isReady(), false)
219
- await eventPromise(handle, "unavailable")
206
+ await expect(async () => {
207
+ await repo.find<TestDoc>(generateAutomergeUrl())
208
+ }).rejects.toThrow(/Document (.*) is unavailable/)
220
209
  })
221
210
 
222
211
  it("doesn't mark a document as unavailable until network adapters are ready", async () => {
223
212
  const { repo, networkAdapter } = setup({ startReady: false })
224
213
  const url = generateAutomergeUrl()
225
- const handle = repo.find<TestDoc>(url)
226
214
 
227
- let wasUnavailable = false
228
- handle.on("unavailable", () => {
229
- wasUnavailable = true
230
- })
215
+ const attemptedFind = repo.find<TestDoc>(url)
231
216
 
232
- await pause(50)
233
- assert.equal(wasUnavailable, false)
217
+ // First verify it stays pending for 50ms
218
+ await expect(
219
+ Promise.race([attemptedFind, pause(50)])
220
+ ).resolves.toBeUndefined()
234
221
 
222
+ // Trigger the rejection
235
223
  networkAdapter.forceReady()
236
- await eventPromise(handle, "unavailable")
224
+
225
+ // Now verify it rejects
226
+ await expect(attemptedFind).rejects.toThrow(
227
+ /Document (.*) is unavailable/
228
+ )
237
229
  })
238
230
 
239
231
  it("can find a created document", async () => {
@@ -244,18 +236,18 @@ describe("Repo", () => {
244
236
  })
245
237
  assert.equal(handle.isReady(), true)
246
238
 
247
- const bobHandle = repo.find<TestDoc>(handle.url)
239
+ const bobHandle = await repo.find<TestDoc>(handle.url)
248
240
 
249
241
  assert.equal(handle, bobHandle)
250
242
  assert.equal(handle.isReady(), true)
251
243
 
252
- const v = await bobHandle.doc()
244
+ const v = bobHandle.doc()
253
245
  assert.equal(v?.foo, "bar")
254
246
  })
255
247
 
256
248
  it("saves the document when creating it", async () => {
257
249
  const { repo, storageAdapter } = setup()
258
- const handle = repo.create<TestDoc>()
250
+ const handle = repo.create<TestDoc>({ foo: "saved" })
259
251
 
260
252
  const repo2 = new Repo({
261
253
  storage: storageAdapter,
@@ -263,9 +255,8 @@ describe("Repo", () => {
263
255
 
264
256
  await repo.flush()
265
257
 
266
- const bobHandle = repo2.find<TestDoc>(handle.url)
267
- await bobHandle.whenReady()
268
- assert.equal(bobHandle.isReady(), true)
258
+ const bobHandle = await repo2.find<TestDoc>(handle.url)
259
+ assert.deepEqual(bobHandle.doc(), { foo: "saved" })
269
260
  })
270
261
 
271
262
  it("saves the document when changed and can find it again", async () => {
@@ -284,9 +275,9 @@ describe("Repo", () => {
284
275
  storage: storageAdapter,
285
276
  })
286
277
 
287
- const bobHandle = repo2.find<TestDoc>(handle.url)
278
+ const bobHandle = await repo2.find<TestDoc>(handle.url)
288
279
 
289
- const v = await bobHandle.doc()
280
+ const v = bobHandle.doc()
290
281
  assert.equal(v?.foo, "bar")
291
282
  })
292
283
 
@@ -298,7 +289,7 @@ describe("Repo", () => {
298
289
  })
299
290
  // we now have a snapshot and an incremental change in storage
300
291
  assert.equal(handle.isReady(), true)
301
- const foo = await handle.doc()
292
+ const foo = handle.doc()
302
293
  assert.equal(foo?.foo, "bar")
303
294
 
304
295
  await pause()
@@ -315,7 +306,6 @@ describe("Repo", () => {
315
306
  d.foo = "bar"
316
307
  })
317
308
  assert.equal(handle.isReady(), true)
318
- await handle.doc()
319
309
 
320
310
  await pause()
321
311
  repo.delete(handle.url)
@@ -352,7 +342,7 @@ describe("Repo", () => {
352
342
 
353
343
  const exported = await repo.export(handle.documentId)
354
344
  const loaded = A.load(exported)
355
- const doc = await handle.doc()
345
+ const doc = handle.doc()
356
346
  assert.deepEqual(doc, loaded)
357
347
  })
358
348
 
@@ -386,9 +376,7 @@ describe("Repo", () => {
386
376
  const repo2 = new Repo({
387
377
  storage,
388
378
  })
389
- const handle2 = repo2.find(handle.url)
390
- await handle2.doc()
391
-
379
+ const handle2 = await repo2.find(handle.url)
392
380
  assert.deepEqual(storage.keys(), initialKeys)
393
381
  })
394
382
 
@@ -414,9 +402,7 @@ describe("Repo", () => {
414
402
  const repo2 = new Repo({
415
403
  storage,
416
404
  })
417
- const handle2 = repo2.find(handle.url)
418
- await handle2.doc()
419
-
405
+ const handle2 = await repo2.find(handle.url)
420
406
  assert(storage.keys().length !== 0)
421
407
  }
422
408
  })
@@ -445,6 +431,40 @@ describe("Repo", () => {
445
431
  )
446
432
  })
447
433
 
434
+ it("should not call loadDoc multiple times when find() is called in quick succession", async () => {
435
+ const { repo, storageAdapter } = setup()
436
+ const handle = repo.create<TestDoc>()
437
+ handle.change(d => {
438
+ d.foo = "bar"
439
+ })
440
+ await repo.flush()
441
+
442
+ // Create a new repo instance that will use the same storage
443
+ const repo2 = new Repo({
444
+ storage: storageAdapter,
445
+ })
446
+
447
+ // Track how many times loadDoc is called
448
+ let loadDocCallCount = 0
449
+ const originalLoadDoc = repo2.storageSubsystem!.loadDoc.bind(
450
+ repo2.storageSubsystem
451
+ )
452
+ repo2.storageSubsystem!.loadDoc = async documentId => {
453
+ loadDocCallCount++
454
+ return originalLoadDoc(documentId)
455
+ }
456
+
457
+ // Call find() twice in quick succession
458
+ const find1 = repo2.find(handle.url)
459
+ const find2 = repo2.find(handle.url)
460
+
461
+ // Wait for both calls to complete
462
+ await Promise.all([find1, find2])
463
+
464
+ // Verify loadDoc was only called once
465
+ assert.equal(loadDocCallCount, 1, "loadDoc should only be called once")
466
+ })
467
+
448
468
  it("can import an existing document", async () => {
449
469
  const { repo } = setup()
450
470
  const doc = A.init<TestDoc>()
@@ -456,7 +476,7 @@ describe("Repo", () => {
456
476
 
457
477
  const handle = repo.import<TestDoc>(saved)
458
478
  assert.equal(handle.isReady(), true)
459
- const v = await handle.doc()
479
+ const v = handle.doc()
460
480
  assert.equal(v?.foo, "bar")
461
481
 
462
482
  expect(A.getHistory(v)).toEqual(A.getHistory(updatedDoc))
@@ -475,7 +495,7 @@ describe("Repo", () => {
475
495
  const { repo } = setup()
476
496
  // @ts-ignore - passing something other than UInt8Array
477
497
  const handle = repo.import<TestDoc>(A.from({ foo: 123 }))
478
- const doc = await handle.doc()
498
+ const doc = handle.doc()
479
499
  expect(doc).toEqual({})
480
500
  })
481
501
 
@@ -483,9 +503,39 @@ describe("Repo", () => {
483
503
  const { repo } = setup()
484
504
  // @ts-ignore - passing something other than UInt8Array
485
505
  const handle = repo.import<TestDoc>({ foo: 123 })
486
- const doc = await handle.doc()
506
+ const doc = handle.doc()
487
507
  expect(doc).toEqual({})
488
508
  })
509
+
510
+ describe("handle cache", () => {
511
+ it("contains doc handle", async () => {
512
+ const { repo } = setup()
513
+ const handle = repo.create({ foo: "bar" })
514
+ assert(repo.handles[handle.documentId])
515
+ })
516
+
517
+ it("delete removes doc handle", async () => {
518
+ const { repo } = setup()
519
+ const handle = repo.create({ foo: "bar" })
520
+ await repo.delete(handle.documentId)
521
+ assert(repo.handles[handle.documentId] === undefined)
522
+ })
523
+
524
+ it("removeFromCache removes doc handle", async () => {
525
+ const { repo } = setup()
526
+ const handle = repo.create({ foo: "bar" })
527
+ await repo.removeFromCache(handle.documentId)
528
+ assert(repo.handles[handle.documentId] === undefined)
529
+ })
530
+
531
+ it("removeFromCache for documentId not found", async () => {
532
+ const { repo } = setup()
533
+ const badDocumentId = "badbadbad" as DocumentId
534
+ const handleCacheSize = Object.keys(repo.handles).length
535
+ await repo.removeFromCache(badDocumentId)
536
+ assert(Object.keys(repo.handles).length === handleCacheSize)
537
+ })
538
+ })
489
539
  })
490
540
 
491
541
  describe("flush behaviour", () => {
@@ -532,8 +582,8 @@ describe("Repo", () => {
532
582
 
533
583
  it("should not be in a new repo yet because the storage is slow", async () => {
534
584
  const { pausedStorage, repo, handle, handle2 } = setup()
535
- expect((await handle.doc()).foo).toEqual("first")
536
- expect((await handle2.doc()).foo).toEqual("second")
585
+ expect((await handle).doc().foo).toEqual("first")
586
+ expect((await handle2).doc().foo).toEqual("second")
537
587
 
538
588
  // Reload repo
539
589
  const repo2 = new Repo({
@@ -541,9 +591,10 @@ describe("Repo", () => {
541
591
  })
542
592
 
543
593
  // Could not find the document that is not yet saved because of slow storage.
544
- const reloadedHandle = repo2.find<{ foo: string }>(handle.url)
594
+ await expect(async () => {
595
+ const reloadedHandle = await repo2.find<{ foo: string }>(handle.url)
596
+ }).rejects.toThrow(/Document (.*) is unavailable/)
545
597
  expect(pausedStorage.keys()).to.deep.equal([])
546
- expect(await reloadedHandle.doc()).toEqual(undefined)
547
598
  })
548
599
 
549
600
  it("should be visible to a new repo after flush()", async () => {
@@ -563,10 +614,10 @@ describe("Repo", () => {
563
614
  })
564
615
 
565
616
  expect(
566
- (await repo.find<{ foo: string }>(handle.documentId).doc()).foo
617
+ (await repo.find<{ foo: string }>(handle.documentId)).doc().foo
567
618
  ).toEqual("first")
568
619
  expect(
569
- (await repo.find<{ foo: string }>(handle2.documentId).doc()).foo
620
+ (await repo.find<{ foo: string }>(handle2.documentId)).doc().foo
570
621
  ).toEqual("second")
571
622
  }
572
623
  })
@@ -588,13 +639,13 @@ describe("Repo", () => {
588
639
  })
589
640
 
590
641
  expect(
591
- (await repo.find<{ foo: string }>(handle.documentId).doc()).foo
642
+ (await repo.find<{ foo: string }>(handle.documentId)).doc().foo
592
643
  ).toEqual("first")
593
644
  // Really, it's okay if the second one is also flushed but I'm forcing the issue
594
645
  // in the test storage engine above to make sure the behaviour is as documented
595
- expect(
596
- await repo.find<{ foo: string }>(handle2.documentId).doc()
597
- ).toEqual(undefined)
646
+ await expect(async () => {
647
+ ;(await repo.find<{ foo: string }>(handle2.documentId)).doc()
648
+ }).rejects.toThrow(/Document (.*) is unavailable/)
598
649
  }
599
650
  })
600
651
 
@@ -642,7 +693,7 @@ describe("Repo", () => {
642
693
 
643
694
  if (idx < numberOfPeers - 1) {
644
695
  network.push(pair[0])
645
- pair[0].whenReady()
696
+ networkReady.push(pair[0].whenReady())
646
697
  }
647
698
 
648
699
  const repo = new Repo({
@@ -673,7 +724,6 @@ describe("Repo", () => {
673
724
  }
674
725
 
675
726
  await connectedPromise
676
-
677
727
  return { repos }
678
728
  }
679
729
 
@@ -685,10 +735,14 @@ describe("Repo", () => {
685
735
  d.foo = "bar"
686
736
  })
687
737
 
688
- const handleN = repos[numberOfPeers - 1].find<TestDoc>(handle0.url)
738
+ const handleN = await repos[numberOfPeers - 1].find<TestDoc>(handle0.url)
739
+ assert.deepStrictEqual(handleN.doc(), { foo: "bar" })
689
740
 
690
- await handleN.whenReady()
691
- assert.deepStrictEqual(handleN.docSync(), { foo: "bar" })
741
+ const handleNBack = repos[numberOfPeers - 1].create({
742
+ foo: "reverse-trip",
743
+ })
744
+ const handle0Back = await repos[0].find<TestDoc>(handleNBack.url)
745
+ assert.deepStrictEqual(handle0Back.doc(), { foo: "reverse-trip" })
692
746
  })
693
747
 
694
748
  const setup = async ({
@@ -815,9 +869,8 @@ describe("Repo", () => {
815
869
  it("changes are replicated from aliceRepo to bobRepo", async () => {
816
870
  const { bobRepo, aliceHandle, teardown } = await setup()
817
871
 
818
- const bobHandle = bobRepo.find<TestDoc>(aliceHandle.url)
819
- await eventPromise(bobHandle, "change")
820
- const bobDoc = await bobHandle.doc()
872
+ const bobHandle = await bobRepo.find<TestDoc>(aliceHandle.url)
873
+ const bobDoc = bobHandle.doc()
821
874
  assert.deepStrictEqual(bobDoc, { foo: "bar" })
822
875
  teardown()
823
876
  })
@@ -825,16 +878,14 @@ describe("Repo", () => {
825
878
  it("can load a document from aliceRepo on charlieRepo", async () => {
826
879
  const { charlieRepo, aliceHandle, teardown } = await setup()
827
880
 
828
- const handle3 = charlieRepo.find<TestDoc>(aliceHandle.url)
829
- await eventPromise(handle3, "change")
830
- const doc3 = await handle3.doc()
881
+ const handle3 = await charlieRepo.find<TestDoc>(aliceHandle.url)
882
+ const doc3 = handle3.doc()
831
883
  assert.deepStrictEqual(doc3, { foo: "bar" })
832
884
  teardown()
833
885
  })
834
886
 
835
887
  it("synchronizes changes from bobRepo to charlieRepo when loading from storage", async () => {
836
- const { bobRepo, bobStorage, charlieRepo, aliceHandle, teardown } =
837
- await setup()
888
+ const { bobRepo, bobStorage, teardown } = await setup()
838
889
 
839
890
  // We create a repo that uses bobStorage to put a document into its imaginary disk
840
891
  // without it knowing about it
@@ -846,14 +897,14 @@ describe("Repo", () => {
846
897
  })
847
898
  await bobRepo2.flush()
848
899
 
849
- console.log("loading from disk", inStorageHandle.url)
850
900
  // Now, let's load it on the original bob repo (which shares a "disk")
851
- const bobFoundIt = bobRepo.find<TestDoc>(inStorageHandle.url)
852
- await bobFoundIt.whenReady()
901
+ const bobFoundIt = await bobRepo.find<TestDoc>(inStorageHandle.url)
853
902
 
854
903
  // Before checking if it syncs, make sure we have it!
855
904
  // (This behaviour is mostly test-validation, we are already testing load/save elsewhere.)
856
- assert.deepStrictEqual(await bobFoundIt.doc(), { foo: "foundOnFakeDisk" })
905
+ assert.deepStrictEqual(bobFoundIt.doc(), { foo: "foundOnFakeDisk" })
906
+
907
+ await pause(10)
857
908
 
858
909
  // We should have a docSynchronizer and its peers should be alice and charlie
859
910
  assert.strictEqual(
@@ -891,11 +942,8 @@ describe("Repo", () => {
891
942
  it("charlieRepo can request a document not initially shared with it", async () => {
892
943
  const { charlieRepo, notForCharlie, teardown } = await setup()
893
944
 
894
- const handle = charlieRepo.find<TestDoc>(notForCharlie)
895
-
896
- await pause(50)
897
-
898
- const doc = await handle.doc()
945
+ const handle = await charlieRepo.find<TestDoc>(notForCharlie)
946
+ const doc = handle.doc()
899
947
 
900
948
  assert.deepStrictEqual(doc, { foo: "baz" })
901
949
 
@@ -905,11 +953,11 @@ describe("Repo", () => {
905
953
  it("charlieRepo can request a document across a network of multiple peers", async () => {
906
954
  const { charlieRepo, notForBob, teardown } = await setup()
907
955
 
908
- const handle = charlieRepo.find<TestDoc>(notForBob)
956
+ const handle = await charlieRepo.find<TestDoc>(notForBob)
909
957
 
910
958
  await pause(50)
911
959
 
912
- const doc = await handle.doc()
960
+ const doc = handle.doc()
913
961
  assert.deepStrictEqual(doc, { foo: "bap" })
914
962
 
915
963
  teardown()
@@ -918,42 +966,10 @@ describe("Repo", () => {
918
966
  it("doesn't find a document which doesn't exist anywhere on the network", async () => {
919
967
  const { charlieRepo, teardown } = await setup()
920
968
  const url = generateAutomergeUrl()
921
- const handle = charlieRepo.find<TestDoc>(url)
922
- assert.equal(handle.isReady(), false)
923
-
924
- const doc = await handle.doc()
925
- assert.equal(doc, undefined)
926
-
927
- teardown()
928
- })
929
-
930
- it("emits an unavailable event when it's not found on the network", async () => {
931
- const { aliceRepo, teardown } = await setup()
932
- const url = generateAutomergeUrl()
933
- const handle = aliceRepo.find(url)
934
- assert.equal(handle.isReady(), false)
935
- await eventPromise(handle, "unavailable")
936
- teardown()
937
- })
938
-
939
- it("emits an unavailable event every time an unavailable doc is requested", async () => {
940
- const { charlieRepo, teardown } = await setup()
941
- const url = generateAutomergeUrl()
942
- const handle = charlieRepo.find<TestDoc>(url)
943
- assert.equal(handle.isReady(), false)
944
969
 
945
- await Promise.all([
946
- eventPromise(handle, "unavailable"),
947
- eventPromise(charlieRepo, "unavailable-document"),
948
- ])
949
-
950
- // make sure it emits a second time if the doc is still unavailable
951
- const handle2 = charlieRepo.find<TestDoc>(url)
952
- assert.equal(handle2.isReady(), false)
953
- await Promise.all([
954
- eventPromise(handle, "unavailable"),
955
- eventPromise(charlieRepo, "unavailable-document"),
956
- ])
970
+ await expect(charlieRepo.find<TestDoc>(url)).rejects.toThrow(
971
+ /Document (.*) is unavailable/
972
+ )
957
973
 
958
974
  teardown()
959
975
  })
@@ -968,21 +984,23 @@ describe("Repo", () => {
968
984
  } = await setup({ connectAlice: false })
969
985
 
970
986
  const url = stringifyAutomergeUrl({ documentId: notForCharlie })
971
- const handle = charlieRepo.find<TestDoc>(url)
972
- assert.equal(handle.isReady(), false)
973
-
974
- await eventPromise(handle, "unavailable")
987
+ await expect(charlieRepo.find<TestDoc>(url)).rejects.toThrow(
988
+ /Document (.*) is unavailable/
989
+ )
975
990
 
976
991
  connectAliceToBob()
977
992
 
978
993
  await eventPromise(aliceRepo.networkSubsystem, "peer")
979
994
 
980
- const doc = await handle.doc(["ready"])
995
+ // Not sure why we need this pause here, but... we do.
996
+ await pause(150)
997
+ const handle = await charlieRepo.find<TestDoc>(url)
998
+ const doc = handle.doc()
981
999
  assert.deepStrictEqual(doc, { foo: "baz" })
982
1000
 
983
1001
  // an additional find should also return the correct resolved document
984
- const handle2 = charlieRepo.find<TestDoc>(url)
985
- const doc2 = await handle2.doc()
1002
+ const handle2 = await charlieRepo.find<TestDoc>(url)
1003
+ const doc2 = handle2.doc()
986
1004
  assert.deepStrictEqual(doc2, { foo: "baz" })
987
1005
 
988
1006
  teardown()
@@ -1018,11 +1036,9 @@ describe("Repo", () => {
1018
1036
  sharePolicy: async () => true,
1019
1037
  })
1020
1038
 
1021
- const handle = a.find(url)
1022
-
1023
- // We expect this to be unavailable as there is no connected peer and
1024
- // the repo has no storage.
1025
- await eventPromise(handle, "unavailable")
1039
+ await expect(a.find<TestDoc>(url)).rejects.toThrow(
1040
+ /Document (.*) is unavailable/
1041
+ )
1026
1042
 
1027
1043
  // Now create a repo pointing at the storage containing the document and
1028
1044
  // connect it to the other end of the MessageChannel
@@ -1032,9 +1048,14 @@ describe("Repo", () => {
1032
1048
  network: [new MessageChannelNetworkAdapter(ba)],
1033
1049
  })
1034
1050
 
1051
+ // We need a proper peer status API so we can tell when the
1052
+ // peer is connected. For now we just wait a bit.
1053
+ await pause(50)
1054
+
1035
1055
  // The empty repo should be notified of the new peer, send it a request
1036
1056
  // and eventually resolve the handle to "READY"
1037
- await handle.whenReady()
1057
+ const handle = await a.find<TestDoc>(url)
1058
+ expect(handle.state).toBe("ready")
1038
1059
  })
1039
1060
 
1040
1061
  it("a deleted document from charlieRepo can be refetched", async () => {
@@ -1050,9 +1071,8 @@ describe("Repo", () => {
1050
1071
  })
1051
1072
  await changePromise
1052
1073
 
1053
- const handle3 = charlieRepo.find<TestDoc>(aliceHandle.url)
1054
- await eventPromise(handle3, "change")
1055
- const doc3 = await handle3.doc()
1074
+ const handle3 = await charlieRepo.find<TestDoc>(aliceHandle.url)
1075
+ const doc3 = handle3.doc()
1056
1076
 
1057
1077
  assert.deepStrictEqual(doc3, { foo: "baz" })
1058
1078
 
@@ -1076,11 +1096,6 @@ describe("Repo", () => {
1076
1096
  : // tails, pick a random doc
1077
1097
  (getRandomItem(docs) as DocHandle<TestDoc>)
1078
1098
 
1079
- // make sure the doc is ready
1080
- if (!doc.isReady()) {
1081
- await doc.doc()
1082
- }
1083
-
1084
1099
  // make a random change to it
1085
1100
  doc.change(d => {
1086
1101
  d.foo = Math.random().toString()
@@ -1096,10 +1111,10 @@ describe("Repo", () => {
1096
1111
 
1097
1112
  const data = { presence: "alice" }
1098
1113
 
1099
- const aliceHandle = aliceRepo.find<TestDoc>(
1114
+ const aliceHandle = await aliceRepo.find<TestDoc>(
1100
1115
  stringifyAutomergeUrl({ documentId: notForCharlie })
1101
1116
  )
1102
- const bobHandle = bobRepo.find<TestDoc>(
1117
+ const bobHandle = await bobRepo.find<TestDoc>(
1103
1118
  stringifyAutomergeUrl({ documentId: notForCharlie })
1104
1119
  )
1105
1120
 
@@ -1142,7 +1157,10 @@ describe("Repo", () => {
1142
1157
  bobHandle.documentId,
1143
1158
  await charlieRepo!.storageSubsystem.id()
1144
1159
  )
1145
- assert.deepStrictEqual(storedSyncState.sharedHeads, bobHandle.heads())
1160
+ assert.deepStrictEqual(
1161
+ encodeHeads(storedSyncState.sharedHeads),
1162
+ bobHandle.heads()
1163
+ )
1146
1164
 
1147
1165
  teardown()
1148
1166
  })
@@ -1242,15 +1260,14 @@ describe("Repo", () => {
1242
1260
 
1243
1261
  const nextRemoteHeadsPromise = new Promise<{
1244
1262
  storageId: StorageId
1245
- heads: A.Heads
1263
+ heads: UrlHeads
1246
1264
  }>(resolve => {
1247
1265
  handle.on("remote-heads", ({ storageId, heads }) => {
1248
1266
  resolve({ storageId, heads })
1249
1267
  })
1250
1268
  })
1251
1269
 
1252
- const charlieHandle = charlieRepo.find<TestDoc>(handle.url)
1253
- await charlieHandle.whenReady()
1270
+ const charlieHandle = await charlieRepo.find<TestDoc>(handle.url)
1254
1271
 
1255
1272
  // make a change on charlie
1256
1273
  charlieHandle.change(d => {
@@ -1287,34 +1304,6 @@ describe("Repo", () => {
1287
1304
  })
1288
1305
  })
1289
1306
 
1290
- it("peer receives a document when connection is recovered", async () => {
1291
- const alice = "alice" as PeerId
1292
- const bob = "bob" as PeerId
1293
- const [aliceAdapter, bobAdapter] = DummyNetworkAdapter.createConnectedPair()
1294
- const aliceRepo = new Repo({
1295
- network: [aliceAdapter],
1296
- peerId: alice,
1297
- })
1298
- const bobRepo = new Repo({
1299
- network: [bobAdapter],
1300
- peerId: bob,
1301
- })
1302
- const aliceDoc = aliceRepo.create()
1303
- aliceDoc.change((doc: any) => (doc.text = "Hello world"))
1304
-
1305
- const bobDoc = bobRepo.find(aliceDoc.url)
1306
- await eventPromise(bobDoc, "unavailable")
1307
-
1308
- aliceAdapter.peerCandidate(bob)
1309
- // Bob isn't yet connected to Alice and can't respond to her sync message
1310
- await pause(100)
1311
- bobAdapter.peerCandidate(alice)
1312
-
1313
- await bobDoc.whenReady()
1314
-
1315
- assert.equal(bobDoc.isReady(), true)
1316
- })
1317
-
1318
1307
  describe("with peers (mesh network)", () => {
1319
1308
  const setup = async () => {
1320
1309
  // Set up three repos; connect Alice to Bob, Bob to Charlie, and Alice to Charlie
@@ -1376,8 +1365,8 @@ describe("Repo", () => {
1376
1365
 
1377
1366
  const aliceHandle = aliceRepo.create<TestDoc>()
1378
1367
 
1379
- const bobHandle = bobRepo.find(aliceHandle.url)
1380
- const charlieHandle = charlieRepo.find(aliceHandle.url)
1368
+ const bobHandle = await bobRepo.find(aliceHandle.url)
1369
+ const charlieHandle = await charlieRepo.find(aliceHandle.url)
1381
1370
 
1382
1371
  // Alice should not receive her own ephemeral message
1383
1372
  aliceHandle.on("ephemeral-message", () => {
@@ -1415,9 +1404,8 @@ describe("Repo", () => {
1415
1404
  // pause to let the sync happen
1416
1405
  await pause(50)
1417
1406
 
1418
- const charlieHandle = charlieRepo.find(handle2.url)
1419
- await charlieHandle.doc()
1420
- assert.deepStrictEqual(charlieHandle.docSync(), { foo: "bar" })
1407
+ const charlieHandle = await charlieRepo.find(handle2.url)
1408
+ assert.deepStrictEqual(charlieHandle.doc(), { foo: "bar" })
1421
1409
 
1422
1410
  teardown()
1423
1411
  })
@@ -1434,9 +1422,8 @@ describe("Repo", () => {
1434
1422
  // pause to let the sync happen
1435
1423
  await pause(50)
1436
1424
 
1437
- const charlieHandle = charlieRepo.find(handle2.url)
1438
- await charlieHandle.doc()
1439
- assert.deepStrictEqual(charlieHandle.docSync(), { foo: "bar" })
1425
+ const charlieHandle = await charlieRepo.find(handle2.url)
1426
+ assert.deepStrictEqual(charlieHandle.doc(), { foo: "bar" })
1440
1427
 
1441
1428
  // now make a change to doc2 on bobs side and merge it into doc1
1442
1429
  handle2.change(d => {
@@ -1447,12 +1434,198 @@ describe("Repo", () => {
1447
1434
  // wait for the network to do it's thang
1448
1435
  await pause(350)
1449
1436
 
1450
- await charlieHandle.doc()
1451
- assert.deepStrictEqual(charlieHandle.docSync(), { foo: "baz" })
1437
+ assert.deepStrictEqual(charlieHandle.doc(), { foo: "baz" })
1452
1438
 
1453
1439
  teardown()
1454
1440
  })
1455
1441
  })
1442
+
1443
+ describe("the denylist", () => {
1444
+ it("should immediately return an unavailable message in response to a request for a denylisted document", async () => {
1445
+ const storage = new DummyStorageAdapter()
1446
+
1447
+ // first create the document in storage
1448
+ const dummyRepo = new Repo({ network: [], storage })
1449
+ const doc = dummyRepo.create({ foo: "bar" })
1450
+ await dummyRepo.flush()
1451
+
1452
+ // Check that the document actually is in storage
1453
+ let docId = doc.documentId
1454
+ assert(storage.keys().some((k: string) => k.includes(docId)))
1455
+
1456
+ const channel = new MessageChannel()
1457
+ const { port1: clientToServer, port2: serverToClient } = channel
1458
+ const server = new Repo({
1459
+ network: [new MessageChannelNetworkAdapter(serverToClient)],
1460
+ storage,
1461
+ denylist: [doc.url],
1462
+ })
1463
+ const client = new Repo({
1464
+ network: [new MessageChannelNetworkAdapter(clientToServer)],
1465
+ })
1466
+
1467
+ await Promise.all([
1468
+ eventPromise(server.networkSubsystem, "peer"),
1469
+ eventPromise(client.networkSubsystem, "peer"),
1470
+ ])
1471
+
1472
+ await expect(async () => {
1473
+ const clientDoc = await client.find(doc.url)
1474
+ }).rejects.toThrow(/Document (.*) is unavailable/)
1475
+
1476
+ const openDocs = Object.keys(server.metrics().documents).length
1477
+ assert.deepEqual(openDocs, 0)
1478
+ })
1479
+ })
1480
+ })
1481
+
1482
+ describe("Repo heads-in-URLs functionality", () => {
1483
+ const setup = () => {
1484
+ const repo = new Repo({})
1485
+ const handle = repo.create()
1486
+ handle.change((doc: any) => (doc.title = "Hello World"))
1487
+ return { repo, handle }
1488
+ }
1489
+
1490
+ it("finds a document view by URL with heads", async () => {
1491
+ const { repo, handle } = setup()
1492
+ const heads = handle.heads()!
1493
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1494
+ const view = await repo.find(url)
1495
+ expect(view.doc()).toEqual({ title: "Hello World" })
1496
+ })
1497
+
1498
+ it("returns a view, not the actual handle, when finding by URL with heads", async () => {
1499
+ const { repo, handle } = setup()
1500
+ const heads = handle.heads()!
1501
+ await handle.change((doc: any) => (doc.title = "Changed"))
1502
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1503
+ const view = await repo.find(url)
1504
+ expect(view.doc()).toEqual({ title: "Hello World" })
1505
+ expect(handle.doc()).toEqual({ title: "Changed" })
1506
+ })
1507
+
1508
+ it("changes to a document view do not affect the original", async () => {
1509
+ const { repo, handle } = setup()
1510
+ const heads = handle.heads()!
1511
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1512
+ const view = await repo.find(url)
1513
+ expect(() =>
1514
+ view.change((doc: any) => (doc.title = "Changed in View"))
1515
+ ).toThrow()
1516
+ expect(handle.doc()).toEqual({ title: "Hello World" })
1517
+ })
1518
+
1519
+ it("document views are read-only", async () => {
1520
+ const { repo, handle } = setup()
1521
+ const heads = handle.heads()!
1522
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1523
+ const view = await repo.find(url)
1524
+ expect(() => view.change((doc: any) => (doc.title = "Changed"))).toThrow()
1525
+ })
1526
+
1527
+ it("finds the latest document when given a URL without heads", async () => {
1528
+ const { repo, handle } = setup()
1529
+ await handle.change((doc: any) => (doc.title = "Changed"))
1530
+ const found = await repo.find(handle.url)
1531
+ expect(found.doc()).toEqual({ title: "Changed" })
1532
+ })
1533
+
1534
+ it("getHeadsFromUrl returns heads array if present or undefined", () => {
1535
+ const { repo, handle } = setup()
1536
+ const heads = handle.heads()!
1537
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1538
+ expect(getHeadsFromUrl(url)).toEqual(heads)
1539
+
1540
+ const urlWithoutHeads = generateAutomergeUrl()
1541
+ expect(getHeadsFromUrl(urlWithoutHeads)).toBeUndefined()
1542
+ })
1543
+
1544
+ it("isValidAutomergeUrl returns true for valid URLs", () => {
1545
+ const { repo, handle } = setup()
1546
+ const url = generateAutomergeUrl()
1547
+ expect(isValidAutomergeUrl(url)).toBe(true)
1548
+
1549
+ const urlWithHeads = stringifyAutomergeUrl({
1550
+ documentId: handle.documentId,
1551
+ heads: handle.heads()!,
1552
+ })
1553
+ expect(isValidAutomergeUrl(urlWithHeads)).toBe(true)
1554
+ })
1555
+
1556
+ it("isValidAutomergeUrl returns false for invalid URLs", () => {
1557
+ const { repo, handle } = setup()
1558
+ expect(isValidAutomergeUrl("not a url")).toBe(false)
1559
+ expect(isValidAutomergeUrl("automerge:invalidid")).toBe(false)
1560
+ expect(isValidAutomergeUrl("automerge:validid#invalidhead")).toBe(false)
1561
+ })
1562
+
1563
+ it("parseAutomergeUrl extracts documentId and heads", () => {
1564
+ const { repo, handle } = setup()
1565
+ const url = stringifyAutomergeUrl({
1566
+ documentId: handle.documentId,
1567
+ heads: handle.heads()!,
1568
+ })
1569
+ const parsed = parseAutomergeUrl(url)
1570
+ expect(parsed.documentId).toBe(handle.documentId)
1571
+ expect(parsed.heads).toEqual(handle.heads())
1572
+ })
1573
+
1574
+ it("stringifyAutomergeUrl creates valid URL", () => {
1575
+ const { repo, handle } = setup()
1576
+ const url = stringifyAutomergeUrl({
1577
+ documentId: handle.documentId,
1578
+ heads: handle.heads()!,
1579
+ })
1580
+ expect(isValidAutomergeUrl(url)).toBe(true)
1581
+ const parsed = parseAutomergeUrl(url)
1582
+ expect(parsed.documentId).toBe(handle.documentId)
1583
+ expect(parsed.heads).toEqual(handle.heads())
1584
+ })
1585
+ })
1586
+
1587
+ describe("Repo.find() abort behavior", () => {
1588
+ it("aborts immediately if signal is already aborted", async () => {
1589
+ const repo = new Repo()
1590
+ const controller = new AbortController()
1591
+ controller.abort()
1592
+
1593
+ await expect(
1594
+ repo.find(generateAutomergeUrl(), { signal: controller.signal })
1595
+ ).rejects.toThrow("Operation aborted")
1596
+ })
1597
+
1598
+ it("can abort while waiting for ready state", async () => {
1599
+ // Create a repo with no network adapters so document can't become ready
1600
+ const repo = new Repo()
1601
+ const url = generateAutomergeUrl()
1602
+
1603
+ const controller = new AbortController()
1604
+
1605
+ // Start find and abort after a moment
1606
+ const findPromise = repo.find(url, { signal: controller.signal })
1607
+ controller.abort()
1608
+
1609
+ await expect(findPromise).rejects.toThrow("Operation aborted")
1610
+ await expect(findPromise).rejects.not.toThrow("unavailable")
1611
+ })
1612
+
1613
+ it("returns handle immediately when allow unavailable is true, even with abort signal", async () => {
1614
+ const repo = new Repo()
1615
+ const controller = new AbortController()
1616
+ const url = generateAutomergeUrl()
1617
+
1618
+ const handle = await repo.find(url, {
1619
+ allowableStates: ["unavailable"],
1620
+ signal: controller.signal,
1621
+ })
1622
+
1623
+ expect(handle).toBeDefined()
1624
+
1625
+ // Abort shouldn't affect the result since we skipped ready
1626
+ controller.abort()
1627
+ expect(handle.url).toBe(url)
1628
+ })
1456
1629
  })
1457
1630
 
1458
1631
  const warn = console.warn