@automerge/automerge-repo 2.0.0-alpha.2 → 2.0.0-alpha.22
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 +5 -6
- 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 +89 -20
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +189 -28
- 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 +44 -6
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +226 -87
- package/dist/entrypoints/fullfat.d.ts +1 -0
- package/dist/entrypoints/fullfat.d.ts.map +1 -1
- package/dist/entrypoints/fullfat.js +1 -2
- 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/bufferFromHex.d.ts +3 -0
- package/dist/helpers/bufferFromHex.d.ts.map +1 -0
- package/dist/helpers/bufferFromHex.js +13 -0
- 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/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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/storage/StorageSubsystem.d.ts +11 -1
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +20 -4
- package/dist/synchronizer/CollectionSynchronizer.d.ts +17 -3
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +43 -18
- package/dist/synchronizer/DocSynchronizer.d.ts +10 -2
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +30 -8
- 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 -3
- package/src/AutomergeUrl.ts +101 -26
- package/src/DocHandle.ts +256 -38
- package/src/FindProgress.ts +48 -0
- package/src/RemoteHeadsSubscriptions.ts +11 -9
- package/src/Repo.ts +310 -95
- package/src/entrypoints/fullfat.ts +1 -2
- 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 +2 -0
- package/src/storage/StorageSubsystem.ts +29 -4
- package/src/synchronizer/CollectionSynchronizer.ts +56 -19
- package/src/synchronizer/DocSynchronizer.ts +34 -9
- 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 +189 -29
- package/test/DocSynchronizer.test.ts +10 -3
- package/test/Repo.test.ts +377 -191
- package/test/StorageSubsystem.test.ts +17 -0
- 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,7 @@ 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> = {}
|
|
40
47
|
|
|
41
48
|
/** @hidden */
|
|
42
49
|
constructor(
|
|
@@ -49,6 +56,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
49
56
|
this.#timeoutDelay = options.timeoutDelay
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
if ("heads" in options) {
|
|
60
|
+
this.#fixedHeads = options.heads
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
const doc = A.init<T>()
|
|
53
64
|
|
|
54
65
|
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
|
|
@@ -72,9 +83,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
72
83
|
this.emit("delete", { handle: this })
|
|
73
84
|
return { doc: A.init() }
|
|
74
85
|
}),
|
|
75
|
-
onUnavailable: () => {
|
|
76
|
-
|
|
77
|
-
},
|
|
86
|
+
onUnavailable: assign(() => {
|
|
87
|
+
return { doc: A.init() }
|
|
88
|
+
}),
|
|
89
|
+
onUnload: assign(() => {
|
|
90
|
+
return { doc: A.init() }
|
|
91
|
+
}),
|
|
78
92
|
},
|
|
79
93
|
}).createMachine({
|
|
80
94
|
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgFUAFAEQEEAVAUQG0AGAXUVAAcB7WXAC64e+TiAAeiAOwAOAKwA6ACxSAzKqks1ATjlTdAGhABPRAFolAJksKN2y1KtKAbFLla5AX09G0WPISkVAwAMgyMrBxIILz8QiJikggAjCzOijKqLEqqybJyLizaRqYIFpbJtro5Uo7J2o5S3r4YOATECrgQADZgJADCAEoM9MzsYrGCwqLRSeoyCtra8pa5adquySXmDjY5ac7JljLJeepKzSB+bYGdPX0AYgCSAHJUkRN8UwmziM7HCgqyVcUnqcmScmcMm2ZV2yiyzkOx1OalUFx8V1aAQ63R46AgBCgJGGAEUyAwAMp0D7RSbxGagJKHFgKOSWJTJGRSCosCpKaEmRCqbQKU5yXINeTaer6LwY67YogKXH4wkkKgAeX6AH1hjQqABNGncL70xKIJQ5RY5BHOJag6wwpRyEWImQVeT1aWrVSXBXtJUqgn4Ik0ADqNCedG1L3CYY1gwA0saYqbpuaEG4pKLksKpFDgcsCjDhTnxTKpTLdH6sQGFOgAO7oKYhl5gAQNngAJwA1iRY3R40ndSNDSm6enfpm5BkWAVkvy7bpuTCKq7ndZnfVeSwuTX-HWu2AAI4AVzgQhD6q12rILxoADVIyEaAAhMLjtM-RmIE4LVSQi4nLLDIGzOCWwLKA0cgyLBoFWNy+43B0R5nheaqajqepjuMtJfgyEh-FoixqMCoKqOyhzgYKCDOq6UIeuCSxHOoSGKgop74OgABuzbdOgABGvTXlho5GrhJpxJOP4pLulT6KoMhpJY2hzsWNF0QobqMV6LG+pc+A8BAcBiP6gSfFJ36EQgKksksKxrHamwwmY7gLKB85QjBzoAWxdZdL0FnfARST8ooLC7qoTnWBU4pyC5ViVMKBQaHUDQuM4fm3EGhJBWaU7-CysEAUp3LpEpWw0WYRw2LmqzgqciIsCxWUdI2zaXlAbYdt2PZ5dJ1n5jY2iJY1ikOIcMJHCyUWHC62hRZkUVNPKta3Kh56wJ1-VWUyzhFc64JWJCtQNBBzhQW4cHwbsrVKpxPF8YJgV4ZZIWIKkiKiiNSkqZYWjzCWaQ5hFh0AcCuR3QoR74qUknBRmzholpv3OkpRQNNRpTzaKTWKbIWR5FDxm9AIkA7e9skUYCWayLILBZGoLkUSKbIyIdpxHPoyTeN4QA */
|
|
@@ -86,6 +100,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
86
100
|
context: { documentId, doc },
|
|
87
101
|
on: {
|
|
88
102
|
UPDATE: { actions: "onUpdate" },
|
|
103
|
+
UNLOAD: ".unloaded",
|
|
89
104
|
DELETE: ".deleted",
|
|
90
105
|
},
|
|
91
106
|
states: {
|
|
@@ -113,6 +128,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
113
128
|
on: { DOC_READY: "ready" },
|
|
114
129
|
},
|
|
115
130
|
ready: {},
|
|
131
|
+
unloaded: {
|
|
132
|
+
entry: "onUnload",
|
|
133
|
+
on: {
|
|
134
|
+
RELOAD: "loading",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
116
137
|
deleted: { entry: "onDelete", type: "final" },
|
|
117
138
|
},
|
|
118
139
|
})
|
|
@@ -131,7 +152,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
131
152
|
|
|
132
153
|
// Start the machine, and send a create or find event to get things going
|
|
133
154
|
this.#machine.start()
|
|
134
|
-
this
|
|
155
|
+
this.begin()
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
// PRIVATE
|
|
@@ -166,7 +187,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
166
187
|
#checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
|
|
167
188
|
const beforeHeads = A.getHeads(before)
|
|
168
189
|
const afterHeads = A.getHeads(after)
|
|
169
|
-
const docChanged = !headsAreSame(
|
|
190
|
+
const docChanged = !headsAreSame(
|
|
191
|
+
encodeHeads(afterHeads),
|
|
192
|
+
encodeHeads(beforeHeads)
|
|
193
|
+
)
|
|
170
194
|
if (docChanged) {
|
|
171
195
|
this.emit("heads-changed", { handle: this, doc: after })
|
|
172
196
|
|
|
@@ -192,7 +216,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
192
216
|
/** Our documentId in Automerge URL form.
|
|
193
217
|
*/
|
|
194
218
|
get url(): AutomergeUrl {
|
|
195
|
-
return stringifyAutomergeUrl({
|
|
219
|
+
return stringifyAutomergeUrl({
|
|
220
|
+
documentId: this.documentId,
|
|
221
|
+
heads: this.#fixedHeads,
|
|
222
|
+
})
|
|
196
223
|
}
|
|
197
224
|
|
|
198
225
|
/**
|
|
@@ -203,6 +230,14 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
203
230
|
*/
|
|
204
231
|
isReady = () => this.inState(["ready"])
|
|
205
232
|
|
|
233
|
+
/**
|
|
234
|
+
* @returns true if the document has been unloaded.
|
|
235
|
+
*
|
|
236
|
+
* Unloaded documents are freed from memory but not removed from local storage. It's not currently
|
|
237
|
+
* possible at runtime to reload an unloaded document.
|
|
238
|
+
*/
|
|
239
|
+
isUnloaded = () => this.inState(["unloaded"])
|
|
240
|
+
|
|
206
241
|
/**
|
|
207
242
|
* @returns true if the document has been marked as deleted.
|
|
208
243
|
*
|
|
@@ -246,7 +281,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
246
281
|
* This is the recommended way to access a handle's document. Note that this waits for the handle
|
|
247
282
|
* to be ready if necessary. If loading (or synchronization) fails, this will never resolve.
|
|
248
283
|
*/
|
|
249
|
-
async
|
|
284
|
+
async legacyAsyncDoc(
|
|
250
285
|
/** states to wait for, such as "LOADING". mostly for internal use. */
|
|
251
286
|
awaitStates: HandleState[] = ["ready", "unavailable"]
|
|
252
287
|
) {
|
|
@@ -262,21 +297,28 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
262
297
|
}
|
|
263
298
|
|
|
264
299
|
/**
|
|
265
|
-
*
|
|
266
|
-
* undefined. Consider using `await handle.doc()` instead. Check `isReady()`, or use `whenReady()`
|
|
267
|
-
* if you want to make sure loading is complete first.
|
|
300
|
+
* Returns the current state of the Automerge document this handle manages.
|
|
268
301
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
302
|
+
* @returns the current document
|
|
303
|
+
* @throws on deleted and unavailable documents
|
|
271
304
|
*
|
|
272
|
-
* Note that `undefined` is not a valid Automerge document, so the return from this function is
|
|
273
|
-
* unambigous.
|
|
274
|
-
*
|
|
275
|
-
* @returns the current document, or undefined if the document is not ready.
|
|
276
305
|
*/
|
|
306
|
+
doc() {
|
|
307
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
308
|
+
if (this.#fixedHeads) {
|
|
309
|
+
return A.view(this.#doc, decodeHeads(this.#fixedHeads))
|
|
310
|
+
}
|
|
311
|
+
return this.#doc
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
*
|
|
316
|
+
* @deprecated */
|
|
277
317
|
docSync() {
|
|
278
|
-
|
|
279
|
-
|
|
318
|
+
console.warn(
|
|
319
|
+
"docSync is deprecated. Use doc() instead. This function will be removed as part of the 2.0 release."
|
|
320
|
+
)
|
|
321
|
+
return this.doc()
|
|
280
322
|
}
|
|
281
323
|
|
|
282
324
|
/**
|
|
@@ -284,11 +326,142 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
284
326
|
* This precisely defines the state of a document.
|
|
285
327
|
* @returns the current document's heads, or undefined if the document is not ready
|
|
286
328
|
*/
|
|
287
|
-
heads():
|
|
329
|
+
heads(): UrlHeads {
|
|
330
|
+
if (!this.isReady()) throw new Error("DocHandle is not ready")
|
|
331
|
+
if (this.#fixedHeads) {
|
|
332
|
+
return this.#fixedHeads
|
|
333
|
+
}
|
|
334
|
+
return encodeHeads(A.getHeads(this.#doc))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
begin() {
|
|
338
|
+
this.#machine.send({ type: BEGIN })
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Returns an array of all past "heads" for the document in topological order.
|
|
343
|
+
*
|
|
344
|
+
* @remarks
|
|
345
|
+
* A point-in-time in an automerge document is an *array* of heads since there may be
|
|
346
|
+
* concurrent edits. This API just returns a topologically sorted history of all edits
|
|
347
|
+
* so every previous entry will be (in some sense) before later ones, but the set of all possible
|
|
348
|
+
* history views would be quite large under concurrency (every thing in each branch against each other).
|
|
349
|
+
* There might be a clever way to think about this, but we haven't found it yet, so for now at least
|
|
350
|
+
* we present a single traversable view which excludes concurrency.
|
|
351
|
+
* @returns UrlHeads[] - The individual heads for every change in the document. Each item is a tagged string[1].
|
|
352
|
+
*/
|
|
353
|
+
history(): UrlHeads[] | undefined {
|
|
288
354
|
if (!this.isReady()) {
|
|
289
355
|
return undefined
|
|
290
356
|
}
|
|
291
|
-
|
|
357
|
+
// This just returns all the heads as individual strings.
|
|
358
|
+
|
|
359
|
+
return A.topoHistoryTraversal(this.#doc).map(h =>
|
|
360
|
+
encodeHeads([h])
|
|
361
|
+
) as UrlHeads[]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Creates a fixed "view" of an automerge document at the given point in time represented
|
|
366
|
+
* by the `heads` passed in. The return value is the same type as doc() and will return
|
|
367
|
+
* undefined if the object hasn't finished loading.
|
|
368
|
+
*
|
|
369
|
+
* @remarks
|
|
370
|
+
* Note that our Typescript types do not consider change over time and the current version
|
|
371
|
+
* of Automerge doesn't check types at runtime, so if you go back to an old set of heads
|
|
372
|
+
* that doesn't match the heads here, Typescript will not save you.
|
|
373
|
+
*
|
|
374
|
+
* @argument heads - The heads to view the document at. See history().
|
|
375
|
+
* @returns DocHandle<T> at the time of `heads`
|
|
376
|
+
*/
|
|
377
|
+
view(heads: UrlHeads): DocHandle<T> {
|
|
378
|
+
if (!this.isReady()) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
// Create a new handle with the same documentId but fixed heads
|
|
384
|
+
const handle = new DocHandle<T>(this.documentId, {
|
|
385
|
+
heads,
|
|
386
|
+
timeoutDelay: this.#timeoutDelay,
|
|
387
|
+
})
|
|
388
|
+
handle.update(() => A.clone(this.#doc))
|
|
389
|
+
handle.doneLoading()
|
|
390
|
+
|
|
391
|
+
return handle
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Returns a set of Patch operations that will move a materialized document from one state to another
|
|
396
|
+
* if applied.
|
|
397
|
+
*
|
|
398
|
+
* @remarks
|
|
399
|
+
* We allow specifying either:
|
|
400
|
+
* - Two sets of heads to compare directly
|
|
401
|
+
* - A single set of heads to compare against our current heads
|
|
402
|
+
* - Another DocHandle to compare against (which must share history with this document)
|
|
403
|
+
*
|
|
404
|
+
* @throws Error if the documents don't share history or if either document is not ready
|
|
405
|
+
* @returns Automerge patches that go from one document state to the other
|
|
406
|
+
*/
|
|
407
|
+
diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
|
|
408
|
+
if (!this.isReady()) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const doc = this.#doc
|
|
415
|
+
if (!doc) throw new Error("Document not available")
|
|
416
|
+
|
|
417
|
+
// If first argument is a DocHandle
|
|
418
|
+
if (first instanceof DocHandle) {
|
|
419
|
+
if (!first.isReady()) {
|
|
420
|
+
throw new Error("Cannot diff against a handle that isn't ready")
|
|
421
|
+
}
|
|
422
|
+
const otherHeads = first.heads()
|
|
423
|
+
if (!otherHeads) throw new Error("Other document's heads not available")
|
|
424
|
+
|
|
425
|
+
// Create a temporary merged doc to verify shared history and compute diff
|
|
426
|
+
const mergedDoc = A.merge(A.clone(doc), first.doc()!)
|
|
427
|
+
// Use the merged doc to compute the diff
|
|
428
|
+
return A.diff(
|
|
429
|
+
mergedDoc,
|
|
430
|
+
decodeHeads(this.heads()!),
|
|
431
|
+
decodeHeads(otherHeads)
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Otherwise treat as heads
|
|
436
|
+
const from = second ? first : ((this.heads() || []) as UrlHeads)
|
|
437
|
+
const to = second ? second : first
|
|
438
|
+
return A.diff(doc, decodeHeads(from), decodeHeads(to))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* `metadata(head?)` allows you to look at the metadata for a change
|
|
443
|
+
* this can be used to build history graphs to find commit messages and edit times.
|
|
444
|
+
* this interface.
|
|
445
|
+
*
|
|
446
|
+
* @remarks
|
|
447
|
+
* I'm really not convinced this is the right way to surface this information so
|
|
448
|
+
* I'm leaving this API "hidden".
|
|
449
|
+
*
|
|
450
|
+
* @hidden
|
|
451
|
+
*/
|
|
452
|
+
metadata(change?: string): A.DecodedChange | undefined {
|
|
453
|
+
if (!this.isReady()) {
|
|
454
|
+
return undefined
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!change) {
|
|
458
|
+
change = this.heads()![0]
|
|
459
|
+
}
|
|
460
|
+
// we return undefined instead of null by convention in this API
|
|
461
|
+
return (
|
|
462
|
+
A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
|
|
463
|
+
undefined
|
|
464
|
+
)
|
|
292
465
|
}
|
|
293
466
|
|
|
294
467
|
/**
|
|
@@ -314,13 +487,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
314
487
|
* Called by the repo either when a doc handle changes or we receive new remote heads.
|
|
315
488
|
* @hidden
|
|
316
489
|
*/
|
|
317
|
-
setRemoteHeads(storageId: StorageId, heads:
|
|
490
|
+
setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
|
|
318
491
|
this.#remoteHeads[storageId] = heads
|
|
319
492
|
this.emit("remote-heads", { storageId, heads })
|
|
320
493
|
}
|
|
321
494
|
|
|
322
495
|
/** Returns the heads of the storageId. */
|
|
323
|
-
getRemoteHeads(storageId: StorageId):
|
|
496
|
+
getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
|
|
324
497
|
return this.#remoteHeads[storageId]
|
|
325
498
|
}
|
|
326
499
|
|
|
@@ -345,6 +518,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
345
518
|
`DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
346
519
|
)
|
|
347
520
|
}
|
|
521
|
+
|
|
522
|
+
if (this.#fixedHeads) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
348
528
|
this.#machine.send({
|
|
349
529
|
type: UPDATE,
|
|
350
530
|
payload: { callback: doc => A.change(doc, options, callback) },
|
|
@@ -356,22 +536,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
356
536
|
* @returns A set of heads representing the concurrent change that was made.
|
|
357
537
|
*/
|
|
358
538
|
changeAt(
|
|
359
|
-
heads:
|
|
539
|
+
heads: UrlHeads,
|
|
360
540
|
callback: A.ChangeFn<T>,
|
|
361
541
|
options: A.ChangeOptions<T> = {}
|
|
362
|
-
):
|
|
542
|
+
): UrlHeads[] | undefined {
|
|
363
543
|
if (!this.isReady()) {
|
|
364
544
|
throw new Error(
|
|
365
545
|
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
366
546
|
)
|
|
367
547
|
}
|
|
368
|
-
|
|
548
|
+
if (this.#fixedHeads) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
let resultHeads: UrlHeads | undefined = undefined
|
|
369
554
|
this.#machine.send({
|
|
370
555
|
type: UPDATE,
|
|
371
556
|
payload: {
|
|
372
557
|
callback: doc => {
|
|
373
|
-
const result = A.changeAt(doc, heads, options, callback)
|
|
374
|
-
resultHeads = result.newHeads
|
|
558
|
+
const result = A.changeAt(doc, decodeHeads(heads), options, callback)
|
|
559
|
+
resultHeads = result.newHeads
|
|
560
|
+
? encodeHeads(result.newHeads)
|
|
561
|
+
: undefined
|
|
375
562
|
return result.newDoc
|
|
376
563
|
},
|
|
377
564
|
},
|
|
@@ -396,10 +583,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
396
583
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
397
584
|
throw new Error("Both handles must be ready to merge")
|
|
398
585
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
586
|
+
if (this.#fixedHeads) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
589
|
+
)
|
|
402
590
|
}
|
|
591
|
+
const mergingDoc = otherHandle.doc()
|
|
403
592
|
|
|
404
593
|
this.update(doc => {
|
|
405
594
|
return A.merge(doc, mergingDoc)
|
|
@@ -421,6 +610,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
421
610
|
if (this.#state === "loading") this.#machine.send({ type: REQUEST })
|
|
422
611
|
}
|
|
423
612
|
|
|
613
|
+
/** Called by the repo to free memory used by the document. */
|
|
614
|
+
unload() {
|
|
615
|
+
this.#machine.send({ type: UNLOAD })
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Called by the repo to reuse an unloaded handle. */
|
|
619
|
+
reload() {
|
|
620
|
+
this.#machine.send({ type: RELOAD })
|
|
621
|
+
}
|
|
622
|
+
|
|
424
623
|
/** Called by the repo when the document is deleted. */
|
|
425
624
|
delete() {
|
|
426
625
|
this.#machine.send({ type: DELETE })
|
|
@@ -439,6 +638,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
439
638
|
data: encode(message),
|
|
440
639
|
})
|
|
441
640
|
}
|
|
641
|
+
|
|
642
|
+
metrics(): { numOps: number; numChanges: number } {
|
|
643
|
+
return A.stats(this.#doc)
|
|
644
|
+
}
|
|
442
645
|
}
|
|
443
646
|
|
|
444
647
|
// TYPES
|
|
@@ -457,6 +660,9 @@ export type DocHandleOptions<T> =
|
|
|
457
660
|
| {
|
|
458
661
|
isNew?: false
|
|
459
662
|
|
|
663
|
+
// An optional point in time to lock the document to.
|
|
664
|
+
heads?: UrlHeads
|
|
665
|
+
|
|
460
666
|
/** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
|
|
461
667
|
timeoutDelay?: number
|
|
462
668
|
}
|
|
@@ -468,7 +674,6 @@ export interface DocHandleEvents<T> {
|
|
|
468
674
|
"heads-changed": (payload: DocHandleEncodedChangePayload<T>) => void
|
|
469
675
|
change: (payload: DocHandleChangePayload<T>) => void
|
|
470
676
|
delete: (payload: DocHandleDeletePayload<T>) => void
|
|
471
|
-
unavailable: (payload: DocHandleUnavailablePayload<T>) => void
|
|
472
677
|
"ephemeral-message": (payload: DocHandleEphemeralMessagePayload<T>) => void
|
|
473
678
|
"ephemeral-message-outbound": (
|
|
474
679
|
payload: DocHandleOutboundEphemeralMessagePayload<T>
|
|
@@ -520,7 +725,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
|
520
725
|
/** Emitted when we have new remote heads for this document */
|
|
521
726
|
export interface DocHandleRemoteHeadsPayload {
|
|
522
727
|
storageId: StorageId
|
|
523
|
-
heads:
|
|
728
|
+
heads: UrlHeads
|
|
524
729
|
}
|
|
525
730
|
|
|
526
731
|
// STATE MACHINE TYPES & CONSTANTS
|
|
@@ -539,6 +744,8 @@ export const HandleState = {
|
|
|
539
744
|
REQUESTING: "requesting",
|
|
540
745
|
/** The document is available */
|
|
541
746
|
READY: "ready",
|
|
747
|
+
/** The document has been unloaded from the handle, to free memory usage */
|
|
748
|
+
UNLOADED: "unloaded",
|
|
542
749
|
/** The document has been deleted from the repo */
|
|
543
750
|
DELETED: "deleted",
|
|
544
751
|
/** The document was not available in storage or from any connected peers */
|
|
@@ -546,8 +753,15 @@ export const HandleState = {
|
|
|
546
753
|
} as const
|
|
547
754
|
export type HandleState = (typeof HandleState)[keyof typeof HandleState]
|
|
548
755
|
|
|
549
|
-
export const {
|
|
550
|
-
|
|
756
|
+
export const {
|
|
757
|
+
IDLE,
|
|
758
|
+
LOADING,
|
|
759
|
+
REQUESTING,
|
|
760
|
+
READY,
|
|
761
|
+
UNLOADED,
|
|
762
|
+
DELETED,
|
|
763
|
+
UNAVAILABLE,
|
|
764
|
+
} = HandleState
|
|
551
765
|
|
|
552
766
|
// context
|
|
553
767
|
|
|
@@ -567,14 +781,18 @@ type DocHandleEvent<T> =
|
|
|
567
781
|
type: typeof UPDATE
|
|
568
782
|
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
|
|
569
783
|
}
|
|
570
|
-
| { type: typeof
|
|
784
|
+
| { type: typeof UNLOAD }
|
|
785
|
+
| { type: typeof RELOAD }
|
|
571
786
|
| { type: typeof DELETE }
|
|
787
|
+
| { type: typeof TIMEOUT }
|
|
572
788
|
| { type: typeof DOC_UNAVAILABLE }
|
|
573
789
|
|
|
574
790
|
const BEGIN = "BEGIN"
|
|
575
791
|
const REQUEST = "REQUEST"
|
|
576
792
|
const DOC_READY = "DOC_READY"
|
|
577
793
|
const UPDATE = "UPDATE"
|
|
794
|
+
const UNLOAD = "UNLOAD"
|
|
795
|
+
const RELOAD = "RELOAD"
|
|
578
796
|
const DELETE = "DELETE"
|
|
579
797
|
const TIMEOUT = "TIMEOUT"
|
|
580
798
|
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
|
}
|