@automerge/automerge-repo 2.0.0-alpha.7 → 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 +68 -45
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +166 -69
- 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 +235 -82
- 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 +181 -38
- 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/src/DocHandle.ts
CHANGED
|
@@ -2,11 +2,15 @@ import * as A from "@automerge/automerge/slim/next"
|
|
|
2
2
|
import debug from "debug"
|
|
3
3
|
import { EventEmitter } from "eventemitter3"
|
|
4
4
|
import { assertEvent, assign, createActor, setup, waitFor } from "xstate"
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
decodeHeads,
|
|
7
|
+
encodeHeads,
|
|
8
|
+
stringifyAutomergeUrl,
|
|
9
|
+
} from "./AutomergeUrl.js"
|
|
6
10
|
import { encode } from "./helpers/cbor.js"
|
|
7
11
|
import { headsAreSame } from "./helpers/headsAreSame.js"
|
|
8
12
|
import { withTimeout } from "./helpers/withTimeout.js"
|
|
9
|
-
import type { AutomergeUrl, DocumentId, PeerId } from "./types.js"
|
|
13
|
+
import type { AutomergeUrl, DocumentId, PeerId, UrlHeads } from "./types.js"
|
|
10
14
|
import { StorageId } from "./storage/types.js"
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -28,6 +32,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
28
32
|
/** The XState actor running our state machine. */
|
|
29
33
|
#machine
|
|
30
34
|
|
|
35
|
+
/** If set, this handle will only show the document at these heads */
|
|
36
|
+
#fixedHeads?: UrlHeads
|
|
37
|
+
|
|
31
38
|
/** The last known state of our document. */
|
|
32
39
|
#prevDocState: T = A.init<T>()
|
|
33
40
|
|
|
@@ -36,7 +43,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
36
43
|
#timeoutDelay = 60_000
|
|
37
44
|
|
|
38
45
|
/** A dictionary mapping each peer to the last heads we know they have. */
|
|
39
|
-
#remoteHeads: Record<StorageId,
|
|
46
|
+
#remoteHeads: Record<StorageId, UrlHeads> = {}
|
|
47
|
+
|
|
48
|
+
/** Cache for view handles, keyed by the stringified heads */
|
|
49
|
+
#viewCache: Map<string, DocHandle<T>> = new Map()
|
|
40
50
|
|
|
41
51
|
/** @hidden */
|
|
42
52
|
constructor(
|
|
@@ -49,6 +59,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
49
59
|
this.#timeoutDelay = options.timeoutDelay
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
if ("heads" in options) {
|
|
63
|
+
this.#fixedHeads = options.heads
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
const doc = A.init<T>()
|
|
53
67
|
|
|
54
68
|
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
|
|
@@ -72,9 +86,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
72
86
|
this.emit("delete", { handle: this })
|
|
73
87
|
return { doc: A.init() }
|
|
74
88
|
}),
|
|
75
|
-
onUnavailable: () => {
|
|
76
|
-
|
|
77
|
-
},
|
|
89
|
+
onUnavailable: assign(() => {
|
|
90
|
+
return { doc: A.init() }
|
|
91
|
+
}),
|
|
92
|
+
onUnload: assign(() => {
|
|
93
|
+
return { doc: A.init() }
|
|
94
|
+
}),
|
|
78
95
|
},
|
|
79
96
|
}).createMachine({
|
|
80
97
|
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
|
|
@@ -86,6 +103,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
86
103
|
context: { documentId, doc },
|
|
87
104
|
on: {
|
|
88
105
|
UPDATE: { actions: "onUpdate" },
|
|
106
|
+
UNLOAD: ".unloaded",
|
|
89
107
|
DELETE: ".deleted",
|
|
90
108
|
},
|
|
91
109
|
states: {
|
|
@@ -113,6 +131,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
113
131
|
on: { DOC_READY: "ready" },
|
|
114
132
|
},
|
|
115
133
|
ready: {},
|
|
134
|
+
unloaded: {
|
|
135
|
+
entry: "onUnload",
|
|
136
|
+
on: {
|
|
137
|
+
RELOAD: "loading",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
116
140
|
deleted: { entry: "onDelete", type: "final" },
|
|
117
141
|
},
|
|
118
142
|
})
|
|
@@ -131,7 +155,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
131
155
|
|
|
132
156
|
// Start the machine, and send a create or find event to get things going
|
|
133
157
|
this.#machine.start()
|
|
134
|
-
this
|
|
158
|
+
this.begin()
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
// PRIVATE
|
|
@@ -166,7 +190,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
166
190
|
#checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
|
|
167
191
|
const beforeHeads = A.getHeads(before)
|
|
168
192
|
const afterHeads = A.getHeads(after)
|
|
169
|
-
const docChanged = !headsAreSame(
|
|
193
|
+
const docChanged = !headsAreSame(
|
|
194
|
+
encodeHeads(afterHeads),
|
|
195
|
+
encodeHeads(beforeHeads)
|
|
196
|
+
)
|
|
170
197
|
if (docChanged) {
|
|
171
198
|
this.emit("heads-changed", { handle: this, doc: after })
|
|
172
199
|
|
|
@@ -192,7 +219,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
192
219
|
/** Our documentId in Automerge URL form.
|
|
193
220
|
*/
|
|
194
221
|
get url(): AutomergeUrl {
|
|
195
|
-
return stringifyAutomergeUrl({
|
|
222
|
+
return stringifyAutomergeUrl({
|
|
223
|
+
documentId: this.documentId,
|
|
224
|
+
heads: this.#fixedHeads,
|
|
225
|
+
})
|
|
196
226
|
}
|
|
197
227
|
|
|
198
228
|
/**
|
|
@@ -203,6 +233,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
203
233
|
*/
|
|
204
234
|
isReady = () => this.inState(["ready"])
|
|
205
235
|
|
|
236
|
+
/**
|
|
237
|
+
* @returns true if the document has been unloaded.
|
|
238
|
+
*
|
|
239
|
+
* Unloaded documents are freed from memory but not removed from local storage. It's not currently
|
|
240
|
+
* possible at runtime to reload an unloaded document.
|
|
241
|
+
*/
|
|
242
|
+
isUnloaded = () => this.inState(["unloaded"])
|
|
243
|
+
|
|
206
244
|
/**
|
|
207
245
|
* @returns true if the document has been marked as deleted.
|
|
208
246
|
*
|
|
@@ -241,42 +279,28 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
241
279
|
}
|
|
242
280
|
|
|
243
281
|
/**
|
|
244
|
-
*
|
|
282
|
+
* Returns the current state of the Automerge document this handle manages.
|
|
283
|
+
*
|
|
284
|
+
* @returns the current document
|
|
285
|
+
* @throws on deleted and unavailable documents
|
|
245
286
|
*
|
|
246
|
-
* This is the recommended way to access a handle's document. Note that this waits for the handle
|
|
247
|
-
* to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
|
|
248
287
|
*/
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
// wait for the document to enter one of the desired states
|
|
255
|
-
await this.#statePromise(awaitStates)
|
|
256
|
-
} catch (error) {
|
|
257
|
-
// if we timed out, return undefined
|
|
258
|
-
return undefined
|
|
288
|
+
doc() {
|
|
289
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
290
|
+
if (this.#fixedHeads) {
|
|
291
|
+
return A.view(this.#doc, decodeHeads(this.#fixedHeads))
|
|
259
292
|
}
|
|
260
|
-
|
|
261
|
-
return !this.isUnavailable() ? this.#doc : undefined
|
|
293
|
+
return this.#doc
|
|
262
294
|
}
|
|
263
295
|
|
|
264
296
|
/**
|
|
265
|
-
* Synchronously returns the current state of the Automerge document this handle manages, or
|
|
266
|
-
* undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
|
|
267
|
-
* if you want to make sure loading is complete first.
|
|
268
|
-
*
|
|
269
|
-
* Not to be confused with the SyncState of the document, which describes the state of the
|
|
270
|
-
* synchronization process.
|
|
271
297
|
*
|
|
272
|
-
*
|
|
273
|
-
* unambigous.
|
|
274
|
-
*
|
|
275
|
-
* @returns the current document, or undefined if the document is not ready.
|
|
276
|
-
*/
|
|
298
|
+
* @deprecated */
|
|
277
299
|
docSync() {
|
|
278
|
-
|
|
279
|
-
|
|
300
|
+
console.warn(
|
|
301
|
+
"docSync is deprecated. Use doc() instead. This function will be removed as part of the 2.0 release."
|
|
302
|
+
)
|
|
303
|
+
return this.doc()
|
|
280
304
|
}
|
|
281
305
|
|
|
282
306
|
/**
|
|
@@ -284,17 +308,20 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
284
308
|
* This precisely defines the state of a document.
|
|
285
309
|
* @returns the current document's heads, or undefined if the document is not ready
|
|
286
310
|
*/
|
|
287
|
-
heads():
|
|
288
|
-
if (!this.isReady())
|
|
289
|
-
|
|
311
|
+
heads(): UrlHeads {
|
|
312
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
313
|
+
if (this.#fixedHeads) {
|
|
314
|
+
return this.#fixedHeads
|
|
290
315
|
}
|
|
291
|
-
return A.getHeads(this.#doc)
|
|
316
|
+
return encodeHeads(A.getHeads(this.#doc))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
begin() {
|
|
320
|
+
this.#machine.send({ type: BEGIN })
|
|
292
321
|
}
|
|
293
322
|
|
|
294
323
|
/**
|
|
295
|
-
*
|
|
296
|
-
* by the `heads` passed in. The return value is the same type as docSync() and will return
|
|
297
|
-
* undefined if the object hasn't finished loading.
|
|
324
|
+
* Returns an array of all past "heads" for the document in topological order.
|
|
298
325
|
*
|
|
299
326
|
* @remarks
|
|
300
327
|
* A point-in-time in an automerge document is an *array* of heads since there may be
|
|
@@ -303,49 +330,133 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
303
330
|
* history views would be quite large under concurrency (every thing in each branch against each other).
|
|
304
331
|
* There might be a clever way to think about this, but we haven't found it yet, so for now at least
|
|
305
332
|
* we present a single traversable view which excludes concurrency.
|
|
306
|
-
* @returns The individual heads for every change in the document.
|
|
333
|
+
* @returns UrlHeads[] - The individual heads for every change in the document. Each item is a tagged string[1].
|
|
307
334
|
*/
|
|
308
|
-
history():
|
|
335
|
+
history(): UrlHeads[] | undefined {
|
|
309
336
|
if (!this.isReady()) {
|
|
310
337
|
return undefined
|
|
311
338
|
}
|
|
312
339
|
// This just returns all the heads as individual strings.
|
|
313
340
|
|
|
314
|
-
return A.topoHistoryTraversal(this.#doc).map(h =>
|
|
341
|
+
return A.topoHistoryTraversal(this.#doc).map(h =>
|
|
342
|
+
encodeHeads([h])
|
|
343
|
+
) as UrlHeads[]
|
|
315
344
|
}
|
|
316
345
|
|
|
317
346
|
/**
|
|
318
347
|
* Creates a fixed "view" of an automerge document at the given point in time represented
|
|
319
|
-
* by the `heads` passed in. The return value is the same type as
|
|
348
|
+
* by the `heads` passed in. The return value is the same type as doc() and will return
|
|
320
349
|
* undefined if the object hasn't finished loading.
|
|
321
|
-
*
|
|
350
|
+
*
|
|
351
|
+
* @remarks
|
|
352
|
+
* Note that our Typescript types do not consider change over time and the current version
|
|
353
|
+
* of Automerge doesn't check types at runtime, so if you go back to an old set of heads
|
|
354
|
+
* that doesn't match the heads here, Typescript will not save you.
|
|
355
|
+
*
|
|
356
|
+
* @argument heads - The heads to view the document at. See history().
|
|
357
|
+
* @returns DocHandle<T> at the time of `heads`
|
|
322
358
|
*/
|
|
323
|
-
view(heads:
|
|
359
|
+
view(heads: UrlHeads): DocHandle<T> {
|
|
324
360
|
if (!this.isReady()) {
|
|
325
|
-
|
|
361
|
+
throw new Error(
|
|
362
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
|
|
363
|
+
)
|
|
326
364
|
}
|
|
327
|
-
|
|
365
|
+
|
|
366
|
+
// Create a cache key from the heads
|
|
367
|
+
const cacheKey = JSON.stringify(heads)
|
|
368
|
+
|
|
369
|
+
// Check if we have a cached handle for these heads
|
|
370
|
+
const cachedHandle = this.#viewCache.get(cacheKey)
|
|
371
|
+
if (cachedHandle) {
|
|
372
|
+
return cachedHandle
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Create a new handle with the same documentId but fixed heads
|
|
376
|
+
const handle = new DocHandle<T>(this.documentId, {
|
|
377
|
+
heads,
|
|
378
|
+
timeoutDelay: this.#timeoutDelay,
|
|
379
|
+
})
|
|
380
|
+
handle.update(() => A.clone(this.#doc))
|
|
381
|
+
handle.doneLoading()
|
|
382
|
+
|
|
383
|
+
// Store in cache
|
|
384
|
+
this.#viewCache.set(cacheKey, handle)
|
|
385
|
+
|
|
386
|
+
return handle
|
|
328
387
|
}
|
|
329
388
|
|
|
330
389
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* undefined if the object hasn't finished loading.
|
|
390
|
+
* Returns a set of Patch operations that will move a materialized document from one state to another
|
|
391
|
+
* if applied.
|
|
334
392
|
*
|
|
335
393
|
* @remarks
|
|
336
|
-
* We allow specifying
|
|
337
|
-
*
|
|
394
|
+
* We allow specifying either:
|
|
395
|
+
* - Two sets of heads to compare directly
|
|
396
|
+
* - A single set of heads to compare against our current heads
|
|
397
|
+
* - Another DocHandle to compare against (which must share history with this document)
|
|
338
398
|
*
|
|
339
|
-
* @
|
|
399
|
+
* @throws Error if the documents don't share history or if either document is not ready
|
|
400
|
+
* @returns Automerge patches that go from one document state to the other
|
|
340
401
|
*/
|
|
341
|
-
diff(first:
|
|
402
|
+
diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
|
|
342
403
|
if (!this.isReady()) {
|
|
343
|
-
|
|
404
|
+
throw new Error(
|
|
405
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const doc = this.#doc
|
|
410
|
+
if (!doc) throw new Error("Document not available")
|
|
411
|
+
|
|
412
|
+
// If first argument is a DocHandle
|
|
413
|
+
if (first instanceof DocHandle) {
|
|
414
|
+
if (!first.isReady()) {
|
|
415
|
+
throw new Error("Cannot diff against a handle that isn't ready")
|
|
416
|
+
}
|
|
417
|
+
const otherHeads = first.heads()
|
|
418
|
+
if (!otherHeads) throw new Error("Other document's heads not available")
|
|
419
|
+
|
|
420
|
+
// Create a temporary merged doc to verify shared history and compute diff
|
|
421
|
+
const mergedDoc = A.merge(A.clone(doc), first.doc()!)
|
|
422
|
+
// Use the merged doc to compute the diff
|
|
423
|
+
return A.diff(
|
|
424
|
+
mergedDoc,
|
|
425
|
+
decodeHeads(this.heads()!),
|
|
426
|
+
decodeHeads(otherHeads)
|
|
427
|
+
)
|
|
344
428
|
}
|
|
345
|
-
|
|
346
|
-
|
|
429
|
+
|
|
430
|
+
// Otherwise treat as heads
|
|
431
|
+
const from = second ? first : ((this.heads() || []) as UrlHeads)
|
|
347
432
|
const to = second ? second : first
|
|
348
|
-
return A.diff(
|
|
433
|
+
return A.diff(doc, decodeHeads(from), decodeHeads(to))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* `metadata(head?)` allows you to look at the metadata for a change
|
|
438
|
+
* this can be used to build history graphs to find commit messages and edit times.
|
|
439
|
+
* this interface.
|
|
440
|
+
*
|
|
441
|
+
* @remarks
|
|
442
|
+
* I'm really not convinced this is the right way to surface this information so
|
|
443
|
+
* I'm leaving this API "hidden".
|
|
444
|
+
*
|
|
445
|
+
* @hidden
|
|
446
|
+
*/
|
|
447
|
+
metadata(change?: string): A.DecodedChange | undefined {
|
|
448
|
+
if (!this.isReady()) {
|
|
449
|
+
return undefined
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!change) {
|
|
453
|
+
change = this.heads()![0]
|
|
454
|
+
}
|
|
455
|
+
// we return undefined instead of null by convention in this API
|
|
456
|
+
return (
|
|
457
|
+
A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
|
|
458
|
+
undefined
|
|
459
|
+
)
|
|
349
460
|
}
|
|
350
461
|
|
|
351
462
|
/**
|
|
@@ -368,16 +479,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
368
479
|
}
|
|
369
480
|
|
|
370
481
|
/**
|
|
371
|
-
* Called by the repo
|
|
482
|
+
* Called by the repo when a doc handle changes or we receive new remote heads.
|
|
372
483
|
* @hidden
|
|
373
484
|
*/
|
|
374
|
-
setRemoteHeads(storageId: StorageId, heads:
|
|
485
|
+
setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
|
|
375
486
|
this.#remoteHeads[storageId] = heads
|
|
376
487
|
this.emit("remote-heads", { storageId, heads })
|
|
377
488
|
}
|
|
378
489
|
|
|
379
490
|
/** Returns the heads of the storageId. */
|
|
380
|
-
getRemoteHeads(storageId: StorageId):
|
|
491
|
+
getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
|
|
381
492
|
return this.#remoteHeads[storageId]
|
|
382
493
|
}
|
|
383
494
|
|
|
@@ -402,6 +513,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
402
513
|
`DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
403
514
|
)
|
|
404
515
|
}
|
|
516
|
+
|
|
517
|
+
if (this.#fixedHeads) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
405
523
|
this.#machine.send({
|
|
406
524
|
type: UPDATE,
|
|
407
525
|
payload: { callback: doc => A.change(doc, options, callback) },
|
|
@@ -413,22 +531,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
413
531
|
* @returns A set of heads representing the concurrent change that was made.
|
|
414
532
|
*/
|
|
415
533
|
changeAt(
|
|
416
|
-
heads:
|
|
534
|
+
heads: UrlHeads,
|
|
417
535
|
callback: A.ChangeFn<T>,
|
|
418
536
|
options: A.ChangeOptions<T> = {}
|
|
419
|
-
):
|
|
537
|
+
): UrlHeads[] | undefined {
|
|
420
538
|
if (!this.isReady()) {
|
|
421
539
|
throw new Error(
|
|
422
540
|
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
423
541
|
)
|
|
424
542
|
}
|
|
425
|
-
|
|
543
|
+
if (this.#fixedHeads) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
let resultHeads: UrlHeads | undefined = undefined
|
|
426
549
|
this.#machine.send({
|
|
427
550
|
type: UPDATE,
|
|
428
551
|
payload: {
|
|
429
552
|
callback: doc => {
|
|
430
|
-
const result = A.changeAt(doc, heads, options, callback)
|
|
431
|
-
resultHeads = result.newHeads
|
|
553
|
+
const result = A.changeAt(doc, decodeHeads(heads), options, callback)
|
|
554
|
+
resultHeads = result.newHeads
|
|
555
|
+
? encodeHeads(result.newHeads)
|
|
556
|
+
: undefined
|
|
432
557
|
return result.newDoc
|
|
433
558
|
},
|
|
434
559
|
},
|
|
@@ -453,10 +578,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
453
578
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
454
579
|
throw new Error("Both handles must be ready to merge")
|
|
455
580
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
581
|
+
if (this.#fixedHeads) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
584
|
+
)
|
|
459
585
|
}
|
|
586
|
+
const mergingDoc = otherHandle.doc()
|
|
460
587
|
|
|
461
588
|
this.update(doc => {
|
|
462
589
|
return A.merge(doc, mergingDoc)
|
|
@@ -464,20 +591,31 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
464
591
|
}
|
|
465
592
|
|
|
466
593
|
/**
|
|
467
|
-
*
|
|
594
|
+
* Updates the internal state machine to mark the document unavailable.
|
|
468
595
|
* @hidden
|
|
469
596
|
*/
|
|
470
597
|
unavailable() {
|
|
471
598
|
this.#machine.send({ type: DOC_UNAVAILABLE })
|
|
472
599
|
}
|
|
473
600
|
|
|
474
|
-
/**
|
|
601
|
+
/**
|
|
602
|
+
* Called by the repo either when the document is not found in storage.
|
|
475
603
|
* @hidden
|
|
476
604
|
* */
|
|
477
605
|
request() {
|
|
478
606
|
if (this.#state === "loading") this.#machine.send({ type: REQUEST })
|
|
479
607
|
}
|
|
480
608
|
|
|
609
|
+
/** Called by the repo to free memory used by the document. */
|
|
610
|
+
unload() {
|
|
611
|
+
this.#machine.send({ type: UNLOAD })
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Called by the repo to reuse an unloaded handle. */
|
|
615
|
+
reload() {
|
|
616
|
+
this.#machine.send({ type: RELOAD })
|
|
617
|
+
}
|
|
618
|
+
|
|
481
619
|
/** Called by the repo when the document is deleted. */
|
|
482
620
|
delete() {
|
|
483
621
|
this.#machine.send({ type: DELETE })
|
|
@@ -493,7 +631,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
493
631
|
broadcast(message: unknown) {
|
|
494
632
|
this.emit("ephemeral-message-outbound", {
|
|
495
633
|
handle: this,
|
|
496
|
-
data: encode(message),
|
|
634
|
+
data: new Uint8Array(encode(message)),
|
|
497
635
|
})
|
|
498
636
|
}
|
|
499
637
|
|
|
@@ -518,6 +656,9 @@ export type DocHandleOptions<T> =
|
|
|
518
656
|
| {
|
|
519
657
|
isNew?: false
|
|
520
658
|
|
|
659
|
+
// An optional point in time to lock the document to.
|
|
660
|
+
heads?: UrlHeads
|
|
661
|
+
|
|
521
662
|
/** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
|
|
522
663
|
timeoutDelay?: number
|
|
523
664
|
}
|
|
@@ -529,7 +670,6 @@ export interface DocHandleEvents<T> {
|
|
|
529
670
|
"heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
|
|
530
671
|
change: (payload: DocHandleChangePayload<T>) => void
|
|
531
672
|
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
532
|
-
unavailable: (payload: DocHandleUnavailablePayload<T>) => void
|
|
533
673
|
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
534
674
|
"ephemeral-message-outbound": (
|
|
535
675
|
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
@@ -581,7 +721,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
|
581
721
|
/** Emitted when we have new remote heads for this document */
|
|
582
722
|
export interface DocHandleRemoteHeadsPayload {
|
|
583
723
|
storageId: StorageId
|
|
584
|
-
heads:
|
|
724
|
+
heads: UrlHeads
|
|
585
725
|
}
|
|
586
726
|
|
|
587
727
|
// STATE MACHINE TYPES & CONSTANTS
|
|
@@ -600,6 +740,8 @@ export const HandleState = {
|
|
|
600
740
|
REQUESTING: "requesting",
|
|
601
741
|
/** The document is available */
|
|
602
742
|
READY: "ready",
|
|
743
|
+
/** The document has been unloaded from the handle, to free memory usage */
|
|
744
|
+
UNLOADED: "unloaded",
|
|
603
745
|
/** The document has been deleted from the repo */
|
|
604
746
|
DELETED: "deleted",
|
|
605
747
|
/** The document was not available in storage or from any connected peers */
|
|
@@ -607,8 +749,15 @@ export const HandleState = {
|
|
|
607
749
|
} as const
|
|
608
750
|
export type HandleState = (typeof HandleState)[keyof typeof HandleState]
|
|
609
751
|
|
|
610
|
-
export const {
|
|
611
|
-
|
|
752
|
+
export const {
|
|
753
|
+
IDLE,
|
|
754
|
+
LOADING,
|
|
755
|
+
REQUESTING,
|
|
756
|
+
READY,
|
|
757
|
+
UNLOADED,
|
|
758
|
+
DELETED,
|
|
759
|
+
UNAVAILABLE,
|
|
760
|
+
} = HandleState
|
|
612
761
|
|
|
613
762
|
// context
|
|
614
763
|
|
|
@@ -628,14 +777,18 @@ type DocHandleEvent<T> =
|
|
|
628
777
|
type: typeof UPDATE
|
|
629
778
|
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
|
|
630
779
|
}
|
|
631
|
-
| { type: typeof
|
|
780
|
+
| { type: typeof UNLOAD }
|
|
781
|
+
| { type: typeof RELOAD }
|
|
632
782
|
| { type: typeof DELETE }
|
|
783
|
+
| { type: typeof TIMEOUT }
|
|
633
784
|
| { type: typeof DOC_UNAVAILABLE }
|
|
634
785
|
|
|
635
786
|
const BEGIN = "BEGIN"
|
|
636
787
|
const REQUEST = "REQUEST"
|
|
637
788
|
const DOC_READY = "DOC_READY"
|
|
638
789
|
const UPDATE = "UPDATE"
|
|
790
|
+
const UNLOAD = "UNLOAD"
|
|
791
|
+
const RELOAD = "RELOAD"
|
|
639
792
|
const DELETE = "DELETE"
|
|
640
793
|
const TIMEOUT = "TIMEOUT"
|
|
641
794
|
const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DocHandle } from "./DocHandle.js"
|
|
2
|
+
|
|
3
|
+
export type FindProgressState =
|
|
4
|
+
| "loading"
|
|
5
|
+
| "ready"
|
|
6
|
+
| "failed"
|
|
7
|
+
| "aborted"
|
|
8
|
+
| "unavailable"
|
|
9
|
+
|
|
10
|
+
interface FindProgressBase<T> {
|
|
11
|
+
state: FindProgressState
|
|
12
|
+
handle: DocHandle<T>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FindProgressLoading<T> extends FindProgressBase<T> {
|
|
16
|
+
state: "loading"
|
|
17
|
+
progress: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FindProgressReady<T> extends FindProgressBase<T> {
|
|
21
|
+
state: "ready"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FindProgressFailed<T> extends FindProgressBase<T> {
|
|
25
|
+
state: "failed"
|
|
26
|
+
error: Error
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FindProgressUnavailable<T> extends FindProgressBase<T> {
|
|
30
|
+
state: "unavailable"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FindProgressAborted<T> extends FindProgressBase<T> {
|
|
34
|
+
state: "aborted"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type FindProgress<T> =
|
|
38
|
+
| FindProgressLoading<T>
|
|
39
|
+
| FindProgressReady<T>
|
|
40
|
+
| FindProgressFailed<T>
|
|
41
|
+
| FindProgressUnavailable<T>
|
|
42
|
+
| FindProgressAborted<T>
|
|
43
|
+
|
|
44
|
+
export type FindProgressWithMethods<T> = FindProgress<T> & {
|
|
45
|
+
next: () => Promise<FindProgressWithMethods<T>>
|
|
46
|
+
// TODO: i don't like this allowableStates
|
|
47
|
+
untilReady: (allowableStates: string[]) => Promise<DocHandle<T>>
|
|
48
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { next as A } from "@automerge/automerge/slim"
|
|
2
1
|
import { EventEmitter } from "eventemitter3"
|
|
3
|
-
import { DocumentId, PeerId } from "./types.js"
|
|
2
|
+
import { DocumentId, PeerId, UrlHeads } from "./types.js"
|
|
4
3
|
import {
|
|
5
4
|
RemoteHeadsChanged,
|
|
6
5
|
RemoteSubscriptionControlMessage,
|
|
@@ -12,7 +11,7 @@ import debug from "debug"
|
|
|
12
11
|
export type RemoteHeadsSubscriptionEventPayload = {
|
|
13
12
|
documentId: DocumentId
|
|
14
13
|
storageId: StorageId
|
|
15
|
-
remoteHeads:
|
|
14
|
+
remoteHeads: UrlHeads
|
|
16
15
|
timestamp: number
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -21,7 +20,7 @@ export type NotifyRemoteHeadsPayload = {
|
|
|
21
20
|
targetId: PeerId
|
|
22
21
|
documentId: DocumentId
|
|
23
22
|
storageId: StorageId
|
|
24
|
-
heads:
|
|
23
|
+
heads: UrlHeads
|
|
25
24
|
timestamp: number
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -216,7 +215,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
216
215
|
handleImmediateRemoteHeadsChanged(
|
|
217
216
|
documentId: DocumentId,
|
|
218
217
|
storageId: StorageId,
|
|
219
|
-
heads:
|
|
218
|
+
heads: UrlHeads
|
|
220
219
|
) {
|
|
221
220
|
this.#log("handleLocalHeadsChanged", documentId, storageId, heads)
|
|
222
221
|
const remote = this.#knownHeads.get(documentId)
|
|
@@ -334,7 +333,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
334
333
|
#changedHeads(msg: RemoteHeadsChanged): {
|
|
335
334
|
documentId: DocumentId
|
|
336
335
|
storageId: StorageId
|
|
337
|
-
remoteHeads:
|
|
336
|
+
remoteHeads: UrlHeads
|
|
338
337
|
timestamp: number
|
|
339
338
|
}[] {
|
|
340
339
|
const changedHeads = []
|
|
@@ -356,11 +355,14 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
356
355
|
if (docRemote && docRemote.timestamp >= timestamp) {
|
|
357
356
|
continue
|
|
358
357
|
} else {
|
|
359
|
-
remote.set(storageId as StorageId, {
|
|
358
|
+
remote.set(storageId as StorageId, {
|
|
359
|
+
timestamp,
|
|
360
|
+
heads: heads as UrlHeads,
|
|
361
|
+
})
|
|
360
362
|
changedHeads.push({
|
|
361
363
|
documentId,
|
|
362
364
|
storageId: storageId as StorageId,
|
|
363
|
-
remoteHeads: heads,
|
|
365
|
+
remoteHeads: heads as UrlHeads,
|
|
364
366
|
timestamp,
|
|
365
367
|
})
|
|
366
368
|
}
|
|
@@ -371,5 +373,5 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
371
373
|
|
|
372
374
|
type LastHeads = {
|
|
373
375
|
timestamp: number
|
|
374
|
-
heads:
|
|
376
|
+
heads: UrlHeads
|
|
375
377
|
}
|