@automerge/automerge-repo 1.1.0-alpha.7 → 1.1.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 (73) hide show
  1. package/README.md +5 -3
  2. package/dist/AutomergeUrl.js +1 -1
  3. package/dist/DocHandle.d.ts +10 -4
  4. package/dist/DocHandle.d.ts.map +1 -1
  5. package/dist/DocHandle.js +21 -13
  6. package/dist/Repo.d.ts +22 -10
  7. package/dist/Repo.d.ts.map +1 -1
  8. package/dist/Repo.js +90 -76
  9. package/dist/helpers/pause.d.ts +0 -1
  10. package/dist/helpers/pause.d.ts.map +1 -1
  11. package/dist/helpers/pause.js +2 -8
  12. package/dist/helpers/tests/network-adapter-tests.d.ts +2 -2
  13. package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
  14. package/dist/helpers/tests/network-adapter-tests.js +16 -1
  15. package/dist/helpers/withTimeout.d.ts.map +1 -1
  16. package/dist/helpers/withTimeout.js +2 -0
  17. package/dist/index.d.ts +4 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/network/NetworkAdapter.d.ts +4 -34
  21. package/dist/network/NetworkAdapter.d.ts.map +1 -1
  22. package/dist/network/NetworkAdapter.js +2 -0
  23. package/dist/network/NetworkAdapterInterface.d.ts +61 -0
  24. package/dist/network/NetworkAdapterInterface.d.ts.map +1 -0
  25. package/dist/network/NetworkAdapterInterface.js +2 -0
  26. package/dist/network/NetworkSubsystem.d.ts +3 -3
  27. package/dist/network/NetworkSubsystem.d.ts.map +1 -1
  28. package/dist/network/NetworkSubsystem.js +7 -5
  29. package/dist/network/messages.d.ts +43 -38
  30. package/dist/network/messages.d.ts.map +1 -1
  31. package/dist/network/messages.js +7 -9
  32. package/dist/storage/StorageAdapter.d.ts +3 -1
  33. package/dist/storage/StorageAdapter.d.ts.map +1 -1
  34. package/dist/storage/StorageAdapter.js +1 -0
  35. package/dist/storage/StorageAdapterInterface.d.ts +30 -0
  36. package/dist/storage/StorageAdapterInterface.d.ts.map +1 -0
  37. package/dist/storage/StorageAdapterInterface.js +1 -0
  38. package/dist/storage/StorageSubsystem.d.ts +2 -2
  39. package/dist/storage/StorageSubsystem.d.ts.map +1 -1
  40. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
  41. package/dist/synchronizer/CollectionSynchronizer.js +1 -0
  42. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
  43. package/dist/synchronizer/DocSynchronizer.js +13 -9
  44. package/dist/synchronizer/Synchronizer.d.ts +11 -3
  45. package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
  46. package/package.json +3 -4
  47. package/src/AutomergeUrl.ts +1 -1
  48. package/src/DocHandle.ts +40 -19
  49. package/src/Repo.ts +123 -98
  50. package/src/helpers/pause.ts +3 -11
  51. package/src/helpers/tests/network-adapter-tests.ts +30 -4
  52. package/src/helpers/withTimeout.ts +2 -0
  53. package/src/index.ts +4 -2
  54. package/src/network/NetworkAdapter.ts +9 -45
  55. package/src/network/NetworkAdapterInterface.ts +77 -0
  56. package/src/network/NetworkSubsystem.ts +16 -14
  57. package/src/network/messages.ts +60 -63
  58. package/src/storage/StorageAdapter.ts +3 -1
  59. package/src/storage/StorageAdapterInterface.ts +34 -0
  60. package/src/storage/StorageSubsystem.ts +3 -3
  61. package/src/synchronizer/CollectionSynchronizer.ts +1 -0
  62. package/src/synchronizer/DocSynchronizer.ts +22 -18
  63. package/src/synchronizer/Synchronizer.ts +11 -3
  64. package/test/CollectionSynchronizer.test.ts +7 -5
  65. package/test/DocHandle.test.ts +35 -3
  66. package/test/RemoteHeadsSubscriptions.test.ts +49 -49
  67. package/test/Repo.test.ts +71 -2
  68. package/test/StorageSubsystem.test.ts +1 -1
  69. package/test/helpers/DummyNetworkAdapter.ts +37 -5
  70. package/test/helpers/collectMessages.ts +19 -0
  71. package/test/remoteHeads.test.ts +142 -119
  72. package/.eslintrc +0 -28
  73. package/test/helpers/waitForMessages.ts +0 -22
@@ -1,8 +1,7 @@
1
- import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
2
1
  import * as A from "@automerge/automerge/next"
3
2
  import assert from "assert"
4
- import { setTimeout } from "timers/promises"
5
3
  import { describe, it } from "vitest"
4
+ import { MessageChannelNetworkAdapter } from "../../automerge-repo-network-messagechannel/dist/index.js"
6
5
  import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js"
7
6
  import { eventPromise } from "../src/helpers/eventPromise.js"
8
7
  import {
@@ -12,7 +11,7 @@ import {
12
11
  Repo,
13
12
  } from "../src/index.js"
14
13
  import { DummyStorageAdapter } from "./helpers/DummyStorageAdapter.js"
15
- import { waitForMessages } from "./helpers/waitForMessages.js"
14
+ import { collectMessages } from "./helpers/collectMessages.js"
16
15
  import { TestDoc } from "./types.js"
17
16
 
18
17
  describe("DocHandle.remoteHeads", () => {
@@ -24,6 +23,7 @@ describe("DocHandle.remoteHeads", () => {
24
23
  peerId: "bob" as PeerId,
25
24
  network: [],
26
25
  storage: new DummyStorageAdapter(),
26
+ enableRemoteHeadsGossiping: true,
27
27
  })
28
28
  const bobStorageId = await bobRepo.storageId()
29
29
 
@@ -41,23 +41,26 @@ describe("DocHandle.remoteHeads", () => {
41
41
 
42
42
  describe("multi hop sync", () => {
43
43
  async function setup() {
44
- // setup topology: tab -> service worker -> sync server <- service worker <- tab
45
- const leftTab1 = new Repo({
46
- peerId: "left-tab-1" as PeerId,
44
+ // setup topology: alice -> service worker -> sync server <- service worker <- bob
45
+ const alice = new Repo({
46
+ peerId: "alice-tab-1" as PeerId,
47
47
  network: [],
48
48
  sharePolicy: async () => true,
49
+ enableRemoteHeadsGossiping: true,
49
50
  })
50
- const leftTab2 = new Repo({
51
- peerId: "left-tab-2" as PeerId,
51
+ const alice2 = new Repo({
52
+ peerId: "alice-tab-2" as PeerId,
52
53
  network: [],
53
54
  sharePolicy: async () => true,
55
+ enableRemoteHeadsGossiping: true,
54
56
  })
55
- const leftServiceWorker = new Repo({
56
- peerId: "left-service-worker" as PeerId,
57
+ const aliceServiceWorker = new Repo({
58
+ peerId: "alice-service-worker" as PeerId,
57
59
  network: [],
58
60
  sharePolicy: async peer => peer === "sync-server",
59
61
  storage: new DummyStorageAdapter(),
60
62
  isEphemeral: false,
63
+ enableRemoteHeadsGossiping: true,
61
64
  })
62
65
  const syncServer = new Repo({
63
66
  peerId: "sync-server" as PeerId,
@@ -65,189 +68,209 @@ describe("DocHandle.remoteHeads", () => {
65
68
  isEphemeral: false,
66
69
  sharePolicy: async () => false,
67
70
  storage: new DummyStorageAdapter(),
71
+ enableRemoteHeadsGossiping: true,
68
72
  })
69
- const rightServiceWorker = new Repo({
70
- peerId: "right-service-worker" as PeerId,
73
+ const bobServiceWorker = new Repo({
74
+ peerId: "bob-service-worker" as PeerId,
71
75
  network: [],
72
76
  sharePolicy: async peer => peer === "sync-server",
73
77
  isEphemeral: false,
74
78
  storage: new DummyStorageAdapter(),
79
+ enableRemoteHeadsGossiping: true,
75
80
  })
76
- const rightTab = new Repo({
77
- peerId: "right-tab" as PeerId,
81
+ const bob = new Repo({
82
+ peerId: "bob-tab" as PeerId,
78
83
  network: [],
79
84
  sharePolicy: async () => true,
85
+ enableRemoteHeadsGossiping: true,
80
86
  })
81
87
 
82
88
  // connect them all up
83
- connectRepos(leftTab1, leftServiceWorker)
84
- connectRepos(leftTab2, leftServiceWorker)
85
- connectRepos(leftServiceWorker, syncServer)
86
- connectRepos(syncServer, rightServiceWorker)
87
- connectRepos(rightServiceWorker, rightTab)
88
-
89
- await setTimeout(100)
89
+ await Promise.all([
90
+ connectRepos(alice, aliceServiceWorker),
91
+ connectRepos(alice2, aliceServiceWorker),
92
+ connectRepos(aliceServiceWorker, syncServer),
93
+ connectRepos(syncServer, bobServiceWorker),
94
+ connectRepos(bobServiceWorker, bob),
95
+ ])
96
+
97
+ const alice1StorageId = await aliceServiceWorker.storageId()
98
+ const alice2StorageId = await aliceServiceWorker.storageId()
99
+ const aliceServiceWorkerStorageId = await aliceServiceWorker.storageId()
100
+ const syncServerStorageId = await syncServer.storageId()
101
+ const bobServiceWorkerStorageId = await bobServiceWorker.storageId()
102
+ const bobStorageId = await bobServiceWorker.storageId()
90
103
 
91
104
  return {
92
- leftTab1,
93
- leftTab2,
94
- leftServiceWorker,
105
+ alice,
106
+ alice2,
107
+ aliceServiceWorker,
95
108
  syncServer,
96
- rightServiceWorker,
97
- rightTab,
109
+ bobServiceWorker,
110
+ bob,
111
+ alice1StorageId,
112
+ alice2StorageId,
113
+ aliceServiceWorkerStorageId,
114
+ syncServerStorageId,
115
+ bobServiceWorkerStorageId,
116
+ bobStorageId,
98
117
  }
99
118
  }
100
119
 
101
120
  it("should report remoteHeads for peers", async () => {
102
- const { rightTab, rightServiceWorker, leftServiceWorker, leftTab1 } =
121
+ const { bob, aliceServiceWorkerStorageId, aliceServiceWorker, alice } =
103
122
  await setup()
104
123
 
105
- // subscribe to the left service worker storage ID on the right tab
106
- rightTab.subscribeToRemotes([await leftServiceWorker.storageId()!])
107
-
108
- await setTimeout(100)
124
+ // bob subscribes to alice's service worker's storageId
125
+ bob.subscribeToRemotes([aliceServiceWorkerStorageId])
109
126
 
110
- // create a doc in the left tab
111
- const leftTabDoc = leftTab1.create<TestDoc>()
112
- leftTabDoc.change(d => (d.foo = "bar"))
127
+ // alice creates a doc
128
+ const aliceDoc = alice.create<TestDoc>()
129
+ aliceDoc.change(d => (d.foo = "bar"))
113
130
 
114
- // wait for the document to arrive on the right tab
115
- const rightTabDoc = rightTab.find<TestDoc>(leftTabDoc.url)
116
- await rightTabDoc.whenReady()
131
+ // bob waits for the document to arrive
132
+ const bobDoc = bob.find<TestDoc>(aliceDoc.url)
133
+ await bobDoc.whenReady()
117
134
 
118
- // wait for the document to arrive in the left service worker
119
- const leftServiceWorkerDoc = leftServiceWorker.find(leftTabDoc.documentId)
120
- await leftServiceWorkerDoc.whenReady()
135
+ // alice's service worker waits for the document to arrive
136
+ const aliceServiceWorkerDoc = aliceServiceWorker.find(aliceDoc.documentId)
137
+ await aliceServiceWorkerDoc.whenReady()
121
138
 
122
- const leftServiceWorkerStorageId = await leftServiceWorker.storageId()
123
- let leftSeenByRightPromise = new Promise<DocHandleRemoteHeadsPayload>(
139
+ let aliceSeenByBobPromise = new Promise<DocHandleRemoteHeadsPayload>(
124
140
  resolve => {
125
- rightTabDoc.on("remote-heads", message => {
126
- if (message.storageId === leftServiceWorkerStorageId) {
141
+ bobDoc.on("remote-heads", message => {
142
+ if (message.storageId === aliceServiceWorkerStorageId) {
127
143
  resolve(message)
128
144
  }
129
145
  })
130
146
  }
131
147
  )
132
148
 
133
- // make a change on the right
134
- rightTabDoc.change(d => (d.foo = "baz"))
149
+ // bob makes a change
150
+ bobDoc.change(d => (d.foo = "baz"))
135
151
 
136
- // wait for the change to be acknolwedged by the left
137
- const leftSeenByRight = await leftSeenByRightPromise
152
+ // wait for alice's service worker to acknowledge the change
153
+ const { heads } = await aliceSeenByBobPromise
138
154
 
139
- assert.deepStrictEqual(
140
- leftSeenByRight.heads,
141
- A.getHeads(leftServiceWorkerDoc.docSync())
142
- )
155
+ assert.deepStrictEqual(heads, A.getHeads(aliceServiceWorkerDoc.docSync()))
143
156
  })
144
157
 
145
158
  it("should report remoteHeads only for documents the subscriber has open", async () => {
146
- const { leftTab1, rightTab, rightServiceWorker } = await setup()
159
+ const { alice, bob, bobServiceWorkerStorageId } = await setup()
147
160
 
148
- // subscribe leftTab to storageId of rightServiceWorker
149
- leftTab1.subscribeToRemotes([await rightServiceWorker.storageId()!])
161
+ // alice subscribes to bob's service worker
162
+ alice.subscribeToRemotes([bobServiceWorkerStorageId])
150
163
 
151
- await setTimeout(100)
164
+ // bob creates two docs
165
+ const bobDocA = bob.create<TestDoc>()
166
+ bobDocA.change(d => (d.foo = "A"))
152
167
 
153
- // create 2 docs in right tab
154
- const rightTabDocA = rightTab.create<TestDoc>()
155
- rightTabDocA.change(d => (d.foo = "A"))
168
+ const bobDocB = bob.create<TestDoc>()
169
+ bobDocB.change(d => (d.foo = "B"))
156
170
 
157
- const rightTabDocB = rightTab.create<TestDoc>()
158
- rightTabDocB.change(d => (d.foo = "B"))
159
-
160
- // open doc b in left tab 1
161
- const leftTabDocA = leftTab1.find<TestDoc>(rightTabDocA.url)
171
+ // alice opens doc A
172
+ const aliceDocA = alice.find<TestDoc>(bobDocA.url)
162
173
 
163
174
  const remoteHeadsChangedMessages = (
164
- await waitForMessages(leftTab1.networkSubsystem, "message")
175
+ await collectMessages({
176
+ emitter: alice.networkSubsystem,
177
+ event: "message",
178
+ until: aliceDocA.whenReady(),
179
+ })
165
180
  ).filter(({ type }) => type === "remote-heads-changed")
166
181
 
167
182
  // we should only be notified of the head changes of doc A
168
- const docIds = remoteHeadsChangedMessages.map(d => d.documentId)
169
- const uniqueDocIds = [...new Set(docIds)]
170
- assert.deepStrictEqual(uniqueDocIds, [leftTabDocA.documentId])
183
+ assert(
184
+ remoteHeadsChangedMessages.every(
185
+ d => d.documentId === aliceDocA.documentId
186
+ )
187
+ )
171
188
  })
172
189
 
173
190
  it("should report remote heads for doc on subscribe if peer already knows them", async () => {
174
- const { leftTab1, leftTab2, rightTab, rightServiceWorker } = await setup()
191
+ const { alice, alice2, bob, bobServiceWorkerStorageId } = await setup()
175
192
 
176
- // create 2 docs in right tab
177
- const rightTabDocA = rightTab.create<TestDoc>()
178
- rightTabDocA.change(d => (d.foo = "A"))
193
+ // bob creates 2 docs
194
+ const bobDocA = bob.create<TestDoc>()
195
+ bobDocA.change(d => (d.foo = "A"))
179
196
 
180
- const rightTabDocB = rightTab.create<TestDoc>()
181
- rightTabDocB.change(d => (d.foo = "B"))
197
+ const bobDocB = bob.create<TestDoc>()
198
+ bobDocB.change(d => (d.foo = "B"))
182
199
 
183
- // open docs in left tab 1
184
- const leftTab1DocA = leftTab1.find<TestDoc>(rightTabDocA.url)
185
- const leftTab1DocB = leftTab1.find<TestDoc>(rightTabDocB.url)
200
+ // alice opens the docs
201
+ const _aliceDocA = alice.find<TestDoc>(bobDocA.url)
202
+ const _aliceDocB = alice.find<TestDoc>(bobDocB.url)
186
203
 
187
- // subscribe leftTab 1 to storageId of rightServiceWorker
188
- leftTab1.subscribeToRemotes([await rightServiceWorker.storageId()!])
204
+ // alice subscribes to bob's service worker
205
+ alice.subscribeToRemotes([bobServiceWorkerStorageId])
189
206
 
190
- await setTimeout(200)
207
+ // Now alice's service worker has the remote heads of bob's service worker for both doc A and
208
+ // doc B. If alice subscribes to bob's service worker, bob's service worker should send its
209
+ // stored remote heads immediately.
191
210
 
192
- // now the left service worker has the remote heads of the right service worker for both doc A and doc B
193
- // if we subscribe from left tab 1 the left service workers should send it's stored remote heads immediately
194
-
195
- // open doc and subscribe leftTab 2 to storageId of rightServiceWorker
196
- const leftTab2DocA = leftTab2.find<TestDoc>(rightTabDocA.url)
197
- leftTab2.subscribeToRemotes([await rightServiceWorker.storageId()!])
211
+ // open doc and subscribe alice's second tab to bob's service worker
212
+ const alice2DocA = alice2.find<TestDoc>(bobDocA.url)
213
+ alice2.subscribeToRemotes([bobServiceWorkerStorageId])
198
214
 
199
215
  const remoteHeadsChangedMessages = (
200
- await waitForMessages(leftTab2.networkSubsystem, "message")
216
+ await collectMessages({
217
+ emitter: alice2.networkSubsystem,
218
+ event: "message",
219
+ until: alice2DocA.whenReady(),
220
+ })
201
221
  ).filter(({ type }) => type === "remote-heads-changed")
202
222
 
203
223
  // we should only be notified of the head changes of doc A
204
- assert.strictEqual(remoteHeadsChangedMessages.length, 1)
205
- assert.strictEqual(
206
- remoteHeadsChangedMessages[0].documentId,
207
- leftTab1DocA.documentId
224
+ assert.strictEqual(remoteHeadsChangedMessages.length, 2)
225
+ assert(
226
+ remoteHeadsChangedMessages.every(
227
+ d => d.documentId === alice2DocA.documentId
228
+ )
208
229
  )
209
230
  })
210
231
 
211
232
  it("should report remote heads for subscribed storage id once we open a new doc", async () => {
212
- const { leftTab1, leftTab2, rightTab, rightServiceWorker } = await setup()
213
-
214
- // create 2 docs in right tab
215
- const rightTabDocA = rightTab.create<TestDoc>()
216
- rightTabDocA.change(d => (d.foo = "A"))
233
+ const { alice, bob, bobServiceWorkerStorageId } = await setup()
217
234
 
218
- const rightTabDocB = rightTab.create<TestDoc>()
219
- rightTabDocB.change(d => (d.foo = "B"))
235
+ // bob creates 2 docs
236
+ const bobDocA = bob.create<TestDoc>()
237
+ bobDocA.change(d => (d.foo = "A"))
220
238
 
221
- await setTimeout(200)
239
+ const bobDocB = bob.create<TestDoc>()
240
+ bobDocB.change(d => (d.foo = "B"))
222
241
 
223
- // subscribe leftTab 1 to storageId of rightServiceWorker
224
- leftTab1.subscribeToRemotes([await rightServiceWorker.storageId()!])
242
+ // alice subscribes to bob's service worker
243
+ alice.subscribeToRemotes([bobServiceWorkerStorageId])
225
244
 
226
- // in leftTab 1 open doc A
227
- const leftTab1DocA = leftTab1.find<TestDoc>(rightTabDocA.url)
245
+ // alice opens doc A
246
+ const alice1DocA = alice.find<TestDoc>(bobDocA.url)
228
247
 
229
248
  const remoteHeadsChangedMessages = (
230
- await waitForMessages(leftTab1.networkSubsystem, "message")
249
+ await collectMessages({
250
+ emitter: alice.networkSubsystem,
251
+ event: "message",
252
+ until: alice1DocA.whenReady(),
253
+ })
231
254
  ).filter(({ type }) => type === "remote-heads-changed")
232
255
 
233
- console.log(JSON.stringify(remoteHeadsChangedMessages, null, 2))
234
-
235
- assert.strictEqual(remoteHeadsChangedMessages.length, 1)
236
- assert.strictEqual(
237
- remoteHeadsChangedMessages[0].documentId,
238
- leftTab1DocA.documentId
256
+ assert.strictEqual(remoteHeadsChangedMessages.length, 2)
257
+ assert(
258
+ remoteHeadsChangedMessages.every(
259
+ d => d.documentId === alice1DocA.documentId
260
+ )
239
261
  )
240
262
  })
241
263
  })
242
264
  })
243
265
 
244
- function connectRepos(repo1: Repo, repo2: Repo) {
245
- const { port1: leftToRight, port2: rightToLeft } = new MessageChannel()
246
-
247
- repo1.networkSubsystem.addNetworkAdapter(
248
- new MessageChannelNetworkAdapter(leftToRight)
249
- )
250
- repo2.networkSubsystem.addNetworkAdapter(
251
- new MessageChannelNetworkAdapter(rightToLeft)
252
- )
266
+ async function connectRepos(a: Repo, b: Repo) {
267
+ const { port1: a2b, port2: b2a } = new MessageChannel()
268
+ const aAdapter = new MessageChannelNetworkAdapter(a2b)
269
+ const bAdapter = new MessageChannelNetworkAdapter(b2a)
270
+ a.networkSubsystem.addNetworkAdapter(aAdapter)
271
+ b.networkSubsystem.addNetworkAdapter(bAdapter)
272
+ await Promise.all([
273
+ eventPromise(a.networkSubsystem, "ready"),
274
+ eventPromise(b.networkSubsystem, "ready"),
275
+ ])
253
276
  }
package/.eslintrc DELETED
@@ -1,28 +0,0 @@
1
- {
2
- "env": {
3
- "browser": true,
4
- "es2021": true
5
- },
6
- "extends": [
7
- "eslint:recommended",
8
- "plugin:@typescript-eslint/eslint-recommended",
9
- "plugin:@typescript-eslint/recommended"
10
- ],
11
- "ignorePatterns": ["dist/**", "test/**", "node_modules/**"],
12
- "parser": "@typescript-eslint/parser",
13
- "plugins": ["@typescript-eslint"],
14
- "parserOptions": {
15
- "project": "./tsconfig.json",
16
- "ecmaVersion": "latest",
17
- "sourceType": "module"
18
- },
19
- "rules": {
20
- "semi": ["error", "never"],
21
- "import/extensions": 0,
22
- "lines-between-class-members": 0,
23
- "@typescript-eslint/no-floating-promises": "error",
24
- "@typescript-eslint/no-empty-function": ["warn", { "allow": ["methods"] }],
25
- "no-param-reassign": 0,
26
- "no-use-before-define": 0
27
- }
28
- }
@@ -1,22 +0,0 @@
1
- import { EventEmitter } from "eventemitter3"
2
- import { pause } from "../../src/helpers/pause.js"
3
-
4
- export async function waitForMessages(
5
- emitter: EventEmitter,
6
- event: string,
7
- timeout: number = 100
8
- ): Promise<any[]> {
9
- const messages = []
10
-
11
- const onEvent = message => {
12
- messages.push(message)
13
- }
14
-
15
- emitter.on(event, onEvent)
16
-
17
- await pause(timeout)
18
-
19
- emitter.off(event)
20
-
21
- return messages
22
- }