@automerge/automerge-repo 2.0.0-alpha.6 → 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 +87 -30
  6. package/dist/DocHandle.d.ts.map +1 -1
  7. package/dist/DocHandle.js +198 -48
  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 +268 -58
  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 +255 -30
  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,26 +66,185 @@ 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")
69
73
  })
70
74
 
75
+ /** HISTORY TRAVERSAL
76
+ * This API is relatively alpha-ish but we're already
77
+ * doing things in our own apps that are fairly ambitious
78
+ * by routing around to a lower-level API.
79
+ * This is an attempt to wrap up the existing practice
80
+ * in a slightly more supportable set of APIs but should be
81
+ * considered provisional: expect further improvements.
82
+ */
83
+
71
84
  it("should return the heads when requested", async () => {
72
85
  const handle = setup()
73
86
  handle.change(d => (d.foo = "bar"))
74
87
  assert.equal(handle.isReady(), true)
75
88
 
76
- const heads = A.getHeads(handle.docSync())
89
+ const heads = encodeHeads(A.getHeads(handle.doc()))
77
90
  assert.notDeepEqual(handle.heads(), [])
78
91
  assert.deepEqual(heads, handle.heads())
79
92
  })
80
93
 
81
- it("should return undefined if the heads aren't loaded", async () => {
94
+ it("should throw an if the heads aren't loaded", async () => {
82
95
  const handle = new DocHandle<TestDoc>(TEST_ID)
83
96
  assert.equal(handle.isReady(), false)
84
- assert.deepEqual(handle.heads(), undefined)
97
+ expect(() => handle.heads()).toThrow("DocHandle is not ready")
98
+ })
99
+
100
+ it("should return the history when requested", async () => {
101
+ const handle = setup()
102
+ handle.change(d => (d.foo = "bar"))
103
+ handle.change(d => (d.foo = "baz"))
104
+ assert.equal(handle.isReady(), true)
105
+
106
+ const history = handle.history()
107
+ assert.deepEqual(handle.history().length, 2)
108
+ })
109
+
110
+ it("should return a commit from the history", async () => {
111
+ const handle = setup()
112
+ handle.change(d => (d.foo = "zero"))
113
+ handle.change(d => (d.foo = "one"))
114
+ handle.change(d => (d.foo = "two"))
115
+ handle.change(d => (d.foo = "three"))
116
+ assert.equal(handle.isReady(), true)
117
+
118
+ const history = handle.history()
119
+ const viewHandle = handle.view(history[1])
120
+ assert.deepEqual(await viewHandle.doc(), { foo: "one" })
121
+ })
122
+
123
+ it("should support fixed heads from construction", async () => {
124
+ const handle = setup()
125
+ handle.change(d => (d.foo = "zero"))
126
+ handle.change(d => (d.foo = "one"))
127
+
128
+ const history = handle.history()
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())
158
+ })
159
+
160
+ it("should return diffs", async () => {
161
+ const handle = setup()
162
+ handle.change(d => (d.foo = "zero"))
163
+ handle.change(d => (d.foo = "one"))
164
+ handle.change(d => (d.foo = "two"))
165
+ handle.change(d => (d.foo = "three"))
166
+ assert.equal(handle.isReady(), true)
167
+
168
+ const history = handle.history()
169
+ const patches = handle.diff(history[1])
170
+ assert.deepEqual(patches, [
171
+ { action: "put", path: ["foo"], value: "" },
172
+ { action: "splice", path: ["foo", 0], value: "one" },
173
+ ])
174
+ })
175
+
176
+ it("should support arbitrary diffs too", async () => {
177
+ const handle = setup()
178
+ handle.change(d => (d.foo = "zero"))
179
+ handle.change(d => (d.foo = "one"))
180
+ handle.change(d => (d.foo = "two"))
181
+ handle.change(d => (d.foo = "three"))
182
+ assert.equal(handle.isReady(), true)
183
+
184
+ const history = handle.history()
185
+ const patches = handle.diff(history[1], history[3])
186
+ assert.deepEqual(patches, [
187
+ { action: "put", path: ["foo"], value: "" },
188
+ { action: "splice", path: ["foo", 0], value: "three" },
189
+ ])
190
+ const backPatches = handle.diff(history[3], history[1])
191
+ assert.deepEqual(backPatches, [
192
+ { action: "put", path: ["foo"], value: "" },
193
+ { action: "splice", path: ["foo", 0], value: "one" },
194
+ ])
195
+ })
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!
85
248
  })
86
249
 
87
250
  /**
@@ -96,8 +259,6 @@ describe("DocHandle", () => {
96
259
  const handle = new DocHandle<TestDoc>(TEST_ID)
97
260
  assert.equal(handle.isReady(), false)
98
261
 
99
- handle.doc()
100
-
101
262
  assert(vi.getTimerCount() > timerCount)
102
263
 
103
264
  // simulate loading from storage
@@ -122,7 +283,7 @@ describe("DocHandle", () => {
122
283
  assert.equal(handle.isReady(), true)
123
284
  handle.change(d => (d.foo = "pizza"))
124
285
 
125
- const doc = await handle.doc()
286
+ const doc = handle.doc()
126
287
  assert.equal(doc?.foo, "pizza")
127
288
  })
128
289
 
@@ -132,7 +293,9 @@ describe("DocHandle", () => {
132
293
  // we don't have it in storage, so we request it from the network
133
294
  handle.request()
134
295
 
135
- assert.equal(handle.docSync(), undefined)
296
+ await expect(() => {
297
+ handle.doc()
298
+ }).toThrowError("DocHandle is not ready")
136
299
  assert.equal(handle.isReady(), false)
137
300
  assert.throws(() => handle.change(_ => {}))
138
301
  })
@@ -148,7 +311,7 @@ describe("DocHandle", () => {
148
311
  return A.change(doc, d => (d.foo = "bar"))
149
312
  })
150
313
 
151
- const doc = await handle.doc()
314
+ const doc = handle.doc()
152
315
  assert.equal(handle.isReady(), true)
153
316
  assert.equal(doc?.foo, "bar")
154
317
  })
@@ -164,7 +327,7 @@ describe("DocHandle", () => {
164
327
  doc.foo = "bar"
165
328
  })
166
329
 
167
- const doc = await handle.doc()
330
+ const doc = handle.doc()
168
331
  assert.equal(doc?.foo, "bar")
169
332
 
170
333
  const changePayload = await p
@@ -189,7 +352,7 @@ describe("DocHandle", () => {
189
352
 
190
353
  const p = new Promise<void>(resolve =>
191
354
  handle.once("change", ({ handle, doc }) => {
192
- assert.equal(handle.docSync()?.foo, doc.foo)
355
+ assert.equal(handle.doc()?.foo, doc.foo)
193
356
 
194
357
  resolve()
195
358
  })
@@ -226,7 +389,7 @@ describe("DocHandle", () => {
226
389
  doc.foo = "baz"
227
390
  })
228
391
 
229
- const doc = await handle.doc()
392
+ const doc = handle.doc()
230
393
  assert.equal(doc?.foo, "baz")
231
394
 
232
395
  return p
@@ -241,7 +404,7 @@ describe("DocHandle", () => {
241
404
  })
242
405
 
243
406
  await p
244
- const doc = await handle.doc()
407
+ const doc = handle.doc()
245
408
  assert.equal(doc?.foo, "bar")
246
409
  })
247
410
 
@@ -261,11 +424,7 @@ describe("DocHandle", () => {
261
424
  // set docHandle time out after 5 ms
262
425
  const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
263
426
 
264
- const doc = await handle.doc()
265
-
266
- assert.equal(doc, undefined)
267
-
268
- assert.equal(handle.state, "unavailable")
427
+ expect(() => handle.doc()).toThrowError("DocHandle is not ready")
269
428
  })
270
429
 
271
430
  it("should not time out if the document is loaded in time", async () => {
@@ -276,11 +435,11 @@ describe("DocHandle", () => {
276
435
  handle.update(doc => docFromMockStorage(doc))
277
436
 
278
437
  // now it should not time out
279
- const doc = await handle.doc()
438
+ const doc = handle.doc()
280
439
  assert.equal(doc?.foo, "bar")
281
440
  })
282
441
 
283
- 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 () => {
284
443
  // set docHandle time out after 5 ms
285
444
  const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
286
445
 
@@ -290,8 +449,7 @@ describe("DocHandle", () => {
290
449
  // there's no update
291
450
  await pause(10)
292
451
 
293
- const doc = await handle.doc()
294
- assert.equal(doc, undefined)
452
+ expect(() => handle.doc()).toThrowError("DocHandle is not ready")
295
453
  })
296
454
 
297
455
  it("should not time out if the document is updated in time", async () => {
@@ -309,7 +467,7 @@ describe("DocHandle", () => {
309
467
  // now it should not time out
310
468
  await pause(5)
311
469
 
312
- const doc = await handle.doc()
470
+ const doc = handle.doc()
313
471
  assert.equal(doc?.foo, "bar")
314
472
  })
315
473
 
@@ -362,4 +520,71 @@ describe("DocHandle", () => {
362
520
  assert.deepStrictEqual(decode(data), message)
363
521
  })
364
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
+ })
365
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 () => {