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

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 +39 -0
  18. package/dist/helpers/abortable.d.ts.map +1 -0
  19. package/dist/helpers/abortable.js +45 -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 +364 -74
  62. package/src/helpers/abortable.ts +61 -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
@@ -1,8 +1,12 @@
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"
5
- import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
4
+ import { describe, expect, it, vi } from "vitest"
5
+ import {
6
+ encodeHeads,
7
+ generateAutomergeUrl,
8
+ parseAutomergeUrl,
9
+ } from "../src/AutomergeUrl.js"
6
10
  import { eventPromise } from "../src/helpers/eventPromise.js"
7
11
  import { pause } from "../src/helpers/pause.js"
8
12
  import { DocHandle, DocHandleChangePayload } from "../src/index.js"
@@ -34,7 +38,7 @@ describe("DocHandle", () => {
34
38
  handle.update(doc => docFromMockStorage(doc))
35
39
 
36
40
  assert.equal(handle.isReady(), true)
37
- const doc = await handle.doc()
41
+ const doc = handle.doc()
38
42
  assert.equal(doc?.foo, "bar")
39
43
  })
40
44
 
@@ -46,13 +50,13 @@ describe("DocHandle", () => {
46
50
  handle.update(doc => docFromMockStorage(doc))
47
51
 
48
52
  assert.equal(handle.isReady(), true)
49
- const doc = await handle.doc()
50
- assert.deepEqual(doc, handle.docSync())
53
+ const doc = handle.doc()
54
+ assert.deepEqual(doc, handle.doc())
51
55
  })
52
56
 
53
- 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 () => {
54
58
  const handle = new DocHandle<TestDoc>(TEST_ID)
55
- assert.equal(handle.docSync(), undefined)
59
+ assert.throws(() => handle.doc())
56
60
  })
57
61
 
58
62
  it("should not return a doc until ready", async () => {
@@ -62,7 +66,7 @@ describe("DocHandle", () => {
62
66
  // simulate loading from storage
63
67
  handle.update(doc => docFromMockStorage(doc))
64
68
 
65
- const doc = await handle.doc()
69
+ const doc = handle.doc()
66
70
 
67
71
  assert.equal(handle.isReady(), true)
68
72
  assert.equal(doc?.foo, "bar")
@@ -82,15 +86,15 @@ describe("DocHandle", () => {
82
86
  handle.change(d => (d.foo = "bar"))
83
87
  assert.equal(handle.isReady(), true)
84
88
 
85
- const heads = A.getHeads(handle.docSync())
89
+ const heads = encodeHeads(A.getHeads(handle.doc()))
86
90
  assert.notDeepEqual(handle.heads(), [])
87
91
  assert.deepEqual(heads, handle.heads())
88
92
  })
89
93
 
90
- it("should return undefined if the heads aren't loaded", async () => {
94
+ it("should throw an if the heads aren't loaded", async () => {
91
95
  const handle = new DocHandle<TestDoc>(TEST_ID)
92
96
  assert.equal(handle.isReady(), false)
93
- assert.deepEqual(handle.heads(), undefined)
97
+ expect(() => handle.heads()).toThrow("DocHandle is not ready")
94
98
  })
95
99
 
96
100
  it("should return the history when requested", async () => {
@@ -112,21 +116,45 @@ describe("DocHandle", () => {
112
116
  assert.equal(handle.isReady(), true)
113
117
 
114
118
  const history = handle.history()
115
- const view = handle.view(history[1])
116
- assert.deepEqual(view, { foo: "one" })
119
+ const viewHandle = handle.view(history[1])
120
+ assert.deepEqual(await viewHandle.doc(), { foo: "one" })
117
121
  })
118
122
 
119
- it("should return a commit from the history", async () => {
123
+ it("should support fixed heads from construction", async () => {
120
124
  const handle = setup()
121
125
  handle.change(d => (d.foo = "zero"))
122
126
  handle.change(d => (d.foo = "one"))
123
- handle.change(d => (d.foo = "two"))
124
- handle.change(d => (d.foo = "three"))
125
- assert.equal(handle.isReady(), true)
126
127
 
127
128
  const history = handle.history()
128
- const view = handle.view(history[1])
129
- assert.deepEqual(view, { foo: "one" })
129
+ const viewHandle = new DocHandle<TestDoc>(TEST_ID, { heads: history[0] })
130
+ viewHandle.update(() => A.clone(handle.doc()!))
131
+ viewHandle.doneLoading()
132
+
133
+ assert.deepEqual(await viewHandle.doc(), { foo: "zero" })
134
+ })
135
+
136
+ it("should prevent changes on fixed-heads handles", async () => {
137
+ const handle = setup()
138
+ handle.change(d => (d.foo = "zero"))
139
+ const viewHandle = handle.view(handle.heads()!)
140
+
141
+ assert.throws(() => viewHandle.change(d => (d.foo = "one")))
142
+ assert.throws(() =>
143
+ viewHandle.changeAt(handle.heads()!, d => (d.foo = "one"))
144
+ )
145
+ assert.throws(() => viewHandle.merge(handle))
146
+ })
147
+
148
+ it("should return fixed heads from heads()", async () => {
149
+ const handle = setup()
150
+ handle.change(d => (d.foo = "zero"))
151
+ const originalHeads = handle.heads()!
152
+
153
+ handle.change(d => (d.foo = "one"))
154
+ const viewHandle = handle.view(originalHeads)
155
+
156
+ assert.deepEqual(viewHandle.heads(), originalHeads)
157
+ assert.notDeepEqual(viewHandle.heads(), handle.heads())
130
158
  })
131
159
 
132
160
  it("should return diffs", async () => {
@@ -166,6 +194,59 @@ describe("DocHandle", () => {
166
194
  ])
167
195
  })
168
196
 
197
+ it("should support diffing against another handle", async () => {
198
+ const handle = setup()
199
+ handle.change(d => (d.foo = "zero"))
200
+ const viewHandle = handle.view(handle.heads()!)
201
+
202
+ handle.change(d => (d.foo = "one"))
203
+
204
+ const patches = viewHandle.diff(handle)
205
+ assert.deepEqual(patches, [
206
+ { action: "put", path: ["foo"], value: "" },
207
+ { action: "splice", path: ["foo", 0], value: "one" },
208
+ ])
209
+ })
210
+
211
+ // TODO: alexg -- should i remove this test? should this fail or no?
212
+ it.skip("should fail diffing against unrelated handles", async () => {
213
+ const handle1 = setup()
214
+ const handle2 = setup()
215
+
216
+ handle1.change(d => (d.foo = "zero"))
217
+ handle2.change(d => (d.foo = "one"))
218
+
219
+ assert.throws(() => handle1.diff(handle2))
220
+ })
221
+
222
+ it("should allow direct access to decoded changes", async () => {
223
+ const handle = setup()
224
+ const time = Date.now()
225
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage" })
226
+ assert.equal(handle.isReady(), true)
227
+
228
+ const metadata = handle.metadata()
229
+ assert.deepEqual(metadata.message, "commitMessage")
230
+ // NOTE: I'm not testing time because of https://github.com/automerge/automerge/issues/965
231
+ // but it does round-trip successfully!
232
+ })
233
+
234
+ it("should allow direct access to a specific decoded change", async () => {
235
+ const handle = setup()
236
+ const time = Date.now()
237
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage" })
238
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage2" })
239
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage3" })
240
+ handle.change(d => (d.foo = "foo"), { message: "commitMessage4" })
241
+ assert.equal(handle.isReady(), true)
242
+
243
+ const history = handle.history()
244
+ const metadata = handle.metadata(history[0][0])
245
+ assert.deepEqual(metadata.message, "commitMessage")
246
+ // NOTE: I'm not testing time because of https://github.com/automerge/automerge/issues/965
247
+ // but it does round-trip successfully!
248
+ })
249
+
169
250
  /**
170
251
  * Once there's a Repo#stop API this case should be covered in accompanying
171
252
  * tests and the following test removed.
@@ -178,8 +259,6 @@ describe("DocHandle", () => {
178
259
  const handle = new DocHandle<TestDoc>(TEST_ID)
179
260
  assert.equal(handle.isReady(), false)
180
261
 
181
- handle.doc()
182
-
183
262
  assert(vi.getTimerCount() > timerCount)
184
263
 
185
264
  // simulate loading from storage
@@ -204,7 +283,7 @@ describe("DocHandle", () => {
204
283
  assert.equal(handle.isReady(), true)
205
284
  handle.change(d => (d.foo = "pizza"))
206
285
 
207
- const doc = await handle.doc()
286
+ const doc = handle.doc()
208
287
  assert.equal(doc?.foo, "pizza")
209
288
  })
210
289
 
@@ -214,7 +293,9 @@ describe("DocHandle", () => {
214
293
  // we don't have it in storage, so we request it from the network
215
294
  handle.request()
216
295
 
217
- assert.equal(handle.docSync(), undefined)
296
+ await expect(() => {
297
+ handle.doc()
298
+ }).toThrowError("DocHandle is not ready")
218
299
  assert.equal(handle.isReady(), false)
219
300
  assert.throws(() => handle.change(_ => {}))
220
301
  })
@@ -230,7 +311,7 @@ describe("DocHandle", () => {
230
311
  return A.change(doc, d => (d.foo = "bar"))
231
312
  })
232
313
 
233
- const doc = await handle.doc()
314
+ const doc = handle.doc()
234
315
  assert.equal(handle.isReady(), true)
235
316
  assert.equal(doc?.foo, "bar")
236
317
  })
@@ -246,7 +327,7 @@ describe("DocHandle", () => {
246
327
  doc.foo = "bar"
247
328
  })
248
329
 
249
- const doc = await handle.doc()
330
+ const doc = handle.doc()
250
331
  assert.equal(doc?.foo, "bar")
251
332
 
252
333
  const changePayload = await p
@@ -271,7 +352,7 @@ describe("DocHandle", () => {
271
352
 
272
353
  const p = new Promise<void>(resolve =>
273
354
  handle.once("change", ({ handle, doc }) => {
274
- assert.equal(handle.docSync()?.foo, doc.foo)
355
+ assert.equal(handle.doc()?.foo, doc.foo)
275
356
 
276
357
  resolve()
277
358
  })
@@ -308,7 +389,7 @@ describe("DocHandle", () => {
308
389
  doc.foo = "baz"
309
390
  })
310
391
 
311
- const doc = await handle.doc()
392
+ const doc = handle.doc()
312
393
  assert.equal(doc?.foo, "baz")
313
394
 
314
395
  return p
@@ -323,7 +404,7 @@ describe("DocHandle", () => {
323
404
  })
324
405
 
325
406
  await p
326
- const doc = await handle.doc()
407
+ const doc = handle.doc()
327
408
  assert.equal(doc?.foo, "bar")
328
409
  })
329
410
 
@@ -343,11 +424,7 @@ describe("DocHandle", () => {
343
424
  // set docHandle time out after 5 ms
344
425
  const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
345
426
 
346
- const doc = await handle.doc()
347
-
348
- assert.equal(doc, undefined)
349
-
350
- assert.equal(handle.state, "unavailable")
427
+ expect(() => handle.doc()).toThrowError("DocHandle is not ready")
351
428
  })
352
429
 
353
430
  it("should not time out if the document is loaded in time", async () => {
@@ -358,11 +435,11 @@ describe("DocHandle", () => {
358
435
  handle.update(doc => docFromMockStorage(doc))
359
436
 
360
437
  // now it should not time out
361
- const doc = await handle.doc()
438
+ const doc = handle.doc()
362
439
  assert.equal(doc?.foo, "bar")
363
440
  })
364
441
 
365
- 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 () => {
366
443
  // set docHandle time out after 5 ms
367
444
  const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
368
445
 
@@ -372,8 +449,7 @@ describe("DocHandle", () => {
372
449
  // there's no update
373
450
  await pause(10)
374
451
 
375
- const doc = await handle.doc()
376
- assert.equal(doc, undefined)
452
+ expect(() => handle.doc()).toThrowError("DocHandle is not ready")
377
453
  })
378
454
 
379
455
  it("should not time out if the document is updated in time", async () => {
@@ -391,7 +467,7 @@ describe("DocHandle", () => {
391
467
  // now it should not time out
392
468
  await pause(5)
393
469
 
394
- const doc = await handle.doc()
470
+ const doc = handle.doc()
395
471
  assert.equal(doc?.foo, "bar")
396
472
  })
397
473
 
@@ -444,4 +520,71 @@ describe("DocHandle", () => {
444
520
  assert.deepStrictEqual(decode(data), message)
445
521
  })
446
522
  })
523
+
524
+ it("should cache view handles based on heads", async () => {
525
+ // Create and setup a document with some data
526
+ const handle = setup()
527
+ handle.change(doc => {
528
+ doc.foo = "Hello"
529
+ })
530
+ const heads1 = handle.heads()
531
+
532
+ // Make another change to get a different set of heads
533
+ handle.change(doc => {
534
+ doc.foo = "Hello, World!"
535
+ })
536
+
537
+ // Create a view at the first set of heads
538
+ const view1 = handle.view(heads1)
539
+
540
+ // Request the same view again
541
+ const view2 = handle.view(heads1)
542
+
543
+ // Verify we got the same handle instance back (cached version)
544
+ expect(view1).toBe(view2)
545
+
546
+ // Verify the contents are correct
547
+ expect(view1.doc().foo).toBe("Hello")
548
+
549
+ // Test with a different set of heads
550
+ const view3 = handle.view(handle.heads())
551
+ expect(view3).not.toBe(view1)
552
+ expect(view3.doc().foo).toBe("Hello, World!")
553
+ })
554
+
555
+ it("should improve performance when requesting the same view multiple times", () => {
556
+ // Create and setup a document with some data
557
+ const handle = setup()
558
+ handle.change(doc => {
559
+ doc.foo = "Hello"
560
+ })
561
+ const heads = handle.heads()
562
+
563
+ // First, measure time without cache (first access)
564
+ const startTimeNoCached = performance.now()
565
+ const firstView = handle.view(heads)
566
+ const endTimeNoCached = performance.now()
567
+
568
+ // Now measure with cache (subsequent accesses)
569
+ const startTimeCached = performance.now()
570
+ for (let i = 0; i < 100; i++) {
571
+ handle.view(heads)
572
+ }
573
+ const endTimeCached = performance.now()
574
+
575
+ // Assert that all views are the same instance
576
+ for (let i = 0; i < 10; i++) {
577
+ expect(handle.view(heads)).toBe(firstView)
578
+ }
579
+
580
+ // Calculate average times
581
+ const timeForFirstAccess = endTimeNoCached - startTimeNoCached
582
+ const timeForCachedAccesses = (endTimeCached - startTimeCached) / 100
583
+
584
+ console.log(`Time for first view (no cache): ${timeForFirstAccess}ms`)
585
+ console.log(`Average time per cached view: ${timeForCachedAccesses}ms`)
586
+
587
+ // Cached access should be significantly faster
588
+ expect(timeForCachedAccesses).toBeLessThan(timeForFirstAccess / 10)
589
+ })
447
590
  })
@@ -1,7 +1,11 @@
1
1
  import assert from "assert"
2
2
  import { describe, it } from "vitest"
3
3
  import { next as Automerge } from "@automerge/automerge"
4
- import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
4
+ import {
5
+ encodeHeads,
6
+ generateAutomergeUrl,
7
+ parseAutomergeUrl,
8
+ } from "../src/AutomergeUrl.js"
5
9
  import { DocHandle } from "../src/DocHandle.js"
6
10
  import { eventPromise } from "../src/helpers/eventPromise.js"
7
11
  import {
@@ -67,11 +71,14 @@ describe("DocSynchronizer", () => {
67
71
 
68
72
  assert.equal(message1.peerId, "alice")
69
73
  assert.equal(message1.documentId, handle.documentId)
70
- assert.deepEqual(message1.syncState.lastSentHeads, [])
74
+ assert.deepStrictEqual(message1.syncState.lastSentHeads, [])
71
75
 
72
76
  assert.equal(message2.peerId, "alice")
73
77
  assert.equal(message2.documentId, handle.documentId)
74
- assert.deepEqual(message2.syncState.lastSentHeads, handle.heads())
78
+ assert.deepStrictEqual(
79
+ encodeHeads(message2.syncState.lastSentHeads),
80
+ handle.heads()
81
+ )
75
82
  })
76
83
 
77
84
  it("still syncs with a peer after it disconnects and reconnects", async () => {