@automerge/automerge-repo 2.0.0-alpha.14 → 2.0.0-alpha.16

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 (35) hide show
  1. package/dist/AutomergeUrl.d.ts +19 -4
  2. package/dist/AutomergeUrl.d.ts.map +1 -1
  3. package/dist/AutomergeUrl.js +71 -24
  4. package/dist/DocHandle.d.ts +21 -17
  5. package/dist/DocHandle.d.ts.map +1 -1
  6. package/dist/DocHandle.js +83 -26
  7. package/dist/RemoteHeadsSubscriptions.d.ts +4 -4
  8. package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
  9. package/dist/RemoteHeadsSubscriptions.js +4 -1
  10. package/dist/Repo.d.ts.map +1 -1
  11. package/dist/Repo.js +17 -12
  12. package/dist/helpers/bufferFromHex.d.ts +3 -0
  13. package/dist/helpers/bufferFromHex.d.ts.map +1 -0
  14. package/dist/helpers/bufferFromHex.js +13 -0
  15. package/dist/helpers/headsAreSame.d.ts +2 -2
  16. package/dist/helpers/headsAreSame.d.ts.map +1 -1
  17. package/dist/helpers/mergeArrays.d.ts +1 -1
  18. package/dist/helpers/mergeArrays.d.ts.map +1 -1
  19. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
  20. package/dist/helpers/tests/storage-adapter-tests.js +6 -9
  21. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  22. package/dist/storage/StorageSubsystem.js +2 -1
  23. package/package.json +2 -2
  24. package/src/AutomergeUrl.ts +103 -26
  25. package/src/DocHandle.ts +130 -37
  26. package/src/RemoteHeadsSubscriptions.ts +11 -8
  27. package/src/Repo.ts +22 -11
  28. package/src/helpers/bufferFromHex.ts +14 -0
  29. package/src/helpers/headsAreSame.ts +2 -2
  30. package/src/helpers/tests/storage-adapter-tests.ts +13 -24
  31. package/src/storage/StorageSubsystem.ts +3 -1
  32. package/test/AutomergeUrl.test.ts +130 -0
  33. package/test/DocHandle.test.ts +70 -4
  34. package/test/DocSynchronizer.test.ts +10 -3
  35. package/test/Repo.test.ts +117 -3
@@ -3,9 +3,11 @@ import bs58check from "bs58check"
3
3
  import { describe, it } from "vitest"
4
4
  import {
5
5
  generateAutomergeUrl,
6
+ getHeadsFromUrl,
6
7
  isValidAutomergeUrl,
7
8
  parseAutomergeUrl,
8
9
  stringifyAutomergeUrl,
10
+ UrlHeads,
9
11
  } from "../src/AutomergeUrl.js"
10
12
  import type {
11
13
  AutomergeUrl,
@@ -102,3 +104,131 @@ describe("AutomergeUrl", () => {
102
104
  })
103
105
  })
104
106
  })
107
+
108
+ describe("AutomergeUrl with heads", () => {
109
+ // Create some sample encoded heads for testing
110
+ const head1 = bs58check.encode(new Uint8Array([1, 2, 3, 4])) as string
111
+ const head2 = bs58check.encode(new Uint8Array([5, 6, 7, 8])) as string
112
+ const goodHeads = [head1, head2] as UrlHeads
113
+ const urlWithHeads = `${goodUrl}#${head1}|${head2}` as AutomergeUrl
114
+ const invalidHead = "not-base58-encoded"
115
+ const invalidHeads = [invalidHead] as UrlHeads
116
+
117
+ describe("stringifyAutomergeUrl", () => {
118
+ it("should stringify a url with heads", () => {
119
+ const url = stringifyAutomergeUrl({
120
+ documentId: goodDocumentId,
121
+ heads: goodHeads,
122
+ })
123
+ assert.strictEqual(url, urlWithHeads)
124
+ })
125
+
126
+ it("should throw if heads are not valid base58check", () => {
127
+ assert.throws(() =>
128
+ stringifyAutomergeUrl({
129
+ documentId: goodDocumentId,
130
+ heads: invalidHeads,
131
+ })
132
+ )
133
+ })
134
+ })
135
+
136
+ describe("parseAutomergeUrl", () => {
137
+ it("should parse a url with heads", () => {
138
+ const { documentId, heads } = parseAutomergeUrl(urlWithHeads)
139
+ assert.equal(documentId, goodDocumentId)
140
+ assert.deepEqual(heads, [head1, head2])
141
+ })
142
+
143
+ it("should parse a url without heads", () => {
144
+ const { documentId, heads } = parseAutomergeUrl(goodUrl)
145
+ assert.equal(documentId, goodDocumentId)
146
+ assert.equal(heads, undefined)
147
+ })
148
+
149
+ it("should throw on url with invalid heads encoding", () => {
150
+ const badUrl = `${goodUrl}#${invalidHead}` as AutomergeUrl
151
+ assert.throws(() => parseAutomergeUrl(badUrl))
152
+ })
153
+ })
154
+
155
+ describe("isValidAutomergeUrl", () => {
156
+ it("should return true for a valid url with heads", () => {
157
+ assert(isValidAutomergeUrl(urlWithHeads) === true)
158
+ })
159
+
160
+ it("should return false for a url with invalid heads", () => {
161
+ const badUrl = `${goodUrl}#${invalidHead}` as AutomergeUrl
162
+ assert(isValidAutomergeUrl(badUrl) === false)
163
+ })
164
+ })
165
+
166
+ describe("getHeadsFromUrl", () => {
167
+ it("should return heads from a valid url", () => {
168
+ const heads = getHeadsFromUrl(urlWithHeads)
169
+ assert.deepEqual(heads, [head1, head2])
170
+ })
171
+
172
+ it("should return undefined for url without heads", () => {
173
+ const heads = getHeadsFromUrl(goodUrl)
174
+ assert.equal(heads, undefined)
175
+ })
176
+ })
177
+ it("should handle a single head correctly", () => {
178
+ const urlWithOneHead = `${goodUrl}#${head1}` as AutomergeUrl
179
+ const { heads } = parseAutomergeUrl(urlWithOneHead)
180
+ assert.deepEqual(heads, [head1])
181
+ })
182
+
183
+ it("should round-trip urls with heads", () => {
184
+ const originalUrl = urlWithHeads
185
+ const parsed = parseAutomergeUrl(originalUrl)
186
+ const roundTripped = stringifyAutomergeUrl({
187
+ documentId: parsed.documentId,
188
+ heads: parsed.heads,
189
+ })
190
+ assert.equal(roundTripped, originalUrl)
191
+ })
192
+
193
+ describe("should reject malformed urls", () => {
194
+ it("should reject urls with trailing delimiter", () => {
195
+ assert(!isValidAutomergeUrl(`${goodUrl}#${head1}:` as AutomergeUrl))
196
+ })
197
+
198
+ it("should reject urls with empty head", () => {
199
+ assert(!isValidAutomergeUrl(`${goodUrl}#|${head1}` as AutomergeUrl))
200
+ })
201
+
202
+ it("should reject urls with multiple hash characters", () => {
203
+ assert(
204
+ !isValidAutomergeUrl(`${goodUrl}#${head1}#${head2}` as AutomergeUrl)
205
+ )
206
+ })
207
+ })
208
+ })
209
+
210
+ describe("empty heads section", () => {
211
+ it("should treat bare # as empty heads array", () => {
212
+ const urlWithEmptyHeads = `${goodUrl}#` as AutomergeUrl
213
+ const { heads } = parseAutomergeUrl(urlWithEmptyHeads)
214
+ assert.deepEqual(heads, [])
215
+ })
216
+
217
+ it("should round-trip empty heads array", () => {
218
+ const original = `${goodUrl}#` as AutomergeUrl
219
+ const parsed = parseAutomergeUrl(original)
220
+ const roundTripped = stringifyAutomergeUrl({
221
+ documentId: parsed.documentId,
222
+ heads: parsed.heads,
223
+ })
224
+ assert.equal(roundTripped, original)
225
+ })
226
+
227
+ it("should distinguish between no heads and empty heads", () => {
228
+ const noHeads = parseAutomergeUrl(goodUrl)
229
+ const emptyHeads = parseAutomergeUrl(`${goodUrl}#` as AutomergeUrl)
230
+
231
+ assert.equal(noHeads.heads, undefined)
232
+ assert.deepEqual(emptyHeads.heads, [])
233
+ })
234
+ })
@@ -2,7 +2,11 @@ import * as A from "@automerge/automerge/next"
2
2
  import assert from "assert"
3
3
  import { decode } from "cbor-x"
4
4
  import { describe, it, vi } from "vitest"
5
- import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
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"
@@ -83,7 +87,7 @@ describe("DocHandle", () => {
83
87
  handle.change(d => (d.foo = "bar"))
84
88
  assert.equal(handle.isReady(), true)
85
89
 
86
- const heads = A.getHeads(handle.docSync())
90
+ const heads = encodeHeads(A.getHeads(handle.docSync()))
87
91
  assert.notDeepEqual(handle.heads(), [])
88
92
  assert.deepEqual(heads, handle.heads())
89
93
  })
@@ -113,8 +117,45 @@ describe("DocHandle", () => {
113
117
  assert.equal(handle.isReady(), true)
114
118
 
115
119
  const history = handle.history()
116
- const view = handle.view(history[1])
117
- assert.deepEqual(view, { foo: "one" })
120
+ const viewHandle = handle.view(history[1])
121
+ assert.deepEqual(await viewHandle.doc(), { foo: "one" })
122
+ })
123
+
124
+ it("should support fixed heads from construction", async () => {
125
+ const handle = setup()
126
+ handle.change(d => (d.foo = "zero"))
127
+ handle.change(d => (d.foo = "one"))
128
+
129
+ const history = handle.history()
130
+ const viewHandle = new DocHandle<TestDoc>(TEST_ID, { heads: history[0] })
131
+ viewHandle.update(() => A.clone(handle.docSync()!))
132
+ viewHandle.doneLoading()
133
+
134
+ assert.deepEqual(await viewHandle.doc(), { foo: "zero" })
135
+ })
136
+
137
+ it("should prevent changes on fixed-heads handles", async () => {
138
+ const handle = setup()
139
+ handle.change(d => (d.foo = "zero"))
140
+ const viewHandle = handle.view(handle.heads()!)
141
+
142
+ assert.throws(() => viewHandle.change(d => (d.foo = "one")))
143
+ assert.throws(() =>
144
+ viewHandle.changeAt(handle.heads()!, d => (d.foo = "one"))
145
+ )
146
+ assert.throws(() => viewHandle.merge(handle))
147
+ })
148
+
149
+ it("should return fixed heads from heads()", async () => {
150
+ const handle = setup()
151
+ handle.change(d => (d.foo = "zero"))
152
+ const originalHeads = handle.heads()!
153
+
154
+ handle.change(d => (d.foo = "one"))
155
+ const viewHandle = handle.view(originalHeads)
156
+
157
+ assert.deepEqual(viewHandle.heads(), originalHeads)
158
+ assert.notDeepEqual(viewHandle.heads(), handle.heads())
118
159
  })
119
160
 
120
161
  it("should return diffs", async () => {
@@ -154,6 +195,31 @@ describe("DocHandle", () => {
154
195
  ])
155
196
  })
156
197
 
198
+ it("should support diffing against another handle", async () => {
199
+ const handle = setup()
200
+ handle.change(d => (d.foo = "zero"))
201
+ const viewHandle = handle.view(handle.heads()!)
202
+
203
+ handle.change(d => (d.foo = "one"))
204
+
205
+ const patches = viewHandle.diff(handle)
206
+ assert.deepEqual(patches, [
207
+ { action: "put", path: ["foo"], value: "" },
208
+ { action: "splice", path: ["foo", 0], value: "one" },
209
+ ])
210
+ })
211
+
212
+ // TODO: alexg -- should i remove this test? should this fail or no?
213
+ it.skip("should fail diffing against unrelated handles", async () => {
214
+ const handle1 = setup()
215
+ const handle2 = setup()
216
+
217
+ handle1.change(d => (d.foo = "zero"))
218
+ handle2.change(d => (d.foo = "one"))
219
+
220
+ assert.throws(() => handle1.diff(handle2))
221
+ })
222
+
157
223
  it("should allow direct access to decoded changes", async () => {
158
224
  const handle = setup()
159
225
  const time = Date.now()
@@ -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 () => {
package/test/Repo.test.ts CHANGED
@@ -3,7 +3,13 @@ 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"
6
+ import {
7
+ encodeHeads,
8
+ getHeadsFromUrl,
9
+ isValidAutomergeUrl,
10
+ parseAutomergeUrl,
11
+ UrlHeads,
12
+ } from "../src/AutomergeUrl.js"
7
13
  import {
8
14
  generateAutomergeUrl,
9
15
  stringifyAutomergeUrl,
@@ -1175,7 +1181,10 @@ describe("Repo", () => {
1175
1181
  bobHandle.documentId,
1176
1182
  await charlieRepo!.storageSubsystem.id()
1177
1183
  )
1178
- assert.deepStrictEqual(storedSyncState.sharedHeads, bobHandle.heads())
1184
+ assert.deepStrictEqual(
1185
+ encodeHeads(storedSyncState.sharedHeads),
1186
+ bobHandle.heads()
1187
+ )
1179
1188
 
1180
1189
  teardown()
1181
1190
  })
@@ -1275,7 +1284,7 @@ describe("Repo", () => {
1275
1284
 
1276
1285
  const nextRemoteHeadsPromise = new Promise<{
1277
1286
  storageId: StorageId
1278
- heads: A.Heads
1287
+ heads: UrlHeads
1279
1288
  }>(resolve => {
1280
1289
  handle.on("remote-heads", ({ storageId, heads }) => {
1281
1290
  resolve({ storageId, heads })
@@ -1526,6 +1535,111 @@ describe("Repo", () => {
1526
1535
  })
1527
1536
  })
1528
1537
 
1538
+ describe("Repo heads-in-URLs functionality", () => {
1539
+ const setup = () => {
1540
+ const repo = new Repo({})
1541
+ const handle = repo.create()
1542
+ handle.change((doc: any) => (doc.title = "Hello World"))
1543
+ return { repo, handle }
1544
+ }
1545
+
1546
+ it("finds a document view by URL with heads", async () => {
1547
+ const { repo, handle } = setup()
1548
+ const heads = handle.heads()!
1549
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1550
+ const view = repo.find(url)
1551
+ expect(view.docSync()).toEqual({ title: "Hello World" })
1552
+ })
1553
+
1554
+ it("returns a view, not the actual handle, when finding by URL with heads", async () => {
1555
+ const { repo, handle } = setup()
1556
+ const heads = handle.heads()!
1557
+ await handle.change((doc: any) => (doc.title = "Changed"))
1558
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1559
+ const view = repo.find(url)
1560
+ expect(view.docSync()).toEqual({ title: "Hello World" })
1561
+ expect(handle.docSync()).toEqual({ title: "Changed" })
1562
+ })
1563
+
1564
+ it("changes to a document view do not affect the original", async () => {
1565
+ const { repo, handle } = setup()
1566
+ const heads = handle.heads()!
1567
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1568
+ const view = repo.find(url)
1569
+ expect(() =>
1570
+ view.change((doc: any) => (doc.title = "Changed in View"))
1571
+ ).toThrow()
1572
+ expect(handle.docSync()).toEqual({ title: "Hello World" })
1573
+ })
1574
+
1575
+ it("document views are read-only", async () => {
1576
+ const { repo, handle } = setup()
1577
+ const heads = handle.heads()!
1578
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1579
+ const view = repo.find(url)
1580
+ expect(() => view.change((doc: any) => (doc.title = "Changed"))).toThrow()
1581
+ })
1582
+
1583
+ it("finds the latest document when given a URL without heads", async () => {
1584
+ const { repo, handle } = setup()
1585
+ await handle.change((doc: any) => (doc.title = "Changed"))
1586
+ const found = repo.find(handle.url)
1587
+ expect(found.docSync()).toEqual({ title: "Changed" })
1588
+ })
1589
+
1590
+ it("getHeadsFromUrl returns heads array if present or undefined", () => {
1591
+ const { repo, handle } = setup()
1592
+ const heads = handle.heads()!
1593
+ const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
1594
+ expect(getHeadsFromUrl(url)).toEqual(heads)
1595
+
1596
+ const urlWithoutHeads = generateAutomergeUrl()
1597
+ expect(getHeadsFromUrl(urlWithoutHeads)).toBeUndefined()
1598
+ })
1599
+
1600
+ it("isValidAutomergeUrl returns true for valid URLs", () => {
1601
+ const { repo, handle } = setup()
1602
+ const url = generateAutomergeUrl()
1603
+ expect(isValidAutomergeUrl(url)).toBe(true)
1604
+
1605
+ const urlWithHeads = stringifyAutomergeUrl({
1606
+ documentId: handle.documentId,
1607
+ heads: handle.heads()!,
1608
+ })
1609
+ expect(isValidAutomergeUrl(urlWithHeads)).toBe(true)
1610
+ })
1611
+
1612
+ it("isValidAutomergeUrl returns false for invalid URLs", () => {
1613
+ const { repo, handle } = setup()
1614
+ expect(isValidAutomergeUrl("not a url")).toBe(false)
1615
+ expect(isValidAutomergeUrl("automerge:invalidid")).toBe(false)
1616
+ expect(isValidAutomergeUrl("automerge:validid#invalidhead")).toBe(false)
1617
+ })
1618
+
1619
+ it("parseAutomergeUrl extracts documentId and heads", () => {
1620
+ const { repo, handle } = setup()
1621
+ const url = stringifyAutomergeUrl({
1622
+ documentId: handle.documentId,
1623
+ heads: handle.heads()!,
1624
+ })
1625
+ const parsed = parseAutomergeUrl(url)
1626
+ expect(parsed.documentId).toBe(handle.documentId)
1627
+ expect(parsed.heads).toEqual(handle.heads())
1628
+ })
1629
+
1630
+ it("stringifyAutomergeUrl creates valid URL", () => {
1631
+ const { repo, handle } = setup()
1632
+ const url = stringifyAutomergeUrl({
1633
+ documentId: handle.documentId,
1634
+ heads: handle.heads()!,
1635
+ })
1636
+ expect(isValidAutomergeUrl(url)).toBe(true)
1637
+ const parsed = parseAutomergeUrl(url)
1638
+ expect(parsed.documentId).toBe(handle.documentId)
1639
+ expect(parsed.heads).toEqual(handle.heads())
1640
+ })
1641
+ })
1642
+
1529
1643
  const warn = console.warn
1530
1644
  const NO_OP = () => {}
1531
1645