@automerge/automerge-repo 1.1.4 → 1.1.8

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 (37) hide show
  1. package/README.md +3 -22
  2. package/dist/DocHandle.d.ts +124 -100
  3. package/dist/DocHandle.d.ts.map +1 -1
  4. package/dist/DocHandle.js +239 -231
  5. package/dist/Repo.d.ts +10 -3
  6. package/dist/Repo.d.ts.map +1 -1
  7. package/dist/Repo.js +22 -1
  8. package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
  9. package/dist/helpers/debounce.d.ts.map +1 -1
  10. package/dist/helpers/tests/network-adapter-tests.d.ts +1 -1
  11. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  12. package/dist/helpers/tests/network-adapter-tests.js +2 -2
  13. package/dist/helpers/tests/storage-adapter-tests.d.ts +7 -0
  14. package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -0
  15. package/dist/helpers/tests/storage-adapter-tests.js +128 -0
  16. package/dist/helpers/throttle.d.ts.map +1 -1
  17. package/dist/helpers/withTimeout.d.ts.map +1 -1
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -0
  21. package/dist/synchronizer/DocSynchronizer.js +1 -1
  22. package/package.json +4 -4
  23. package/src/DocHandle.ts +325 -375
  24. package/src/Repo.ts +35 -8
  25. package/src/helpers/tests/network-adapter-tests.ts +4 -2
  26. package/src/helpers/tests/storage-adapter-tests.ts +193 -0
  27. package/src/index.ts +43 -0
  28. package/src/synchronizer/DocSynchronizer.ts +1 -1
  29. package/test/CollectionSynchronizer.test.ts +1 -3
  30. package/test/DocHandle.test.ts +19 -1
  31. package/test/DocSynchronizer.test.ts +1 -4
  32. package/test/DummyStorageAdapter.test.ts +11 -0
  33. package/test/Repo.test.ts +179 -53
  34. package/test/helpers/DummyNetworkAdapter.ts +20 -18
  35. package/test/helpers/DummyStorageAdapter.ts +5 -1
  36. package/test/remoteHeads.test.ts +1 -1
  37. package/tsconfig.json +1 -0
package/src/Repo.ts CHANGED
@@ -2,15 +2,18 @@ import { next as Automerge } from "@automerge/automerge"
2
2
  import debug from "debug"
3
3
  import { EventEmitter } from "eventemitter3"
4
4
  import {
5
- generateAutomergeUrl,
6
- interpretAsDocumentId,
7
- parseAutomergeUrl,
5
+ generateAutomergeUrl,
6
+ interpretAsDocumentId,
7
+ parseAutomergeUrl,
8
8
  } from "./AutomergeUrl.js"
9
9
  import { DocHandle, DocHandleEncodedChangePayload } from "./DocHandle.js"
10
10
  import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js"
11
11
  import { headsAreSame } from "./helpers/headsAreSame.js"
12
12
  import { throttle } from "./helpers/throttle.js"
13
- import { NetworkAdapterInterface, type PeerMetadata } from "./network/NetworkAdapterInterface.js"
13
+ import {
14
+ NetworkAdapterInterface,
15
+ type PeerMetadata,
16
+ } from "./network/NetworkAdapterInterface.js"
14
17
  import { NetworkSubsystem } from "./network/NetworkSubsystem.js"
15
18
  import { RepoMessage } from "./network/messages.js"
16
19
  import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js"
@@ -57,12 +60,12 @@ export class Repo extends EventEmitter<RepoEvents> {
57
60
 
58
61
  constructor({
59
62
  storage,
60
- network,
63
+ network = [],
61
64
  peerId,
62
65
  sharePolicy,
63
66
  isEphemeral = storage === undefined,
64
67
  enableRemoteHeadsGossiping = false,
65
- }: RepoConfig) {
68
+ }: RepoConfig = {}) {
66
69
  super()
67
70
  this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping
68
71
  this.#log = debug(`automerge-repo:repo`)
@@ -502,6 +505,30 @@ export class Repo extends EventEmitter<RepoEvents> {
502
505
  return this.storageSubsystem.id()
503
506
  }
504
507
  }
508
+
509
+ /**
510
+ * Writes Documents to a disk.
511
+ * @hidden this API is experimental and may change.
512
+ * @param documents - if provided, only writes the specified documents.
513
+ * @returns Promise<void>
514
+ */
515
+ async flush(documents?: DocumentId[]): Promise<void> {
516
+ if (!this.storageSubsystem) {
517
+ return
518
+ }
519
+ const handles = documents
520
+ ? documents.map(id => this.#handleCache[id])
521
+ : Object.values(this.#handleCache)
522
+ await Promise.all(
523
+ handles.map(async handle => {
524
+ const doc = handle.docSync()
525
+ if (!doc) {
526
+ return
527
+ }
528
+ return this.storageSubsystem!.saveDoc(handle.documentId, doc)
529
+ })
530
+ )
531
+ }
505
532
  }
506
533
 
507
534
  export interface RepoConfig {
@@ -515,8 +542,8 @@ export interface RepoConfig {
515
542
  /** A storage adapter can be provided, or not */
516
543
  storage?: StorageAdapterInterface
517
544
 
518
- /** One or more network adapters must be provided */
519
- network: NetworkAdapterInterface[]
545
+ /** A list of network adapters (more can be added at runtime). */
546
+ network?: NetworkAdapterInterface[]
520
547
 
521
548
  /**
522
549
  * Normal peers typically share generously with everyone (meaning we sync all our documents with
@@ -17,7 +17,7 @@ import { pause } from "../pause.js"
17
17
  * - `teardown`: An optional function that will be called after the tests have run. This can be used
18
18
  * to clean up any resources that were created during the test.
19
19
  */
20
- export function runAdapterTests(_setup: SetupFn, title?: string): void {
20
+ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
21
21
  // Wrap the provided setup function
22
22
  const setup = async () => {
23
23
  const { adapters, teardown = NO_OP } = await _setup()
@@ -28,7 +28,9 @@ export function runAdapterTests(_setup: SetupFn, title?: string): void {
28
28
  return { adapters: [a, b, c], teardown }
29
29
  }
30
30
 
31
- describe(`Adapter acceptance tests ${title ? `(${title})` : ""}`, () => {
31
+ describe(`Network adapter acceptance tests ${
32
+ title ? `(${title})` : ""
33
+ }`, () => {
32
34
  it("can sync 2 repos", async () => {
33
35
  const doTest = async (
34
36
  a: NetworkAdapterInterface[],
@@ -0,0 +1,193 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import type { StorageAdapterInterface } from "../../storage/StorageAdapterInterface.js"
4
+
5
+ const PAYLOAD_A = () => new Uint8Array([0, 1, 127, 99, 154, 235])
6
+ const PAYLOAD_B = () => new Uint8Array([1, 76, 160, 53, 57, 10, 230])
7
+ const PAYLOAD_C = () => new Uint8Array([2, 111, 74, 131, 236, 96, 142, 193])
8
+
9
+ const LARGE_PAYLOAD = new Uint8Array(100000).map(() => Math.random() * 256)
10
+
11
+ export function runStorageAdapterTests(_setup: SetupFn, title?: string): void {
12
+ const setup = async () => {
13
+ const { adapter, teardown = NO_OP } = await _setup()
14
+ return { adapter, teardown }
15
+ }
16
+
17
+ describe(`Storage adapter acceptance tests ${
18
+ title ? `(${title})` : ""
19
+ }`, () => {
20
+ describe("load", () => {
21
+ it("should return undefined if there is no data", async () => {
22
+ const { adapter, teardown } = await setup()
23
+
24
+ const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
25
+ expect(actual).toBeUndefined()
26
+
27
+ teardown()
28
+ })
29
+ })
30
+
31
+ describe("save and load", () => {
32
+ it("should return data that was saved", async () => {
33
+ const { adapter, teardown } = await setup()
34
+
35
+ await adapter.save(["storage-adapter-id"], PAYLOAD_A())
36
+ const actual = await adapter.load(["storage-adapter-id"])
37
+ expect(actual).toStrictEqual(PAYLOAD_A())
38
+
39
+ teardown()
40
+ })
41
+
42
+ it("should work with composite keys", async () => {
43
+ const { adapter, teardown } = await setup()
44
+
45
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
46
+ const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
47
+ expect(actual).toStrictEqual(PAYLOAD_A())
48
+
49
+ teardown()
50
+ })
51
+
52
+ it("should work with a large payload", async () => {
53
+ const { adapter, teardown } = await setup()
54
+
55
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], LARGE_PAYLOAD)
56
+ const actual = await adapter.load(["AAAAA", "sync-state", "xxxxx"])
57
+ expect(actual).toStrictEqual(LARGE_PAYLOAD)
58
+
59
+ teardown()
60
+ })
61
+ })
62
+
63
+ describe("loadRange", () => {
64
+ it("should return an empty array if there is no data", async () => {
65
+ const { adapter, teardown } = await setup()
66
+
67
+ expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([])
68
+
69
+ teardown()
70
+ })
71
+ })
72
+
73
+ describe("save and loadRange", () => {
74
+ it("should return all the data that matches the key", async () => {
75
+ const { adapter, teardown } = await setup()
76
+
77
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
78
+ await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B())
79
+ await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C())
80
+
81
+ expect(await adapter.loadRange(["AAAAA"])).toStrictEqual(
82
+ expect.arrayContaining([
83
+ { key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
84
+ { key: ["AAAAA", "snapshot", "yyyyy"], data: PAYLOAD_B() },
85
+ { key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
86
+ ])
87
+ )
88
+
89
+ expect(await adapter.loadRange(["AAAAA", "sync-state"])).toStrictEqual(
90
+ expect.arrayContaining([
91
+ { key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
92
+ { key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
93
+ ])
94
+ )
95
+
96
+ teardown()
97
+ })
98
+
99
+ it("should only load values that match they key", async () => {
100
+ const { adapter, teardown } = await setup()
101
+
102
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
103
+ await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_C())
104
+
105
+ const actual = await adapter.loadRange(["AAAAA"])
106
+ expect(actual).toStrictEqual(
107
+ expect.arrayContaining([
108
+ { key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
109
+ ])
110
+ )
111
+ expect(actual).toStrictEqual(
112
+ expect.not.arrayContaining([
113
+ { key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_C() },
114
+ ])
115
+ )
116
+
117
+ teardown()
118
+ })
119
+ })
120
+
121
+ describe("save and remove", () => {
122
+ it("after removing, should be empty", async () => {
123
+ const { adapter, teardown } = await setup()
124
+
125
+ await adapter.save(["AAAAA", "snapshot", "xxxxx"], PAYLOAD_A())
126
+ await adapter.remove(["AAAAA", "snapshot", "xxxxx"])
127
+
128
+ expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([])
129
+ expect(
130
+ await adapter.load(["AAAAA", "snapshot", "xxxxx"])
131
+ ).toBeUndefined()
132
+
133
+ teardown()
134
+ })
135
+ })
136
+
137
+ describe("save and save", () => {
138
+ it("should overwrite data saved with the same key", async () => {
139
+ const { adapter, teardown } = await setup()
140
+
141
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
142
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_B())
143
+
144
+ expect(await adapter.loadRange(["AAAAA", "sync-state"])).toStrictEqual([
145
+ { key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_B() },
146
+ ])
147
+
148
+ teardown()
149
+ })
150
+ })
151
+
152
+ describe("removeRange", () => {
153
+ it("should remove a range of records", async () => {
154
+ const { adapter, teardown } = await setup()
155
+
156
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
157
+ await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B())
158
+ await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C())
159
+
160
+ await adapter.removeRange(["AAAAA", "sync-state"])
161
+
162
+ expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([
163
+ { key: ["AAAAA", "snapshot", "yyyyy"], data: PAYLOAD_B() },
164
+ ])
165
+
166
+ teardown()
167
+ })
168
+
169
+ it("should not remove records that don't match", async () => {
170
+ const { adapter, teardown } = await setup()
171
+
172
+ await adapter.save(["AAAAA", "sync-state", "xxxxx"], PAYLOAD_A())
173
+ await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_B())
174
+
175
+ await adapter.removeRange(["AAAAA"])
176
+
177
+ const actual = await adapter.loadRange(["BBBBB"])
178
+ expect(actual).toStrictEqual([
179
+ { key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_B() },
180
+ ])
181
+
182
+ teardown()
183
+ })
184
+ })
185
+ })
186
+ }
187
+
188
+ const NO_OP = () => {}
189
+
190
+ export type SetupFn = () => Promise<{
191
+ adapter: StorageAdapterInterface
192
+ teardown?: () => void
193
+ }>
package/src/index.ts CHANGED
@@ -90,3 +90,46 @@ export type {
90
90
  } from "./storage/types.js"
91
91
 
92
92
  export * from "./types.js"
93
+
94
+ // export commonly used data types
95
+ export type { Counter, RawString, Cursor } from "@automerge/automerge/next"
96
+
97
+ // export some automerge API types
98
+ export type {
99
+ Doc,
100
+ Heads,
101
+ Patch,
102
+ PatchCallback,
103
+ Prop,
104
+ ActorId,
105
+ Change,
106
+ ChangeFn,
107
+ Mark,
108
+ MarkSet,
109
+ MarkRange,
110
+ MarkValue,
111
+ } from "@automerge/automerge/next"
112
+
113
+ // export a few utility functions that aren't in automerge-repo
114
+ // NB that these should probably all just be available via the dochandle
115
+ export {
116
+ getChanges,
117
+ getAllChanges,
118
+ applyChanges,
119
+ view,
120
+ getConflicts,
121
+ } from "@automerge/automerge/next"
122
+
123
+ // export type-specific utility functions
124
+ // these mostly can't be on the data-type in question because
125
+ // JS strings can't have methods added to them
126
+ export {
127
+ getCursor,
128
+ getCursorPosition,
129
+ splice,
130
+ updateText,
131
+ insertAt,
132
+ deleteAt,
133
+ mark,
134
+ unmark,
135
+ } from "@automerge/automerge/next"
@@ -228,7 +228,7 @@ export class DocSynchronizer extends Synchronizer {
228
228
 
229
229
  beginSync(peerIds: PeerId[]) {
230
230
  const noPeersWithDocument = peerIds.every(
231
- (peerId) => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
231
+ peerId => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
232
232
  )
233
233
 
234
234
  // At this point if we don't have anything in our storage, we need to use an empty doc to sync
@@ -8,9 +8,7 @@ describe("CollectionSynchronizer", () => {
8
8
  let synchronizer: CollectionSynchronizer
9
9
 
10
10
  beforeEach(() => {
11
- repo = new Repo({
12
- network: [],
13
- })
11
+ repo = new Repo()
14
12
  synchronizer = new CollectionSynchronizer(repo)
15
13
  })
16
14
 
@@ -72,6 +72,24 @@ describe("DocHandle", () => {
72
72
  assert.equal(doc?.foo, "bar")
73
73
  })
74
74
 
75
+ it("should return the heads when requested", async () => {
76
+ const handle = new DocHandle<TestDoc>(TEST_ID, {
77
+ isNew: true,
78
+ initialValue: { foo: "bar" },
79
+ })
80
+ assert.equal(handle.isReady(), true)
81
+
82
+ const heads = A.getHeads(handle.docSync())
83
+ assert.notDeepEqual(handle.heads(), [])
84
+ assert.deepEqual(heads, handle.heads())
85
+ })
86
+
87
+ it("should return undefined if the heads aren't loaded", async () => {
88
+ const handle = new DocHandle<TestDoc>(TEST_ID)
89
+ assert.equal(handle.isReady(), false)
90
+ assert.deepEqual(handle.heads(), undefined)
91
+ })
92
+
75
93
  /**
76
94
  * Once there's a Repo#stop API this case should be covered in accompanying
77
95
  * tests and the following test removed.
@@ -319,7 +337,7 @@ describe("DocHandle", () => {
319
337
  doc.foo = "bar"
320
338
  })
321
339
 
322
- const headsBefore = A.getHeads(handle.docSync()!)
340
+ const headsBefore = handle.heads()!
323
341
 
324
342
  handle.change(doc => {
325
343
  doc.foo = "rab"
@@ -68,10 +68,7 @@ describe("DocSynchronizer", () => {
68
68
 
69
69
  assert.equal(message2.peerId, "alice")
70
70
  assert.equal(message2.documentId, handle.documentId)
71
- assert.deepEqual(
72
- message2.syncState.lastSentHeads,
73
- A.getHeads(handle.docSync())
74
- )
71
+ assert.deepEqual(message2.syncState.lastSentHeads, handle.heads())
75
72
  })
76
73
 
77
74
  it("still syncs with a peer after it disconnects and reconnects", async () => {
@@ -0,0 +1,11 @@
1
+ import { beforeEach, describe } from "vitest"
2
+ import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
3
+ import { runStorageAdapterTests } from "../src/helpers/tests/storage-adapter-tests.js"
4
+
5
+ describe("DummyStorageAdapter", () => {
6
+ const setup = async () => ({
7
+ adapter: new DummyStorageAdapter(),
8
+ })
9
+
10
+ runStorageAdapterTests(setup, "DummyStorageAdapter")
11
+ })