@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.
- package/README.md +8 -8
- package/dist/AutomergeUrl.d.ts +17 -5
- package/dist/AutomergeUrl.d.ts.map +1 -1
- package/dist/AutomergeUrl.js +71 -24
- package/dist/DocHandle.d.ts +87 -30
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +198 -48
- package/dist/FindProgress.d.ts +30 -0
- package/dist/FindProgress.d.ts.map +1 -0
- package/dist/FindProgress.js +1 -0
- package/dist/RemoteHeadsSubscriptions.d.ts +4 -5
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
- package/dist/RemoteHeadsSubscriptions.js +4 -1
- package/dist/Repo.d.ts +46 -6
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +252 -67
- package/dist/helpers/abortable.d.ts +39 -0
- package/dist/helpers/abortable.d.ts.map +1 -0
- package/dist/helpers/abortable.js +45 -0
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -1
- package/dist/helpers/bufferFromHex.d.ts +3 -0
- package/dist/helpers/bufferFromHex.d.ts.map +1 -0
- package/dist/helpers/bufferFromHex.js +13 -0
- package/dist/helpers/debounce.d.ts.map +1 -1
- package/dist/helpers/eventPromise.d.ts.map +1 -1
- package/dist/helpers/headsAreSame.d.ts +2 -2
- package/dist/helpers/headsAreSame.d.ts.map +1 -1
- package/dist/helpers/mergeArrays.d.ts +1 -1
- package/dist/helpers/mergeArrays.d.ts.map +1 -1
- package/dist/helpers/pause.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +13 -13
- package/dist/helpers/tests/storage-adapter-tests.d.ts +2 -2
- package/dist/helpers/tests/storage-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.js +25 -48
- package/dist/helpers/throttle.d.ts.map +1 -1
- package/dist/helpers/withTimeout.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/network/messages.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.d.ts +15 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +50 -14
- package/dist/synchronizer/CollectionSynchronizer.d.ts +4 -3
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +34 -15
- package/dist/synchronizer/DocSynchronizer.d.ts +3 -2
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +51 -27
- package/dist/synchronizer/Synchronizer.d.ts +11 -0
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +3 -3
- package/package.json +3 -4
- package/src/AutomergeUrl.ts +101 -26
- package/src/DocHandle.ts +268 -58
- package/src/FindProgress.ts +48 -0
- package/src/RemoteHeadsSubscriptions.ts +11 -9
- package/src/Repo.ts +364 -74
- package/src/helpers/abortable.ts +61 -0
- package/src/helpers/bufferFromHex.ts +14 -0
- package/src/helpers/headsAreSame.ts +2 -2
- package/src/helpers/tests/network-adapter-tests.ts +14 -13
- package/src/helpers/tests/storage-adapter-tests.ts +44 -86
- package/src/index.ts +7 -0
- package/src/storage/StorageSubsystem.ts +66 -16
- package/src/synchronizer/CollectionSynchronizer.ts +37 -16
- package/src/synchronizer/DocSynchronizer.ts +59 -32
- package/src/synchronizer/Synchronizer.ts +14 -0
- package/src/types.ts +4 -1
- package/test/AutomergeUrl.test.ts +130 -0
- package/test/CollectionSynchronizer.test.ts +4 -4
- package/test/DocHandle.test.ts +255 -30
- package/test/DocSynchronizer.test.ts +10 -3
- package/test/Repo.test.ts +376 -203
- package/test/StorageSubsystem.test.ts +80 -1
- package/test/remoteHeads.test.ts +27 -12
package/test/Repo.test.ts
CHANGED
|
@@ -3,8 +3,11 @@ 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"
|
|
7
6
|
import {
|
|
7
|
+
encodeHeads,
|
|
8
|
+
getHeadsFromUrl,
|
|
9
|
+
isValidAutomergeUrl,
|
|
10
|
+
parseAutomergeUrl,
|
|
8
11
|
generateAutomergeUrl,
|
|
9
12
|
stringifyAutomergeUrl,
|
|
10
13
|
} from "../src/AutomergeUrl.js"
|
|
@@ -13,6 +16,7 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
|
|
|
13
16
|
import { pause } from "../src/helpers/pause.js"
|
|
14
17
|
import {
|
|
15
18
|
AnyDocumentId,
|
|
19
|
+
UrlHeads,
|
|
16
20
|
AutomergeUrl,
|
|
17
21
|
DocHandle,
|
|
18
22
|
DocumentId,
|
|
@@ -29,6 +33,7 @@ import {
|
|
|
29
33
|
import { getRandomItem } from "./helpers/getRandomItem.js"
|
|
30
34
|
import { TestDoc } from "./types.js"
|
|
31
35
|
import { StorageId, StorageKey } from "../src/storage/types.js"
|
|
36
|
+
import { chunkTypeFromKey } from "../src/storage/chunkTypeFromKey.js"
|
|
32
37
|
|
|
33
38
|
describe("Repo", () => {
|
|
34
39
|
describe("constructor", () => {
|
|
@@ -72,35 +77,34 @@ describe("Repo", () => {
|
|
|
72
77
|
it("can create a document with an initial value", async () => {
|
|
73
78
|
const { repo } = setup()
|
|
74
79
|
const handle = repo.create({ foo: "bar" })
|
|
75
|
-
|
|
76
|
-
assert.equal(handle.docSync().foo, "bar")
|
|
80
|
+
assert.equal(handle.doc().foo, "bar")
|
|
77
81
|
})
|
|
78
82
|
|
|
79
|
-
it("can find a document by url", () => {
|
|
83
|
+
it("can find a document by url", async () => {
|
|
80
84
|
const { repo } = setup()
|
|
81
85
|
const handle = repo.create<TestDoc>()
|
|
82
86
|
handle.change((d: TestDoc) => {
|
|
83
87
|
d.foo = "bar"
|
|
84
88
|
})
|
|
85
89
|
|
|
86
|
-
const handle2 = repo.find(handle.url)
|
|
90
|
+
const handle2 = await repo.find(handle.url)
|
|
87
91
|
assert.equal(handle, handle2)
|
|
88
|
-
assert.deepEqual(handle2.
|
|
92
|
+
assert.deepEqual(handle2.doc(), { foo: "bar" })
|
|
89
93
|
})
|
|
90
94
|
|
|
91
|
-
it("can find a document by its unprefixed document ID", () => {
|
|
95
|
+
it("can find a document by its unprefixed document ID", async () => {
|
|
92
96
|
const { repo } = setup()
|
|
93
97
|
const handle = repo.create<TestDoc>()
|
|
94
98
|
handle.change((d: TestDoc) => {
|
|
95
99
|
d.foo = "bar"
|
|
96
100
|
})
|
|
97
101
|
|
|
98
|
-
const handle2 = repo.find(handle.documentId)
|
|
102
|
+
const handle2 = await repo.find(handle.documentId)
|
|
99
103
|
assert.equal(handle, handle2)
|
|
100
|
-
assert.deepEqual(handle2.
|
|
104
|
+
assert.deepEqual(handle2.doc(), { foo: "bar" })
|
|
101
105
|
})
|
|
102
106
|
|
|
103
|
-
it("can find a document by legacy UUID (for now)", () => {
|
|
107
|
+
it("can find a document by legacy UUID (for now)", async () => {
|
|
104
108
|
disableConsoleWarn()
|
|
105
109
|
|
|
106
110
|
const { repo } = setup()
|
|
@@ -113,9 +117,9 @@ describe("Repo", () => {
|
|
|
113
117
|
const { binaryDocumentId } = parseAutomergeUrl(url)
|
|
114
118
|
const legacyDocId = Uuid.stringify(binaryDocumentId) as LegacyDocumentId
|
|
115
119
|
|
|
116
|
-
const handle2 = repo.find(legacyDocId)
|
|
120
|
+
const handle2 = await repo.find(legacyDocId)
|
|
117
121
|
assert.equal(handle, handle2)
|
|
118
|
-
assert.deepEqual(handle2.
|
|
122
|
+
assert.deepEqual(handle2.doc(), { foo: "bar" })
|
|
119
123
|
|
|
120
124
|
reenableConsoleWarn()
|
|
121
125
|
})
|
|
@@ -126,7 +130,7 @@ describe("Repo", () => {
|
|
|
126
130
|
handle.change(d => {
|
|
127
131
|
d.foo = "bar"
|
|
128
132
|
})
|
|
129
|
-
const v =
|
|
133
|
+
const v = handle.doc()
|
|
130
134
|
assert.equal(handle.isReady(), true)
|
|
131
135
|
assert.equal(v.foo, "bar")
|
|
132
136
|
})
|
|
@@ -140,8 +144,8 @@ describe("Repo", () => {
|
|
|
140
144
|
const handle2 = repo.clone(handle)
|
|
141
145
|
assert.equal(handle2.isReady(), true)
|
|
142
146
|
assert.notEqual(handle.documentId, handle2.documentId)
|
|
143
|
-
assert.deepStrictEqual(handle.
|
|
144
|
-
assert.deepStrictEqual(handle2.
|
|
147
|
+
assert.deepStrictEqual(handle.doc(), handle2.doc())
|
|
148
|
+
assert.deepStrictEqual(handle2.doc(), { foo: "bar" })
|
|
145
149
|
})
|
|
146
150
|
|
|
147
151
|
it("the cloned documents are distinct", () => {
|
|
@@ -159,9 +163,9 @@ describe("Repo", () => {
|
|
|
159
163
|
d.baz = "baz"
|
|
160
164
|
})
|
|
161
165
|
|
|
162
|
-
assert.notDeepStrictEqual(handle.
|
|
163
|
-
assert.deepStrictEqual(handle.
|
|
164
|
-
assert.deepStrictEqual(handle2.
|
|
166
|
+
assert.notDeepStrictEqual(handle.doc(), handle2.doc())
|
|
167
|
+
assert.deepStrictEqual(handle.doc(), { foo: "bar", bar: "bif" })
|
|
168
|
+
assert.deepStrictEqual(handle2.doc(), { foo: "bar", baz: "baz" })
|
|
165
169
|
})
|
|
166
170
|
|
|
167
171
|
it("the cloned documents can merge", () => {
|
|
@@ -181,59 +185,47 @@ describe("Repo", () => {
|
|
|
181
185
|
|
|
182
186
|
handle.merge(handle2)
|
|
183
187
|
|
|
184
|
-
assert.deepStrictEqual(handle.
|
|
188
|
+
assert.deepStrictEqual(handle.doc(), {
|
|
185
189
|
foo: "bar",
|
|
186
190
|
bar: "bif",
|
|
187
191
|
baz: "baz",
|
|
188
192
|
})
|
|
189
193
|
// only the one handle should be changed
|
|
190
|
-
assert.deepStrictEqual(handle2.
|
|
194
|
+
assert.deepStrictEqual(handle2.doc(), { foo: "bar", baz: "baz" })
|
|
191
195
|
})
|
|
192
196
|
|
|
193
197
|
it("throws an error if we try to find a handle with an invalid AutomergeUrl", async () => {
|
|
194
198
|
const { repo } = setup()
|
|
195
|
-
|
|
196
|
-
repo.find<TestDoc>("invalid-url" as unknown as AutomergeUrl)
|
|
197
|
-
}
|
|
198
|
-
assert.equal(e.message, "Invalid AutomergeUrl: 'invalid-url'")
|
|
199
|
-
}
|
|
199
|
+
await expect(async () => {
|
|
200
|
+
await repo.find<TestDoc>("invalid-url" as unknown as AutomergeUrl)
|
|
201
|
+
}).rejects.toThrow("Invalid AutomergeUrl: 'invalid-url'")
|
|
200
202
|
})
|
|
201
203
|
|
|
202
204
|
it("doesn't find a document that doesn't exist", async () => {
|
|
203
205
|
const { repo } = setup()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
assert.equal(handle.isReady(), false)
|
|
209
|
-
assert.equal(handle.state, "unavailable")
|
|
210
|
-
const doc = await handle.doc()
|
|
211
|
-
assert.equal(doc, undefined)
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it("emits an unavailable event when you don't have the document locally and are not connected to anyone", async () => {
|
|
215
|
-
const { repo } = setup()
|
|
216
|
-
const url = generateAutomergeUrl()
|
|
217
|
-
const handle = repo.find<TestDoc>(url)
|
|
218
|
-
assert.equal(handle.isReady(), false)
|
|
219
|
-
await eventPromise(handle, "unavailable")
|
|
206
|
+
await expect(async () => {
|
|
207
|
+
await repo.find<TestDoc>(generateAutomergeUrl())
|
|
208
|
+
}).rejects.toThrow(/Document (.*) is unavailable/)
|
|
220
209
|
})
|
|
221
210
|
|
|
222
211
|
it("doesn't mark a document as unavailable until network adapters are ready", async () => {
|
|
223
212
|
const { repo, networkAdapter } = setup({ startReady: false })
|
|
224
213
|
const url = generateAutomergeUrl()
|
|
225
|
-
const handle = repo.find<TestDoc>(url)
|
|
226
214
|
|
|
227
|
-
|
|
228
|
-
handle.on("unavailable", () => {
|
|
229
|
-
wasUnavailable = true
|
|
230
|
-
})
|
|
215
|
+
const attemptedFind = repo.find<TestDoc>(url)
|
|
231
216
|
|
|
232
|
-
|
|
233
|
-
|
|
217
|
+
// First verify it stays pending for 50ms
|
|
218
|
+
await expect(
|
|
219
|
+
Promise.race([attemptedFind, pause(50)])
|
|
220
|
+
).resolves.toBeUndefined()
|
|
234
221
|
|
|
222
|
+
// Trigger the rejection
|
|
235
223
|
networkAdapter.forceReady()
|
|
236
|
-
|
|
224
|
+
|
|
225
|
+
// Now verify it rejects
|
|
226
|
+
await expect(attemptedFind).rejects.toThrow(
|
|
227
|
+
/Document (.*) is unavailable/
|
|
228
|
+
)
|
|
237
229
|
})
|
|
238
230
|
|
|
239
231
|
it("can find a created document", async () => {
|
|
@@ -244,18 +236,18 @@ describe("Repo", () => {
|
|
|
244
236
|
})
|
|
245
237
|
assert.equal(handle.isReady(), true)
|
|
246
238
|
|
|
247
|
-
const bobHandle = repo.find<TestDoc>(handle.url)
|
|
239
|
+
const bobHandle = await repo.find<TestDoc>(handle.url)
|
|
248
240
|
|
|
249
241
|
assert.equal(handle, bobHandle)
|
|
250
242
|
assert.equal(handle.isReady(), true)
|
|
251
243
|
|
|
252
|
-
const v =
|
|
244
|
+
const v = bobHandle.doc()
|
|
253
245
|
assert.equal(v?.foo, "bar")
|
|
254
246
|
})
|
|
255
247
|
|
|
256
248
|
it("saves the document when creating it", async () => {
|
|
257
249
|
const { repo, storageAdapter } = setup()
|
|
258
|
-
const handle = repo.create<TestDoc>()
|
|
250
|
+
const handle = repo.create<TestDoc>({ foo: "saved" })
|
|
259
251
|
|
|
260
252
|
const repo2 = new Repo({
|
|
261
253
|
storage: storageAdapter,
|
|
@@ -263,9 +255,8 @@ describe("Repo", () => {
|
|
|
263
255
|
|
|
264
256
|
await repo.flush()
|
|
265
257
|
|
|
266
|
-
const bobHandle = repo2.find<TestDoc>(handle.url)
|
|
267
|
-
|
|
268
|
-
assert.equal(bobHandle.isReady(), true)
|
|
258
|
+
const bobHandle = await repo2.find<TestDoc>(handle.url)
|
|
259
|
+
assert.deepEqual(bobHandle.doc(), { foo: "saved" })
|
|
269
260
|
})
|
|
270
261
|
|
|
271
262
|
it("saves the document when changed and can find it again", async () => {
|
|
@@ -284,9 +275,9 @@ describe("Repo", () => {
|
|
|
284
275
|
storage: storageAdapter,
|
|
285
276
|
})
|
|
286
277
|
|
|
287
|
-
const bobHandle = repo2.find<TestDoc>(handle.url)
|
|
278
|
+
const bobHandle = await repo2.find<TestDoc>(handle.url)
|
|
288
279
|
|
|
289
|
-
const v =
|
|
280
|
+
const v = bobHandle.doc()
|
|
290
281
|
assert.equal(v?.foo, "bar")
|
|
291
282
|
})
|
|
292
283
|
|
|
@@ -298,7 +289,7 @@ describe("Repo", () => {
|
|
|
298
289
|
})
|
|
299
290
|
// we now have a snapshot and an incremental change in storage
|
|
300
291
|
assert.equal(handle.isReady(), true)
|
|
301
|
-
const foo =
|
|
292
|
+
const foo = handle.doc()
|
|
302
293
|
assert.equal(foo?.foo, "bar")
|
|
303
294
|
|
|
304
295
|
await pause()
|
|
@@ -315,7 +306,6 @@ describe("Repo", () => {
|
|
|
315
306
|
d.foo = "bar"
|
|
316
307
|
})
|
|
317
308
|
assert.equal(handle.isReady(), true)
|
|
318
|
-
await handle.doc()
|
|
319
309
|
|
|
320
310
|
await pause()
|
|
321
311
|
repo.delete(handle.url)
|
|
@@ -352,7 +342,7 @@ describe("Repo", () => {
|
|
|
352
342
|
|
|
353
343
|
const exported = await repo.export(handle.documentId)
|
|
354
344
|
const loaded = A.load(exported)
|
|
355
|
-
const doc =
|
|
345
|
+
const doc = handle.doc()
|
|
356
346
|
assert.deepEqual(doc, loaded)
|
|
357
347
|
})
|
|
358
348
|
|
|
@@ -386,9 +376,7 @@ describe("Repo", () => {
|
|
|
386
376
|
const repo2 = new Repo({
|
|
387
377
|
storage,
|
|
388
378
|
})
|
|
389
|
-
const handle2 = repo2.find(handle.url)
|
|
390
|
-
await handle2.doc()
|
|
391
|
-
|
|
379
|
+
const handle2 = await repo2.find(handle.url)
|
|
392
380
|
assert.deepEqual(storage.keys(), initialKeys)
|
|
393
381
|
})
|
|
394
382
|
|
|
@@ -414,9 +402,7 @@ describe("Repo", () => {
|
|
|
414
402
|
const repo2 = new Repo({
|
|
415
403
|
storage,
|
|
416
404
|
})
|
|
417
|
-
const handle2 = repo2.find(handle.url)
|
|
418
|
-
await handle2.doc()
|
|
419
|
-
|
|
405
|
+
const handle2 = await repo2.find(handle.url)
|
|
420
406
|
assert(storage.keys().length !== 0)
|
|
421
407
|
}
|
|
422
408
|
})
|
|
@@ -445,6 +431,40 @@ describe("Repo", () => {
|
|
|
445
431
|
)
|
|
446
432
|
})
|
|
447
433
|
|
|
434
|
+
it("should not call loadDoc multiple times when find() is called in quick succession", async () => {
|
|
435
|
+
const { repo, storageAdapter } = setup()
|
|
436
|
+
const handle = repo.create<TestDoc>()
|
|
437
|
+
handle.change(d => {
|
|
438
|
+
d.foo = "bar"
|
|
439
|
+
})
|
|
440
|
+
await repo.flush()
|
|
441
|
+
|
|
442
|
+
// Create a new repo instance that will use the same storage
|
|
443
|
+
const repo2 = new Repo({
|
|
444
|
+
storage: storageAdapter,
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
// Track how many times loadDoc is called
|
|
448
|
+
let loadDocCallCount = 0
|
|
449
|
+
const originalLoadDoc = repo2.storageSubsystem!.loadDoc.bind(
|
|
450
|
+
repo2.storageSubsystem
|
|
451
|
+
)
|
|
452
|
+
repo2.storageSubsystem!.loadDoc = async documentId => {
|
|
453
|
+
loadDocCallCount++
|
|
454
|
+
return originalLoadDoc(documentId)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Call find() twice in quick succession
|
|
458
|
+
const find1 = repo2.find(handle.url)
|
|
459
|
+
const find2 = repo2.find(handle.url)
|
|
460
|
+
|
|
461
|
+
// Wait for both calls to complete
|
|
462
|
+
await Promise.all([find1, find2])
|
|
463
|
+
|
|
464
|
+
// Verify loadDoc was only called once
|
|
465
|
+
assert.equal(loadDocCallCount, 1, "loadDoc should only be called once")
|
|
466
|
+
})
|
|
467
|
+
|
|
448
468
|
it("can import an existing document", async () => {
|
|
449
469
|
const { repo } = setup()
|
|
450
470
|
const doc = A.init<TestDoc>()
|
|
@@ -456,7 +476,7 @@ describe("Repo", () => {
|
|
|
456
476
|
|
|
457
477
|
const handle = repo.import<TestDoc>(saved)
|
|
458
478
|
assert.equal(handle.isReady(), true)
|
|
459
|
-
const v =
|
|
479
|
+
const v = handle.doc()
|
|
460
480
|
assert.equal(v?.foo, "bar")
|
|
461
481
|
|
|
462
482
|
expect(A.getHistory(v)).toEqual(A.getHistory(updatedDoc))
|
|
@@ -475,7 +495,7 @@ describe("Repo", () => {
|
|
|
475
495
|
const { repo } = setup()
|
|
476
496
|
// @ts-ignore - passing something other than UInt8Array
|
|
477
497
|
const handle = repo.import<TestDoc>(A.from({ foo: 123 }))
|
|
478
|
-
const doc =
|
|
498
|
+
const doc = handle.doc()
|
|
479
499
|
expect(doc).toEqual({})
|
|
480
500
|
})
|
|
481
501
|
|
|
@@ -483,9 +503,39 @@ describe("Repo", () => {
|
|
|
483
503
|
const { repo } = setup()
|
|
484
504
|
// @ts-ignore - passing something other than UInt8Array
|
|
485
505
|
const handle = repo.import<TestDoc>({ foo: 123 })
|
|
486
|
-
const doc =
|
|
506
|
+
const doc = handle.doc()
|
|
487
507
|
expect(doc).toEqual({})
|
|
488
508
|
})
|
|
509
|
+
|
|
510
|
+
describe("handle cache", () => {
|
|
511
|
+
it("contains doc handle", async () => {
|
|
512
|
+
const { repo } = setup()
|
|
513
|
+
const handle = repo.create({ foo: "bar" })
|
|
514
|
+
assert(repo.handles[handle.documentId])
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it("delete removes doc handle", async () => {
|
|
518
|
+
const { repo } = setup()
|
|
519
|
+
const handle = repo.create({ foo: "bar" })
|
|
520
|
+
await repo.delete(handle.documentId)
|
|
521
|
+
assert(repo.handles[handle.documentId] === undefined)
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it("removeFromCache removes doc handle", async () => {
|
|
525
|
+
const { repo } = setup()
|
|
526
|
+
const handle = repo.create({ foo: "bar" })
|
|
527
|
+
await repo.removeFromCache(handle.documentId)
|
|
528
|
+
assert(repo.handles[handle.documentId] === undefined)
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it("removeFromCache for documentId not found", async () => {
|
|
532
|
+
const { repo } = setup()
|
|
533
|
+
const badDocumentId = "badbadbad" as DocumentId
|
|
534
|
+
const handleCacheSize = Object.keys(repo.handles).length
|
|
535
|
+
await repo.removeFromCache(badDocumentId)
|
|
536
|
+
assert(Object.keys(repo.handles).length === handleCacheSize)
|
|
537
|
+
})
|
|
538
|
+
})
|
|
489
539
|
})
|
|
490
540
|
|
|
491
541
|
describe("flush behaviour", () => {
|
|
@@ -532,8 +582,8 @@ describe("Repo", () => {
|
|
|
532
582
|
|
|
533
583
|
it("should not be in a new repo yet because the storage is slow", async () => {
|
|
534
584
|
const { pausedStorage, repo, handle, handle2 } = setup()
|
|
535
|
-
expect((await handle.doc()
|
|
536
|
-
expect((await handle2.doc()
|
|
585
|
+
expect((await handle).doc().foo).toEqual("first")
|
|
586
|
+
expect((await handle2).doc().foo).toEqual("second")
|
|
537
587
|
|
|
538
588
|
// Reload repo
|
|
539
589
|
const repo2 = new Repo({
|
|
@@ -541,9 +591,10 @@ describe("Repo", () => {
|
|
|
541
591
|
})
|
|
542
592
|
|
|
543
593
|
// Could not find the document that is not yet saved because of slow storage.
|
|
544
|
-
|
|
594
|
+
await expect(async () => {
|
|
595
|
+
const reloadedHandle = await repo2.find<{ foo: string }>(handle.url)
|
|
596
|
+
}).rejects.toThrow(/Document (.*) is unavailable/)
|
|
545
597
|
expect(pausedStorage.keys()).to.deep.equal([])
|
|
546
|
-
expect(await reloadedHandle.doc()).toEqual(undefined)
|
|
547
598
|
})
|
|
548
599
|
|
|
549
600
|
it("should be visible to a new repo after flush()", async () => {
|
|
@@ -563,10 +614,10 @@ describe("Repo", () => {
|
|
|
563
614
|
})
|
|
564
615
|
|
|
565
616
|
expect(
|
|
566
|
-
(await repo.find<{ foo: string }>(handle.documentId).doc()
|
|
617
|
+
(await repo.find<{ foo: string }>(handle.documentId)).doc().foo
|
|
567
618
|
).toEqual("first")
|
|
568
619
|
expect(
|
|
569
|
-
(await repo.find<{ foo: string }>(handle2.documentId).doc()
|
|
620
|
+
(await repo.find<{ foo: string }>(handle2.documentId)).doc().foo
|
|
570
621
|
).toEqual("second")
|
|
571
622
|
}
|
|
572
623
|
})
|
|
@@ -588,13 +639,13 @@ describe("Repo", () => {
|
|
|
588
639
|
})
|
|
589
640
|
|
|
590
641
|
expect(
|
|
591
|
-
(await repo.find<{ foo: string }>(handle.documentId).doc()
|
|
642
|
+
(await repo.find<{ foo: string }>(handle.documentId)).doc().foo
|
|
592
643
|
).toEqual("first")
|
|
593
644
|
// Really, it's okay if the second one is also flushed but I'm forcing the issue
|
|
594
645
|
// in the test storage engine above to make sure the behaviour is as documented
|
|
595
|
-
expect(
|
|
596
|
-
await repo.find<{ foo: string }>(handle2.documentId).doc()
|
|
597
|
-
).
|
|
646
|
+
await expect(async () => {
|
|
647
|
+
;(await repo.find<{ foo: string }>(handle2.documentId)).doc()
|
|
648
|
+
}).rejects.toThrow(/Document (.*) is unavailable/)
|
|
598
649
|
}
|
|
599
650
|
})
|
|
600
651
|
|
|
@@ -642,7 +693,7 @@ describe("Repo", () => {
|
|
|
642
693
|
|
|
643
694
|
if (idx < numberOfPeers - 1) {
|
|
644
695
|
network.push(pair[0])
|
|
645
|
-
pair[0].whenReady()
|
|
696
|
+
networkReady.push(pair[0].whenReady())
|
|
646
697
|
}
|
|
647
698
|
|
|
648
699
|
const repo = new Repo({
|
|
@@ -673,7 +724,6 @@ describe("Repo", () => {
|
|
|
673
724
|
}
|
|
674
725
|
|
|
675
726
|
await connectedPromise
|
|
676
|
-
|
|
677
727
|
return { repos }
|
|
678
728
|
}
|
|
679
729
|
|
|
@@ -685,10 +735,14 @@ describe("Repo", () => {
|
|
|
685
735
|
d.foo = "bar"
|
|
686
736
|
})
|
|
687
737
|
|
|
688
|
-
const handleN = repos[numberOfPeers - 1].find<TestDoc>(handle0.url)
|
|
738
|
+
const handleN = await repos[numberOfPeers - 1].find<TestDoc>(handle0.url)
|
|
739
|
+
assert.deepStrictEqual(handleN.doc(), { foo: "bar" })
|
|
689
740
|
|
|
690
|
-
|
|
691
|
-
|
|
741
|
+
const handleNBack = repos[numberOfPeers - 1].create({
|
|
742
|
+
foo: "reverse-trip",
|
|
743
|
+
})
|
|
744
|
+
const handle0Back = await repos[0].find<TestDoc>(handleNBack.url)
|
|
745
|
+
assert.deepStrictEqual(handle0Back.doc(), { foo: "reverse-trip" })
|
|
692
746
|
})
|
|
693
747
|
|
|
694
748
|
const setup = async ({
|
|
@@ -815,9 +869,8 @@ describe("Repo", () => {
|
|
|
815
869
|
it("changes are replicated from aliceRepo to bobRepo", async () => {
|
|
816
870
|
const { bobRepo, aliceHandle, teardown } = await setup()
|
|
817
871
|
|
|
818
|
-
const bobHandle = bobRepo.find<TestDoc>(aliceHandle.url)
|
|
819
|
-
|
|
820
|
-
const bobDoc = await bobHandle.doc()
|
|
872
|
+
const bobHandle = await bobRepo.find<TestDoc>(aliceHandle.url)
|
|
873
|
+
const bobDoc = bobHandle.doc()
|
|
821
874
|
assert.deepStrictEqual(bobDoc, { foo: "bar" })
|
|
822
875
|
teardown()
|
|
823
876
|
})
|
|
@@ -825,16 +878,14 @@ describe("Repo", () => {
|
|
|
825
878
|
it("can load a document from aliceRepo on charlieRepo", async () => {
|
|
826
879
|
const { charlieRepo, aliceHandle, teardown } = await setup()
|
|
827
880
|
|
|
828
|
-
const handle3 = charlieRepo.find<TestDoc>(aliceHandle.url)
|
|
829
|
-
|
|
830
|
-
const doc3 = await handle3.doc()
|
|
881
|
+
const handle3 = await charlieRepo.find<TestDoc>(aliceHandle.url)
|
|
882
|
+
const doc3 = handle3.doc()
|
|
831
883
|
assert.deepStrictEqual(doc3, { foo: "bar" })
|
|
832
884
|
teardown()
|
|
833
885
|
})
|
|
834
886
|
|
|
835
887
|
it("synchronizes changes from bobRepo to charlieRepo when loading from storage", async () => {
|
|
836
|
-
const { bobRepo, bobStorage,
|
|
837
|
-
await setup()
|
|
888
|
+
const { bobRepo, bobStorage, teardown } = await setup()
|
|
838
889
|
|
|
839
890
|
// We create a repo that uses bobStorage to put a document into its imaginary disk
|
|
840
891
|
// without it knowing about it
|
|
@@ -846,14 +897,14 @@ describe("Repo", () => {
|
|
|
846
897
|
})
|
|
847
898
|
await bobRepo2.flush()
|
|
848
899
|
|
|
849
|
-
console.log("loading from disk", inStorageHandle.url)
|
|
850
900
|
// Now, let's load it on the original bob repo (which shares a "disk")
|
|
851
|
-
const bobFoundIt = bobRepo.find<TestDoc>(inStorageHandle.url)
|
|
852
|
-
await bobFoundIt.whenReady()
|
|
901
|
+
const bobFoundIt = await bobRepo.find<TestDoc>(inStorageHandle.url)
|
|
853
902
|
|
|
854
903
|
// Before checking if it syncs, make sure we have it!
|
|
855
904
|
// (This behaviour is mostly test-validation, we are already testing load/save elsewhere.)
|
|
856
|
-
assert.deepStrictEqual(
|
|
905
|
+
assert.deepStrictEqual(bobFoundIt.doc(), { foo: "foundOnFakeDisk" })
|
|
906
|
+
|
|
907
|
+
await pause(10)
|
|
857
908
|
|
|
858
909
|
// We should have a docSynchronizer and its peers should be alice and charlie
|
|
859
910
|
assert.strictEqual(
|
|
@@ -891,11 +942,8 @@ describe("Repo", () => {
|
|
|
891
942
|
it("charlieRepo can request a document not initially shared with it", async () => {
|
|
892
943
|
const { charlieRepo, notForCharlie, teardown } = await setup()
|
|
893
944
|
|
|
894
|
-
const handle = charlieRepo.find<TestDoc>(notForCharlie)
|
|
895
|
-
|
|
896
|
-
await pause(50)
|
|
897
|
-
|
|
898
|
-
const doc = await handle.doc()
|
|
945
|
+
const handle = await charlieRepo.find<TestDoc>(notForCharlie)
|
|
946
|
+
const doc = handle.doc()
|
|
899
947
|
|
|
900
948
|
assert.deepStrictEqual(doc, { foo: "baz" })
|
|
901
949
|
|
|
@@ -905,11 +953,11 @@ describe("Repo", () => {
|
|
|
905
953
|
it("charlieRepo can request a document across a network of multiple peers", async () => {
|
|
906
954
|
const { charlieRepo, notForBob, teardown } = await setup()
|
|
907
955
|
|
|
908
|
-
const handle = charlieRepo.find<TestDoc>(notForBob)
|
|
956
|
+
const handle = await charlieRepo.find<TestDoc>(notForBob)
|
|
909
957
|
|
|
910
958
|
await pause(50)
|
|
911
959
|
|
|
912
|
-
const doc =
|
|
960
|
+
const doc = handle.doc()
|
|
913
961
|
assert.deepStrictEqual(doc, { foo: "bap" })
|
|
914
962
|
|
|
915
963
|
teardown()
|
|
@@ -918,42 +966,10 @@ describe("Repo", () => {
|
|
|
918
966
|
it("doesn't find a document which doesn't exist anywhere on the network", async () => {
|
|
919
967
|
const { charlieRepo, teardown } = await setup()
|
|
920
968
|
const url = generateAutomergeUrl()
|
|
921
|
-
const handle = charlieRepo.find<TestDoc>(url)
|
|
922
|
-
assert.equal(handle.isReady(), false)
|
|
923
|
-
|
|
924
|
-
const doc = await handle.doc()
|
|
925
|
-
assert.equal(doc, undefined)
|
|
926
|
-
|
|
927
|
-
teardown()
|
|
928
|
-
})
|
|
929
|
-
|
|
930
|
-
it("emits an unavailable event when it's not found on the network", async () => {
|
|
931
|
-
const { aliceRepo, teardown } = await setup()
|
|
932
|
-
const url = generateAutomergeUrl()
|
|
933
|
-
const handle = aliceRepo.find(url)
|
|
934
|
-
assert.equal(handle.isReady(), false)
|
|
935
|
-
await eventPromise(handle, "unavailable")
|
|
936
|
-
teardown()
|
|
937
|
-
})
|
|
938
|
-
|
|
939
|
-
it("emits an unavailable event every time an unavailable doc is requested", async () => {
|
|
940
|
-
const { charlieRepo, teardown } = await setup()
|
|
941
|
-
const url = generateAutomergeUrl()
|
|
942
|
-
const handle = charlieRepo.find<TestDoc>(url)
|
|
943
|
-
assert.equal(handle.isReady(), false)
|
|
944
969
|
|
|
945
|
-
await
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
])
|
|
949
|
-
|
|
950
|
-
// make sure it emits a second time if the doc is still unavailable
|
|
951
|
-
const handle2 = charlieRepo.find<TestDoc>(url)
|
|
952
|
-
assert.equal(handle2.isReady(), false)
|
|
953
|
-
await Promise.all([
|
|
954
|
-
eventPromise(handle, "unavailable"),
|
|
955
|
-
eventPromise(charlieRepo, "unavailable-document"),
|
|
956
|
-
])
|
|
970
|
+
await expect(charlieRepo.find<TestDoc>(url)).rejects.toThrow(
|
|
971
|
+
/Document (.*) is unavailable/
|
|
972
|
+
)
|
|
957
973
|
|
|
958
974
|
teardown()
|
|
959
975
|
})
|
|
@@ -968,21 +984,23 @@ describe("Repo", () => {
|
|
|
968
984
|
} = await setup({ connectAlice: false })
|
|
969
985
|
|
|
970
986
|
const url = stringifyAutomergeUrl({ documentId: notForCharlie })
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
await eventPromise(handle, "unavailable")
|
|
987
|
+
await expect(charlieRepo.find<TestDoc>(url)).rejects.toThrow(
|
|
988
|
+
/Document (.*) is unavailable/
|
|
989
|
+
)
|
|
975
990
|
|
|
976
991
|
connectAliceToBob()
|
|
977
992
|
|
|
978
993
|
await eventPromise(aliceRepo.networkSubsystem, "peer")
|
|
979
994
|
|
|
980
|
-
|
|
995
|
+
// Not sure why we need this pause here, but... we do.
|
|
996
|
+
await pause(150)
|
|
997
|
+
const handle = await charlieRepo.find<TestDoc>(url)
|
|
998
|
+
const doc = handle.doc()
|
|
981
999
|
assert.deepStrictEqual(doc, { foo: "baz" })
|
|
982
1000
|
|
|
983
1001
|
// an additional find should also return the correct resolved document
|
|
984
|
-
const handle2 = charlieRepo.find<TestDoc>(url)
|
|
985
|
-
const doc2 =
|
|
1002
|
+
const handle2 = await charlieRepo.find<TestDoc>(url)
|
|
1003
|
+
const doc2 = handle2.doc()
|
|
986
1004
|
assert.deepStrictEqual(doc2, { foo: "baz" })
|
|
987
1005
|
|
|
988
1006
|
teardown()
|
|
@@ -1018,11 +1036,9 @@ describe("Repo", () => {
|
|
|
1018
1036
|
sharePolicy: async () => true,
|
|
1019
1037
|
})
|
|
1020
1038
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
// the repo has no storage.
|
|
1025
|
-
await eventPromise(handle, "unavailable")
|
|
1039
|
+
await expect(a.find<TestDoc>(url)).rejects.toThrow(
|
|
1040
|
+
/Document (.*) is unavailable/
|
|
1041
|
+
)
|
|
1026
1042
|
|
|
1027
1043
|
// Now create a repo pointing at the storage containing the document and
|
|
1028
1044
|
// connect it to the other end of the MessageChannel
|
|
@@ -1032,9 +1048,14 @@ describe("Repo", () => {
|
|
|
1032
1048
|
network: [new MessageChannelNetworkAdapter(ba)],
|
|
1033
1049
|
})
|
|
1034
1050
|
|
|
1051
|
+
// We need a proper peer status API so we can tell when the
|
|
1052
|
+
// peer is connected. For now we just wait a bit.
|
|
1053
|
+
await pause(50)
|
|
1054
|
+
|
|
1035
1055
|
// The empty repo should be notified of the new peer, send it a request
|
|
1036
1056
|
// and eventually resolve the handle to "READY"
|
|
1037
|
-
await
|
|
1057
|
+
const handle = await a.find<TestDoc>(url)
|
|
1058
|
+
expect(handle.state).toBe("ready")
|
|
1038
1059
|
})
|
|
1039
1060
|
|
|
1040
1061
|
it("a deleted document from charlieRepo can be refetched", async () => {
|
|
@@ -1050,9 +1071,8 @@ describe("Repo", () => {
|
|
|
1050
1071
|
})
|
|
1051
1072
|
await changePromise
|
|
1052
1073
|
|
|
1053
|
-
const handle3 = charlieRepo.find<TestDoc>(aliceHandle.url)
|
|
1054
|
-
|
|
1055
|
-
const doc3 = await handle3.doc()
|
|
1074
|
+
const handle3 = await charlieRepo.find<TestDoc>(aliceHandle.url)
|
|
1075
|
+
const doc3 = handle3.doc()
|
|
1056
1076
|
|
|
1057
1077
|
assert.deepStrictEqual(doc3, { foo: "baz" })
|
|
1058
1078
|
|
|
@@ -1076,11 +1096,6 @@ describe("Repo", () => {
|
|
|
1076
1096
|
: // tails, pick a random doc
|
|
1077
1097
|
(getRandomItem(docs) as DocHandle<TestDoc>)
|
|
1078
1098
|
|
|
1079
|
-
// make sure the doc is ready
|
|
1080
|
-
if (!doc.isReady()) {
|
|
1081
|
-
await doc.doc()
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
1099
|
// make a random change to it
|
|
1085
1100
|
doc.change(d => {
|
|
1086
1101
|
d.foo = Math.random().toString()
|
|
@@ -1096,10 +1111,10 @@ describe("Repo", () => {
|
|
|
1096
1111
|
|
|
1097
1112
|
const data = { presence: "alice" }
|
|
1098
1113
|
|
|
1099
|
-
const aliceHandle = aliceRepo.find<TestDoc>(
|
|
1114
|
+
const aliceHandle = await aliceRepo.find<TestDoc>(
|
|
1100
1115
|
stringifyAutomergeUrl({ documentId: notForCharlie })
|
|
1101
1116
|
)
|
|
1102
|
-
const bobHandle = bobRepo.find<TestDoc>(
|
|
1117
|
+
const bobHandle = await bobRepo.find<TestDoc>(
|
|
1103
1118
|
stringifyAutomergeUrl({ documentId: notForCharlie })
|
|
1104
1119
|
)
|
|
1105
1120
|
|
|
@@ -1142,7 +1157,10 @@ describe("Repo", () => {
|
|
|
1142
1157
|
bobHandle.documentId,
|
|
1143
1158
|
await charlieRepo!.storageSubsystem.id()
|
|
1144
1159
|
)
|
|
1145
|
-
assert.deepStrictEqual(
|
|
1160
|
+
assert.deepStrictEqual(
|
|
1161
|
+
encodeHeads(storedSyncState.sharedHeads),
|
|
1162
|
+
bobHandle.heads()
|
|
1163
|
+
)
|
|
1146
1164
|
|
|
1147
1165
|
teardown()
|
|
1148
1166
|
})
|
|
@@ -1242,15 +1260,14 @@ describe("Repo", () => {
|
|
|
1242
1260
|
|
|
1243
1261
|
const nextRemoteHeadsPromise = new Promise<{
|
|
1244
1262
|
storageId: StorageId
|
|
1245
|
-
heads:
|
|
1263
|
+
heads: UrlHeads
|
|
1246
1264
|
}>(resolve => {
|
|
1247
1265
|
handle.on("remote-heads", ({ storageId, heads }) => {
|
|
1248
1266
|
resolve({ storageId, heads })
|
|
1249
1267
|
})
|
|
1250
1268
|
})
|
|
1251
1269
|
|
|
1252
|
-
const charlieHandle = charlieRepo.find<TestDoc>(handle.url)
|
|
1253
|
-
await charlieHandle.whenReady()
|
|
1270
|
+
const charlieHandle = await charlieRepo.find<TestDoc>(handle.url)
|
|
1254
1271
|
|
|
1255
1272
|
// make a change on charlie
|
|
1256
1273
|
charlieHandle.change(d => {
|
|
@@ -1287,34 +1304,6 @@ describe("Repo", () => {
|
|
|
1287
1304
|
})
|
|
1288
1305
|
})
|
|
1289
1306
|
|
|
1290
|
-
it("peer receives a document when connection is recovered", async () => {
|
|
1291
|
-
const alice = "alice" as PeerId
|
|
1292
|
-
const bob = "bob" as PeerId
|
|
1293
|
-
const [aliceAdapter, bobAdapter] = DummyNetworkAdapter.createConnectedPair()
|
|
1294
|
-
const aliceRepo = new Repo({
|
|
1295
|
-
network: [aliceAdapter],
|
|
1296
|
-
peerId: alice,
|
|
1297
|
-
})
|
|
1298
|
-
const bobRepo = new Repo({
|
|
1299
|
-
network: [bobAdapter],
|
|
1300
|
-
peerId: bob,
|
|
1301
|
-
})
|
|
1302
|
-
const aliceDoc = aliceRepo.create()
|
|
1303
|
-
aliceDoc.change((doc: any) => (doc.text = "Hello world"))
|
|
1304
|
-
|
|
1305
|
-
const bobDoc = bobRepo.find(aliceDoc.url)
|
|
1306
|
-
await eventPromise(bobDoc, "unavailable")
|
|
1307
|
-
|
|
1308
|
-
aliceAdapter.peerCandidate(bob)
|
|
1309
|
-
// Bob isn't yet connected to Alice and can't respond to her sync message
|
|
1310
|
-
await pause(100)
|
|
1311
|
-
bobAdapter.peerCandidate(alice)
|
|
1312
|
-
|
|
1313
|
-
await bobDoc.whenReady()
|
|
1314
|
-
|
|
1315
|
-
assert.equal(bobDoc.isReady(), true)
|
|
1316
|
-
})
|
|
1317
|
-
|
|
1318
1307
|
describe("with peers (mesh network)", () => {
|
|
1319
1308
|
const setup = async () => {
|
|
1320
1309
|
// Set up three repos; connect Alice to Bob, Bob to Charlie, and Alice to Charlie
|
|
@@ -1376,8 +1365,8 @@ describe("Repo", () => {
|
|
|
1376
1365
|
|
|
1377
1366
|
const aliceHandle = aliceRepo.create<TestDoc>()
|
|
1378
1367
|
|
|
1379
|
-
const bobHandle = bobRepo.find(aliceHandle.url)
|
|
1380
|
-
const charlieHandle = charlieRepo.find(aliceHandle.url)
|
|
1368
|
+
const bobHandle = await bobRepo.find(aliceHandle.url)
|
|
1369
|
+
const charlieHandle = await charlieRepo.find(aliceHandle.url)
|
|
1381
1370
|
|
|
1382
1371
|
// Alice should not receive her own ephemeral message
|
|
1383
1372
|
aliceHandle.on("ephemeral-message", () => {
|
|
@@ -1415,9 +1404,8 @@ describe("Repo", () => {
|
|
|
1415
1404
|
// pause to let the sync happen
|
|
1416
1405
|
await pause(50)
|
|
1417
1406
|
|
|
1418
|
-
const charlieHandle = charlieRepo.find(handle2.url)
|
|
1419
|
-
|
|
1420
|
-
assert.deepStrictEqual(charlieHandle.docSync(), { foo: "bar" })
|
|
1407
|
+
const charlieHandle = await charlieRepo.find(handle2.url)
|
|
1408
|
+
assert.deepStrictEqual(charlieHandle.doc(), { foo: "bar" })
|
|
1421
1409
|
|
|
1422
1410
|
teardown()
|
|
1423
1411
|
})
|
|
@@ -1434,9 +1422,8 @@ describe("Repo", () => {
|
|
|
1434
1422
|
// pause to let the sync happen
|
|
1435
1423
|
await pause(50)
|
|
1436
1424
|
|
|
1437
|
-
const charlieHandle = charlieRepo.find(handle2.url)
|
|
1438
|
-
|
|
1439
|
-
assert.deepStrictEqual(charlieHandle.docSync(), { foo: "bar" })
|
|
1425
|
+
const charlieHandle = await charlieRepo.find(handle2.url)
|
|
1426
|
+
assert.deepStrictEqual(charlieHandle.doc(), { foo: "bar" })
|
|
1440
1427
|
|
|
1441
1428
|
// now make a change to doc2 on bobs side and merge it into doc1
|
|
1442
1429
|
handle2.change(d => {
|
|
@@ -1447,12 +1434,198 @@ describe("Repo", () => {
|
|
|
1447
1434
|
// wait for the network to do it's thang
|
|
1448
1435
|
await pause(350)
|
|
1449
1436
|
|
|
1450
|
-
|
|
1451
|
-
assert.deepStrictEqual(charlieHandle.docSync(), { foo: "baz" })
|
|
1437
|
+
assert.deepStrictEqual(charlieHandle.doc(), { foo: "baz" })
|
|
1452
1438
|
|
|
1453
1439
|
teardown()
|
|
1454
1440
|
})
|
|
1455
1441
|
})
|
|
1442
|
+
|
|
1443
|
+
describe("the denylist", () => {
|
|
1444
|
+
it("should immediately return an unavailable message in response to a request for a denylisted document", async () => {
|
|
1445
|
+
const storage = new DummyStorageAdapter()
|
|
1446
|
+
|
|
1447
|
+
// first create the document in storage
|
|
1448
|
+
const dummyRepo = new Repo({ network: [], storage })
|
|
1449
|
+
const doc = dummyRepo.create({ foo: "bar" })
|
|
1450
|
+
await dummyRepo.flush()
|
|
1451
|
+
|
|
1452
|
+
// Check that the document actually is in storage
|
|
1453
|
+
let docId = doc.documentId
|
|
1454
|
+
assert(storage.keys().some((k: string) => k.includes(docId)))
|
|
1455
|
+
|
|
1456
|
+
const channel = new MessageChannel()
|
|
1457
|
+
const { port1: clientToServer, port2: serverToClient } = channel
|
|
1458
|
+
const server = new Repo({
|
|
1459
|
+
network: [new MessageChannelNetworkAdapter(serverToClient)],
|
|
1460
|
+
storage,
|
|
1461
|
+
denylist: [doc.url],
|
|
1462
|
+
})
|
|
1463
|
+
const client = new Repo({
|
|
1464
|
+
network: [new MessageChannelNetworkAdapter(clientToServer)],
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
await Promise.all([
|
|
1468
|
+
eventPromise(server.networkSubsystem, "peer"),
|
|
1469
|
+
eventPromise(client.networkSubsystem, "peer"),
|
|
1470
|
+
])
|
|
1471
|
+
|
|
1472
|
+
await expect(async () => {
|
|
1473
|
+
const clientDoc = await client.find(doc.url)
|
|
1474
|
+
}).rejects.toThrow(/Document (.*) is unavailable/)
|
|
1475
|
+
|
|
1476
|
+
const openDocs = Object.keys(server.metrics().documents).length
|
|
1477
|
+
assert.deepEqual(openDocs, 0)
|
|
1478
|
+
})
|
|
1479
|
+
})
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
describe("Repo heads-in-URLs functionality", () => {
|
|
1483
|
+
const setup = () => {
|
|
1484
|
+
const repo = new Repo({})
|
|
1485
|
+
const handle = repo.create()
|
|
1486
|
+
handle.change((doc: any) => (doc.title = "Hello World"))
|
|
1487
|
+
return { repo, handle }
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
it("finds a document view by URL with heads", async () => {
|
|
1491
|
+
const { repo, handle } = setup()
|
|
1492
|
+
const heads = handle.heads()!
|
|
1493
|
+
const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
|
|
1494
|
+
const view = await repo.find(url)
|
|
1495
|
+
expect(view.doc()).toEqual({ title: "Hello World" })
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
it("returns a view, not the actual handle, when finding by URL with heads", async () => {
|
|
1499
|
+
const { repo, handle } = setup()
|
|
1500
|
+
const heads = handle.heads()!
|
|
1501
|
+
await handle.change((doc: any) => (doc.title = "Changed"))
|
|
1502
|
+
const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
|
|
1503
|
+
const view = await repo.find(url)
|
|
1504
|
+
expect(view.doc()).toEqual({ title: "Hello World" })
|
|
1505
|
+
expect(handle.doc()).toEqual({ title: "Changed" })
|
|
1506
|
+
})
|
|
1507
|
+
|
|
1508
|
+
it("changes to a document view do not affect the original", async () => {
|
|
1509
|
+
const { repo, handle } = setup()
|
|
1510
|
+
const heads = handle.heads()!
|
|
1511
|
+
const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
|
|
1512
|
+
const view = await repo.find(url)
|
|
1513
|
+
expect(() =>
|
|
1514
|
+
view.change((doc: any) => (doc.title = "Changed in View"))
|
|
1515
|
+
).toThrow()
|
|
1516
|
+
expect(handle.doc()).toEqual({ title: "Hello World" })
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
it("document views are read-only", async () => {
|
|
1520
|
+
const { repo, handle } = setup()
|
|
1521
|
+
const heads = handle.heads()!
|
|
1522
|
+
const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
|
|
1523
|
+
const view = await repo.find(url)
|
|
1524
|
+
expect(() => view.change((doc: any) => (doc.title = "Changed"))).toThrow()
|
|
1525
|
+
})
|
|
1526
|
+
|
|
1527
|
+
it("finds the latest document when given a URL without heads", async () => {
|
|
1528
|
+
const { repo, handle } = setup()
|
|
1529
|
+
await handle.change((doc: any) => (doc.title = "Changed"))
|
|
1530
|
+
const found = await repo.find(handle.url)
|
|
1531
|
+
expect(found.doc()).toEqual({ title: "Changed" })
|
|
1532
|
+
})
|
|
1533
|
+
|
|
1534
|
+
it("getHeadsFromUrl returns heads array if present or undefined", () => {
|
|
1535
|
+
const { repo, handle } = setup()
|
|
1536
|
+
const heads = handle.heads()!
|
|
1537
|
+
const url = stringifyAutomergeUrl({ documentId: handle.documentId, heads })
|
|
1538
|
+
expect(getHeadsFromUrl(url)).toEqual(heads)
|
|
1539
|
+
|
|
1540
|
+
const urlWithoutHeads = generateAutomergeUrl()
|
|
1541
|
+
expect(getHeadsFromUrl(urlWithoutHeads)).toBeUndefined()
|
|
1542
|
+
})
|
|
1543
|
+
|
|
1544
|
+
it("isValidAutomergeUrl returns true for valid URLs", () => {
|
|
1545
|
+
const { repo, handle } = setup()
|
|
1546
|
+
const url = generateAutomergeUrl()
|
|
1547
|
+
expect(isValidAutomergeUrl(url)).toBe(true)
|
|
1548
|
+
|
|
1549
|
+
const urlWithHeads = stringifyAutomergeUrl({
|
|
1550
|
+
documentId: handle.documentId,
|
|
1551
|
+
heads: handle.heads()!,
|
|
1552
|
+
})
|
|
1553
|
+
expect(isValidAutomergeUrl(urlWithHeads)).toBe(true)
|
|
1554
|
+
})
|
|
1555
|
+
|
|
1556
|
+
it("isValidAutomergeUrl returns false for invalid URLs", () => {
|
|
1557
|
+
const { repo, handle } = setup()
|
|
1558
|
+
expect(isValidAutomergeUrl("not a url")).toBe(false)
|
|
1559
|
+
expect(isValidAutomergeUrl("automerge:invalidid")).toBe(false)
|
|
1560
|
+
expect(isValidAutomergeUrl("automerge:validid#invalidhead")).toBe(false)
|
|
1561
|
+
})
|
|
1562
|
+
|
|
1563
|
+
it("parseAutomergeUrl extracts documentId and heads", () => {
|
|
1564
|
+
const { repo, handle } = setup()
|
|
1565
|
+
const url = stringifyAutomergeUrl({
|
|
1566
|
+
documentId: handle.documentId,
|
|
1567
|
+
heads: handle.heads()!,
|
|
1568
|
+
})
|
|
1569
|
+
const parsed = parseAutomergeUrl(url)
|
|
1570
|
+
expect(parsed.documentId).toBe(handle.documentId)
|
|
1571
|
+
expect(parsed.heads).toEqual(handle.heads())
|
|
1572
|
+
})
|
|
1573
|
+
|
|
1574
|
+
it("stringifyAutomergeUrl creates valid URL", () => {
|
|
1575
|
+
const { repo, handle } = setup()
|
|
1576
|
+
const url = stringifyAutomergeUrl({
|
|
1577
|
+
documentId: handle.documentId,
|
|
1578
|
+
heads: handle.heads()!,
|
|
1579
|
+
})
|
|
1580
|
+
expect(isValidAutomergeUrl(url)).toBe(true)
|
|
1581
|
+
const parsed = parseAutomergeUrl(url)
|
|
1582
|
+
expect(parsed.documentId).toBe(handle.documentId)
|
|
1583
|
+
expect(parsed.heads).toEqual(handle.heads())
|
|
1584
|
+
})
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
describe("Repo.find() abort behavior", () => {
|
|
1588
|
+
it("aborts immediately if signal is already aborted", async () => {
|
|
1589
|
+
const repo = new Repo()
|
|
1590
|
+
const controller = new AbortController()
|
|
1591
|
+
controller.abort()
|
|
1592
|
+
|
|
1593
|
+
await expect(
|
|
1594
|
+
repo.find(generateAutomergeUrl(), { signal: controller.signal })
|
|
1595
|
+
).rejects.toThrow("Operation aborted")
|
|
1596
|
+
})
|
|
1597
|
+
|
|
1598
|
+
it("can abort while waiting for ready state", async () => {
|
|
1599
|
+
// Create a repo with no network adapters so document can't become ready
|
|
1600
|
+
const repo = new Repo()
|
|
1601
|
+
const url = generateAutomergeUrl()
|
|
1602
|
+
|
|
1603
|
+
const controller = new AbortController()
|
|
1604
|
+
|
|
1605
|
+
// Start find and abort after a moment
|
|
1606
|
+
const findPromise = repo.find(url, { signal: controller.signal })
|
|
1607
|
+
controller.abort()
|
|
1608
|
+
|
|
1609
|
+
await expect(findPromise).rejects.toThrow("Operation aborted")
|
|
1610
|
+
await expect(findPromise).rejects.not.toThrow("unavailable")
|
|
1611
|
+
})
|
|
1612
|
+
|
|
1613
|
+
it("returns handle immediately when allow unavailable is true, even with abort signal", async () => {
|
|
1614
|
+
const repo = new Repo()
|
|
1615
|
+
const controller = new AbortController()
|
|
1616
|
+
const url = generateAutomergeUrl()
|
|
1617
|
+
|
|
1618
|
+
const handle = await repo.find(url, {
|
|
1619
|
+
allowableStates: ["unavailable"],
|
|
1620
|
+
signal: controller.signal,
|
|
1621
|
+
})
|
|
1622
|
+
|
|
1623
|
+
expect(handle).toBeDefined()
|
|
1624
|
+
|
|
1625
|
+
// Abort shouldn't affect the result since we skipped ready
|
|
1626
|
+
controller.abort()
|
|
1627
|
+
expect(handle.url).toBe(url)
|
|
1628
|
+
})
|
|
1456
1629
|
})
|
|
1457
1630
|
|
|
1458
1631
|
const warn = console.warn
|