@automerge/automerge-repo 2.0.0-alpha.20 → 2.0.0-alpha.23
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/DocHandle.d.ts +10 -22
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +21 -51
- package/dist/FindProgress.d.ts +30 -0
- package/dist/FindProgress.d.ts.map +1 -0
- package/dist/FindProgress.js +1 -0
- package/dist/Repo.d.ts +9 -4
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +166 -69
- 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/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +13 -13
- package/dist/synchronizer/CollectionSynchronizer.d.ts +2 -1
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +18 -14
- package/dist/synchronizer/DocSynchronizer.d.ts +3 -2
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +43 -27
- package/fuzz/fuzz.ts +3 -3
- package/package.json +3 -4
- package/src/DocHandle.ts +23 -51
- package/src/FindProgress.ts +48 -0
- package/src/Repo.ts +187 -67
- package/src/helpers/abortable.ts +61 -0
- package/src/helpers/tests/network-adapter-tests.ts +14 -13
- package/src/synchronizer/CollectionSynchronizer.ts +18 -14
- package/src/synchronizer/DocSynchronizer.ts +51 -32
- package/test/CollectionSynchronizer.test.ts +4 -4
- package/test/DocHandle.test.ts +25 -74
- package/test/Repo.test.ts +169 -216
- package/test/remoteHeads.test.ts +27 -12
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a promise that rejects when the signal is aborted.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This utility creates a promise that rejects when the provided AbortSignal is aborted.
|
|
6
|
+
* It's designed to be used with Promise.race() to make operations abortable.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const controller = new AbortController();
|
|
11
|
+
*
|
|
12
|
+
* try {
|
|
13
|
+
* const result = await Promise.race([
|
|
14
|
+
* fetch('https://api.example.com/data'),
|
|
15
|
+
* abortable(controller.signal)
|
|
16
|
+
* ]);
|
|
17
|
+
* } catch (err) {
|
|
18
|
+
* if (err.name === 'AbortError') {
|
|
19
|
+
* console.log('The operation was aborted');
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* // Later, to abort:
|
|
24
|
+
* controller.abort();
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param signal - An AbortSignal that can be used to abort the operation
|
|
28
|
+
* @param cleanup - Optional cleanup function that will be called if aborted
|
|
29
|
+
* @returns A promise that rejects with AbortError when the signal is aborted
|
|
30
|
+
* @throws {DOMException} With name "AbortError" when aborted
|
|
31
|
+
*/
|
|
32
|
+
export function abortable(
|
|
33
|
+
signal?: AbortSignal,
|
|
34
|
+
cleanup?: () => void
|
|
35
|
+
): Promise<never> {
|
|
36
|
+
if (signal?.aborted) {
|
|
37
|
+
throw new DOMException("Operation aborted", "AbortError")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!signal) {
|
|
41
|
+
return new Promise(() => {}) // Never resolves
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new Promise((_, reject) => {
|
|
45
|
+
signal.addEventListener(
|
|
46
|
+
"abort",
|
|
47
|
+
() => {
|
|
48
|
+
cleanup?.()
|
|
49
|
+
reject(new DOMException("Operation aborted", "AbortError"))
|
|
50
|
+
},
|
|
51
|
+
{ once: true }
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Include this type in an options object to pass an AbortSignal to a function.
|
|
58
|
+
*/
|
|
59
|
+
export interface AbortOptions {
|
|
60
|
+
signal?: AbortSignal
|
|
61
|
+
}
|
|
@@ -49,9 +49,10 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
49
49
|
// Alice creates a document
|
|
50
50
|
const aliceHandle = aliceRepo.create<TestDoc>()
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
await
|
|
54
|
-
|
|
52
|
+
// TODO: ... let connections complete. this shouldn't be necessary.
|
|
53
|
+
await pause(50)
|
|
54
|
+
|
|
55
|
+
const bobHandle = await bobRepo.find<TestDoc>(aliceHandle.url)
|
|
55
56
|
|
|
56
57
|
// Alice changes the document
|
|
57
58
|
aliceHandle.change(d => {
|
|
@@ -60,7 +61,7 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
60
61
|
|
|
61
62
|
// Bob receives the change
|
|
62
63
|
await eventPromise(bobHandle, "change")
|
|
63
|
-
assert.equal((await bobHandle.doc()
|
|
64
|
+
assert.equal((await bobHandle).doc()?.foo, "bar")
|
|
64
65
|
|
|
65
66
|
// Bob changes the document
|
|
66
67
|
bobHandle.change(d => {
|
|
@@ -69,7 +70,7 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
69
70
|
|
|
70
71
|
// Alice receives the change
|
|
71
72
|
await eventPromise(aliceHandle, "change")
|
|
72
|
-
assert.equal(
|
|
73
|
+
assert.equal(aliceHandle.doc().foo, "baz")
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// Run the test in both directions, in case they're different types of adapters
|
|
@@ -100,9 +101,9 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
100
101
|
const docUrl = aliceHandle.url
|
|
101
102
|
|
|
102
103
|
// Bob and Charlie receive the document
|
|
103
|
-
await
|
|
104
|
-
const bobHandle = bobRepo.find<TestDoc>(docUrl)
|
|
105
|
-
const charlieHandle = charlieRepo.find<TestDoc>(docUrl)
|
|
104
|
+
await pause(50)
|
|
105
|
+
const bobHandle = await bobRepo.find<TestDoc>(docUrl)
|
|
106
|
+
const charlieHandle = await charlieRepo.find<TestDoc>(docUrl)
|
|
106
107
|
|
|
107
108
|
// Alice changes the document
|
|
108
109
|
aliceHandle.change(d => {
|
|
@@ -111,8 +112,8 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
111
112
|
|
|
112
113
|
// Bob and Charlie receive the change
|
|
113
114
|
await eventPromises([bobHandle, charlieHandle], "change")
|
|
114
|
-
assert.equal(
|
|
115
|
-
assert.equal(
|
|
115
|
+
assert.equal(bobHandle.doc().foo, "bar")
|
|
116
|
+
assert.equal(charlieHandle.doc().foo, "bar")
|
|
116
117
|
|
|
117
118
|
// Charlie changes the document
|
|
118
119
|
charlieHandle.change(d => {
|
|
@@ -121,8 +122,8 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
121
122
|
|
|
122
123
|
// Alice and Bob receive the change
|
|
123
124
|
await eventPromises([aliceHandle, bobHandle], "change")
|
|
124
|
-
assert.equal(
|
|
125
|
-
assert.equal(
|
|
125
|
+
assert.equal(bobHandle.doc().foo, "baz")
|
|
126
|
+
assert.equal(charlieHandle.doc().foo, "baz")
|
|
126
127
|
|
|
127
128
|
teardown()
|
|
128
129
|
})
|
|
@@ -141,7 +142,7 @@ export function runNetworkAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
141
142
|
)
|
|
142
143
|
|
|
143
144
|
const aliceHandle = aliceRepo.create<TestDoc>()
|
|
144
|
-
const charlieHandle = charlieRepo.find(aliceHandle.url)
|
|
145
|
+
const charlieHandle = await charlieRepo.find(aliceHandle.url)
|
|
145
146
|
|
|
146
147
|
// pause to give charlie a chance to let alice know it wants the doc
|
|
147
148
|
await pause(100)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import debug from "debug"
|
|
2
2
|
import { DocHandle } from "../DocHandle.js"
|
|
3
|
-
import { parseAutomergeUrl
|
|
3
|
+
import { parseAutomergeUrl } from "../AutomergeUrl.js"
|
|
4
4
|
import { Repo } from "../Repo.js"
|
|
5
5
|
import { DocMessage } from "../network/messages.js"
|
|
6
6
|
import { AutomergeUrl, DocumentId, PeerId } from "../types.js"
|
|
@@ -29,18 +29,19 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
|
|
32
|
-
#fetchDocSynchronizer(
|
|
33
|
-
if (!this.docSynchronizers[documentId]) {
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
#fetchDocSynchronizer(handle: DocHandle<unknown>) {
|
|
33
|
+
if (!this.docSynchronizers[handle.documentId]) {
|
|
34
|
+
this.docSynchronizers[handle.documentId] =
|
|
35
|
+
this.#initDocSynchronizer(handle)
|
|
36
36
|
}
|
|
37
|
-
return this.docSynchronizers[documentId]
|
|
37
|
+
return this.docSynchronizers[handle.documentId]
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Creates a new docSynchronizer and sets it up to propagate messages */
|
|
41
41
|
#initDocSynchronizer(handle: DocHandle<unknown>): DocSynchronizer {
|
|
42
42
|
const docSynchronizer = new DocSynchronizer({
|
|
43
43
|
handle,
|
|
44
|
+
peerId: this.repo.networkSubsystem.peerId,
|
|
44
45
|
onLoadSyncState: async peerId => {
|
|
45
46
|
if (!this.repo.storageSubsystem) {
|
|
46
47
|
return
|
|
@@ -109,13 +110,16 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
109
110
|
|
|
110
111
|
this.#docSetUp[documentId] = true
|
|
111
112
|
|
|
112
|
-
const
|
|
113
|
+
const handle = await this.repo.find(documentId, {
|
|
114
|
+
allowableStates: ["ready", "unavailable", "requesting"],
|
|
115
|
+
})
|
|
116
|
+
const docSynchronizer = this.#fetchDocSynchronizer(handle)
|
|
113
117
|
|
|
114
118
|
docSynchronizer.receiveMessage(message)
|
|
115
119
|
|
|
116
120
|
// Initiate sync with any new peers
|
|
117
121
|
const peers = await this.#documentGenerousPeers(documentId)
|
|
118
|
-
docSynchronizer.beginSync(
|
|
122
|
+
void docSynchronizer.beginSync(
|
|
119
123
|
peers.filter(peerId => !docSynchronizer.hasPeer(peerId))
|
|
120
124
|
)
|
|
121
125
|
}
|
|
@@ -123,14 +127,14 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
123
127
|
/**
|
|
124
128
|
* Starts synchronizing the given document with all peers that we share it generously with.
|
|
125
129
|
*/
|
|
126
|
-
addDocument(
|
|
130
|
+
addDocument(handle: DocHandle<unknown>) {
|
|
127
131
|
// HACK: this is a hack to prevent us from adding the same document twice
|
|
128
|
-
if (this.#docSetUp[documentId]) {
|
|
132
|
+
if (this.#docSetUp[handle.documentId]) {
|
|
129
133
|
return
|
|
130
134
|
}
|
|
131
|
-
const docSynchronizer = this.#fetchDocSynchronizer(
|
|
132
|
-
void this.#documentGenerousPeers(documentId).then(peers => {
|
|
133
|
-
docSynchronizer.beginSync(peers)
|
|
135
|
+
const docSynchronizer = this.#fetchDocSynchronizer(handle)
|
|
136
|
+
void this.#documentGenerousPeers(handle.documentId).then(peers => {
|
|
137
|
+
void docSynchronizer.beginSync(peers)
|
|
134
138
|
})
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -152,7 +156,7 @@ export class CollectionSynchronizer extends Synchronizer {
|
|
|
152
156
|
for (const docSynchronizer of Object.values(this.docSynchronizers)) {
|
|
153
157
|
const { documentId } = docSynchronizer
|
|
154
158
|
void this.repo.sharePolicy(peerId, documentId).then(okToShare => {
|
|
155
|
-
if (okToShare) docSynchronizer.beginSync([peerId])
|
|
159
|
+
if (okToShare) void docSynchronizer.beginSync([peerId])
|
|
156
160
|
})
|
|
157
161
|
}
|
|
158
162
|
}
|
|
@@ -30,6 +30,7 @@ type PendingMessage = {
|
|
|
30
30
|
|
|
31
31
|
interface DocSynchronizerConfig {
|
|
32
32
|
handle: DocHandle<unknown>
|
|
33
|
+
peerId: PeerId
|
|
33
34
|
onLoadSyncState?: (peerId: PeerId) => Promise<A.SyncState | undefined>
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -56,13 +57,17 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
56
57
|
|
|
57
58
|
#pendingSyncMessages: Array<PendingMessage> = []
|
|
58
59
|
|
|
60
|
+
// We keep this around at least in part for debugging.
|
|
61
|
+
// eslint-disable-next-line no-unused-private-class-members
|
|
62
|
+
#peerId: PeerId
|
|
59
63
|
#syncStarted = false
|
|
60
64
|
|
|
61
65
|
#handle: DocHandle<unknown>
|
|
62
66
|
#onLoadSyncState: (peerId: PeerId) => Promise<A.SyncState | undefined>
|
|
63
67
|
|
|
64
|
-
constructor({ handle, onLoadSyncState }: DocSynchronizerConfig) {
|
|
68
|
+
constructor({ handle, peerId, onLoadSyncState }: DocSynchronizerConfig) {
|
|
65
69
|
super()
|
|
70
|
+
this.#peerId = peerId
|
|
66
71
|
this.#handle = handle
|
|
67
72
|
this.#onLoadSyncState =
|
|
68
73
|
onLoadSyncState ?? (() => Promise.resolve(undefined))
|
|
@@ -81,7 +86,6 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
81
86
|
|
|
82
87
|
// Process pending sync messages immediately after the handle becomes ready.
|
|
83
88
|
void (async () => {
|
|
84
|
-
await handle.doc([READY, REQUESTING])
|
|
85
89
|
this.#processAllPendingSyncMessages()
|
|
86
90
|
})()
|
|
87
91
|
}
|
|
@@ -97,10 +101,13 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
97
101
|
/// PRIVATE
|
|
98
102
|
|
|
99
103
|
async #syncWithPeers() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
try {
|
|
105
|
+
await this.#handle.whenReady()
|
|
106
|
+
const doc = this.#handle.doc() // XXX THIS ONE IS WEIRD
|
|
107
|
+
this.#peers.forEach(peerId => this.#sendSyncMessage(peerId, doc))
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.log("sync with peers threw an exception")
|
|
110
|
+
}
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
async #broadcastToPeers({
|
|
@@ -226,32 +233,26 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
226
233
|
return this.#peers.includes(peerId)
|
|
227
234
|
}
|
|
228
235
|
|
|
229
|
-
beginSync(peerIds: PeerId[]) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
.doc([READY, REQUESTING, UNAVAILABLE])
|
|
239
|
-
.then(doc => {
|
|
240
|
-
// we register out peers first, then say that sync has started
|
|
236
|
+
async beginSync(peerIds: PeerId[]) {
|
|
237
|
+
void this.#handle
|
|
238
|
+
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
239
|
+
.then(() => {
|
|
240
|
+
this.#syncStarted = true
|
|
241
|
+
this.#checkDocUnavailable()
|
|
242
|
+
})
|
|
243
|
+
.catch(e => {
|
|
244
|
+
console.log("caught whenready", e)
|
|
241
245
|
this.#syncStarted = true
|
|
242
246
|
this.#checkDocUnavailable()
|
|
243
|
-
|
|
244
|
-
const wasUnavailable = doc === undefined
|
|
245
|
-
if (wasUnavailable && noPeersWithDocument) {
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// If the doc is unavailable we still need a blank document to generate
|
|
250
|
-
// the sync message from
|
|
251
|
-
return doc ?? A.init<unknown>()
|
|
252
247
|
})
|
|
253
248
|
|
|
254
|
-
this.#
|
|
249
|
+
const peersWithDocument = this.#peers.some(peerId => {
|
|
250
|
+
return this.#peerDocumentStatuses[peerId] == "has"
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (peersWithDocument) {
|
|
254
|
+
await this.#handle.whenReady()
|
|
255
|
+
}
|
|
255
256
|
|
|
256
257
|
peerIds.forEach(peerId => {
|
|
257
258
|
this.#withSyncState(peerId, syncState => {
|
|
@@ -264,11 +265,28 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
264
265
|
)
|
|
265
266
|
this.#setSyncState(peerId, reparsedSyncState)
|
|
266
267
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
// At this point if we don't have anything in our storage, we need to use an empty doc to sync
|
|
269
|
+
// with; but we don't want to surface that state to the front end
|
|
270
|
+
this.#handle
|
|
271
|
+
.whenReady([READY, REQUESTING, UNAVAILABLE])
|
|
272
|
+
.then(() => {
|
|
273
|
+
const doc = this.#handle.isReady()
|
|
274
|
+
? this.#handle.doc()
|
|
275
|
+
: A.init<unknown>()
|
|
276
|
+
|
|
277
|
+
const noPeersWithDocument = peerIds.every(
|
|
278
|
+
peerId =>
|
|
279
|
+
this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const wasUnavailable = doc === undefined
|
|
283
|
+
if (wasUnavailable && noPeersWithDocument) {
|
|
284
|
+
return
|
|
271
285
|
}
|
|
286
|
+
|
|
287
|
+
// If the doc is unavailable we still need a blank document to generate
|
|
288
|
+
// the sync message from
|
|
289
|
+
this.#sendSyncMessage(peerId, doc ?? A.init<unknown>())
|
|
272
290
|
})
|
|
273
291
|
.catch(err => {
|
|
274
292
|
this.#log(`Error loading doc for ${peerId}: ${err}`)
|
|
@@ -352,6 +370,7 @@ export class DocSynchronizer extends Synchronizer {
|
|
|
352
370
|
this.#withSyncState(message.senderId, syncState => {
|
|
353
371
|
this.#handle.update(doc => {
|
|
354
372
|
const start = performance.now()
|
|
373
|
+
|
|
355
374
|
const [newDoc, newSyncState] = A.receiveSyncMessage(
|
|
356
375
|
doc,
|
|
357
376
|
syncState,
|
|
@@ -28,13 +28,13 @@ describe("CollectionSynchronizer", () => {
|
|
|
28
28
|
done()
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
synchronizer.addDocument(handle
|
|
31
|
+
synchronizer.addDocument(handle)
|
|
32
32
|
}))
|
|
33
33
|
|
|
34
34
|
it("starts synchronizing existing documents when a peer is added", () =>
|
|
35
35
|
new Promise<void>(done => {
|
|
36
36
|
const handle = repo.create()
|
|
37
|
-
synchronizer.addDocument(handle
|
|
37
|
+
synchronizer.addDocument(handle)
|
|
38
38
|
synchronizer.once("message", event => {
|
|
39
39
|
const { targetId, documentId } = event as SyncMessage
|
|
40
40
|
assert(targetId === "peer1")
|
|
@@ -50,7 +50,7 @@ describe("CollectionSynchronizer", () => {
|
|
|
50
50
|
|
|
51
51
|
repo.sharePolicy = async (peerId: PeerId) => peerId !== "peer1"
|
|
52
52
|
|
|
53
|
-
synchronizer.addDocument(handle
|
|
53
|
+
synchronizer.addDocument(handle)
|
|
54
54
|
synchronizer.once("message", () => {
|
|
55
55
|
reject(new Error("Should not have sent a message"))
|
|
56
56
|
})
|
|
@@ -71,7 +71,7 @@ describe("CollectionSynchronizer", () => {
|
|
|
71
71
|
reject(new Error("Should not have sent a message"))
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
synchronizer.addDocument(handle
|
|
74
|
+
synchronizer.addDocument(handle)
|
|
75
75
|
|
|
76
76
|
setTimeout(done)
|
|
77
77
|
}))
|
package/test/DocHandle.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as A from "@automerge/automerge/next"
|
|
2
2
|
import assert from "assert"
|
|
3
3
|
import { decode } from "cbor-x"
|
|
4
|
-
import { describe, it, vi } from "vitest"
|
|
4
|
+
import { describe, expect, it, vi } from "vitest"
|
|
5
5
|
import {
|
|
6
6
|
encodeHeads,
|
|
7
7
|
generateAutomergeUrl,
|
|
@@ -11,7 +11,6 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
|
|
|
11
11
|
import { pause } from "../src/helpers/pause.js"
|
|
12
12
|
import { DocHandle, DocHandleChangePayload } from "../src/index.js"
|
|
13
13
|
import { TestDoc } from "./types.js"
|
|
14
|
-
import { UNLOADED } from "../src/DocHandle.js"
|
|
15
14
|
|
|
16
15
|
describe("DocHandle", () => {
|
|
17
16
|
const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
|
|
@@ -39,7 +38,7 @@ describe("DocHandle", () => {
|
|
|
39
38
|
handle.update(doc => docFromMockStorage(doc))
|
|
40
39
|
|
|
41
40
|
assert.equal(handle.isReady(), true)
|
|
42
|
-
const doc =
|
|
41
|
+
const doc = handle.doc()
|
|
43
42
|
assert.equal(doc?.foo, "bar")
|
|
44
43
|
})
|
|
45
44
|
|
|
@@ -51,13 +50,13 @@ describe("DocHandle", () => {
|
|
|
51
50
|
handle.update(doc => docFromMockStorage(doc))
|
|
52
51
|
|
|
53
52
|
assert.equal(handle.isReady(), true)
|
|
54
|
-
const doc =
|
|
55
|
-
assert.deepEqual(doc, handle.
|
|
53
|
+
const doc = handle.doc()
|
|
54
|
+
assert.deepEqual(doc, handle.doc())
|
|
56
55
|
})
|
|
57
56
|
|
|
58
|
-
it("should
|
|
57
|
+
it("should throw an exception if we access the doc before ready", async () => {
|
|
59
58
|
const handle = new DocHandle<TestDoc>(TEST_ID)
|
|
60
|
-
assert.
|
|
59
|
+
assert.throws(() => handle.doc())
|
|
61
60
|
})
|
|
62
61
|
|
|
63
62
|
it("should not return a doc until ready", async () => {
|
|
@@ -67,7 +66,7 @@ describe("DocHandle", () => {
|
|
|
67
66
|
// simulate loading from storage
|
|
68
67
|
handle.update(doc => docFromMockStorage(doc))
|
|
69
68
|
|
|
70
|
-
const doc =
|
|
69
|
+
const doc = handle.doc()
|
|
71
70
|
|
|
72
71
|
assert.equal(handle.isReady(), true)
|
|
73
72
|
assert.equal(doc?.foo, "bar")
|
|
@@ -87,15 +86,15 @@ describe("DocHandle", () => {
|
|
|
87
86
|
handle.change(d => (d.foo = "bar"))
|
|
88
87
|
assert.equal(handle.isReady(), true)
|
|
89
88
|
|
|
90
|
-
const heads = encodeHeads(A.getHeads(handle.
|
|
89
|
+
const heads = encodeHeads(A.getHeads(handle.doc()))
|
|
91
90
|
assert.notDeepEqual(handle.heads(), [])
|
|
92
91
|
assert.deepEqual(heads, handle.heads())
|
|
93
92
|
})
|
|
94
93
|
|
|
95
|
-
it("should
|
|
94
|
+
it("should throw an if the heads aren't loaded", async () => {
|
|
96
95
|
const handle = new DocHandle<TestDoc>(TEST_ID)
|
|
97
96
|
assert.equal(handle.isReady(), false)
|
|
98
|
-
|
|
97
|
+
expect(() => handle.heads()).toThrow("DocHandle is not ready")
|
|
99
98
|
})
|
|
100
99
|
|
|
101
100
|
it("should return the history when requested", async () => {
|
|
@@ -128,7 +127,7 @@ describe("DocHandle", () => {
|
|
|
128
127
|
|
|
129
128
|
const history = handle.history()
|
|
130
129
|
const viewHandle = new DocHandle<TestDoc>(TEST_ID, { heads: history[0] })
|
|
131
|
-
viewHandle.update(() => A.clone(handle.
|
|
130
|
+
viewHandle.update(() => A.clone(handle.doc()!))
|
|
132
131
|
viewHandle.doneLoading()
|
|
133
132
|
|
|
134
133
|
assert.deepEqual(await viewHandle.doc(), { foo: "zero" })
|
|
@@ -260,8 +259,6 @@ describe("DocHandle", () => {
|
|
|
260
259
|
const handle = new DocHandle<TestDoc>(TEST_ID)
|
|
261
260
|
assert.equal(handle.isReady(), false)
|
|
262
261
|
|
|
263
|
-
handle.doc()
|
|
264
|
-
|
|
265
262
|
assert(vi.getTimerCount() > timerCount)
|
|
266
263
|
|
|
267
264
|
// simulate loading from storage
|
|
@@ -286,7 +283,7 @@ describe("DocHandle", () => {
|
|
|
286
283
|
assert.equal(handle.isReady(), true)
|
|
287
284
|
handle.change(d => (d.foo = "pizza"))
|
|
288
285
|
|
|
289
|
-
const doc =
|
|
286
|
+
const doc = handle.doc()
|
|
290
287
|
assert.equal(doc?.foo, "pizza")
|
|
291
288
|
})
|
|
292
289
|
|
|
@@ -296,7 +293,9 @@ describe("DocHandle", () => {
|
|
|
296
293
|
// we don't have it in storage, so we request it from the network
|
|
297
294
|
handle.request()
|
|
298
295
|
|
|
299
|
-
|
|
296
|
+
await expect(() => {
|
|
297
|
+
handle.doc()
|
|
298
|
+
}).toThrowError("DocHandle is not ready")
|
|
300
299
|
assert.equal(handle.isReady(), false)
|
|
301
300
|
assert.throws(() => handle.change(_ => {}))
|
|
302
301
|
})
|
|
@@ -312,7 +311,7 @@ describe("DocHandle", () => {
|
|
|
312
311
|
return A.change(doc, d => (d.foo = "bar"))
|
|
313
312
|
})
|
|
314
313
|
|
|
315
|
-
const doc =
|
|
314
|
+
const doc = handle.doc()
|
|
316
315
|
assert.equal(handle.isReady(), true)
|
|
317
316
|
assert.equal(doc?.foo, "bar")
|
|
318
317
|
})
|
|
@@ -328,7 +327,7 @@ describe("DocHandle", () => {
|
|
|
328
327
|
doc.foo = "bar"
|
|
329
328
|
})
|
|
330
329
|
|
|
331
|
-
const doc =
|
|
330
|
+
const doc = handle.doc()
|
|
332
331
|
assert.equal(doc?.foo, "bar")
|
|
333
332
|
|
|
334
333
|
const changePayload = await p
|
|
@@ -353,7 +352,7 @@ describe("DocHandle", () => {
|
|
|
353
352
|
|
|
354
353
|
const p = new Promise<void>(resolve =>
|
|
355
354
|
handle.once("change", ({ handle, doc }) => {
|
|
356
|
-
assert.equal(handle.
|
|
355
|
+
assert.equal(handle.doc()?.foo, doc.foo)
|
|
357
356
|
|
|
358
357
|
resolve()
|
|
359
358
|
})
|
|
@@ -390,7 +389,7 @@ describe("DocHandle", () => {
|
|
|
390
389
|
doc.foo = "baz"
|
|
391
390
|
})
|
|
392
391
|
|
|
393
|
-
const doc =
|
|
392
|
+
const doc = handle.doc()
|
|
394
393
|
assert.equal(doc?.foo, "baz")
|
|
395
394
|
|
|
396
395
|
return p
|
|
@@ -405,7 +404,7 @@ describe("DocHandle", () => {
|
|
|
405
404
|
})
|
|
406
405
|
|
|
407
406
|
await p
|
|
408
|
-
const doc =
|
|
407
|
+
const doc = handle.doc()
|
|
409
408
|
assert.equal(doc?.foo, "bar")
|
|
410
409
|
})
|
|
411
410
|
|
|
@@ -425,11 +424,7 @@ describe("DocHandle", () => {
|
|
|
425
424
|
// set docHandle time out after 5 ms
|
|
426
425
|
const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
|
|
427
426
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
assert.equal(doc, undefined)
|
|
431
|
-
|
|
432
|
-
assert.equal(handle.state, "unavailable")
|
|
427
|
+
expect(() => handle.doc()).toThrowError("DocHandle is not ready")
|
|
433
428
|
})
|
|
434
429
|
|
|
435
430
|
it("should not time out if the document is loaded in time", async () => {
|
|
@@ -440,11 +435,11 @@ describe("DocHandle", () => {
|
|
|
440
435
|
handle.update(doc => docFromMockStorage(doc))
|
|
441
436
|
|
|
442
437
|
// now it should not time out
|
|
443
|
-
const doc =
|
|
438
|
+
const doc = handle.doc()
|
|
444
439
|
assert.equal(doc?.foo, "bar")
|
|
445
440
|
})
|
|
446
441
|
|
|
447
|
-
it("should
|
|
442
|
+
it("should throw an exception if loading from the network times out", async () => {
|
|
448
443
|
// set docHandle time out after 5 ms
|
|
449
444
|
const handle = new DocHandle<TestDoc>(TEST_ID, { timeoutDelay: 5 })
|
|
450
445
|
|
|
@@ -454,8 +449,7 @@ describe("DocHandle", () => {
|
|
|
454
449
|
// there's no update
|
|
455
450
|
await pause(10)
|
|
456
451
|
|
|
457
|
-
|
|
458
|
-
assert.equal(doc, undefined)
|
|
452
|
+
expect(() => handle.doc()).toThrowError("DocHandle is not ready")
|
|
459
453
|
})
|
|
460
454
|
|
|
461
455
|
it("should not time out if the document is updated in time", async () => {
|
|
@@ -473,7 +467,7 @@ describe("DocHandle", () => {
|
|
|
473
467
|
// now it should not time out
|
|
474
468
|
await pause(5)
|
|
475
469
|
|
|
476
|
-
const doc =
|
|
470
|
+
const doc = handle.doc()
|
|
477
471
|
assert.equal(doc?.foo, "bar")
|
|
478
472
|
})
|
|
479
473
|
|
|
@@ -489,49 +483,6 @@ describe("DocHandle", () => {
|
|
|
489
483
|
assert.equal(handle.isDeleted(), true)
|
|
490
484
|
})
|
|
491
485
|
|
|
492
|
-
it("should clear document reference when unloaded", async () => {
|
|
493
|
-
const handle = setup()
|
|
494
|
-
|
|
495
|
-
handle.change(doc => {
|
|
496
|
-
doc.foo = "bar"
|
|
497
|
-
})
|
|
498
|
-
const doc = await handle.doc()
|
|
499
|
-
assert.equal(doc?.foo, "bar")
|
|
500
|
-
|
|
501
|
-
handle.unload()
|
|
502
|
-
assert.equal(handle.isUnloaded(), true)
|
|
503
|
-
|
|
504
|
-
const clearedDoc = await handle.doc([UNLOADED])
|
|
505
|
-
assert.notEqual(clearedDoc?.foo, "bar")
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
it("should allow reloading after unloading", async () => {
|
|
509
|
-
const handle = setup()
|
|
510
|
-
|
|
511
|
-
handle.change(doc => {
|
|
512
|
-
doc.foo = "bar"
|
|
513
|
-
})
|
|
514
|
-
const doc = await handle.doc()
|
|
515
|
-
assert.equal(doc?.foo, "bar")
|
|
516
|
-
|
|
517
|
-
handle.unload()
|
|
518
|
-
|
|
519
|
-
// reload to transition from unloaded to loading
|
|
520
|
-
handle.reload()
|
|
521
|
-
|
|
522
|
-
// simulate requesting from the network
|
|
523
|
-
handle.request()
|
|
524
|
-
|
|
525
|
-
// simulate updating from the network
|
|
526
|
-
handle.update(doc => {
|
|
527
|
-
return A.change(doc, d => (d.foo = "bar"))
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
const reloadedDoc = await handle.doc()
|
|
531
|
-
assert.equal(handle.isReady(), true)
|
|
532
|
-
assert.equal(reloadedDoc?.foo, "bar")
|
|
533
|
-
})
|
|
534
|
-
|
|
535
486
|
it("should allow changing at old heads", async () => {
|
|
536
487
|
const handle = setup()
|
|
537
488
|
|