@automerge/automerge-repo 2.0.0-alpha.2 → 2.0.0-alpha.20
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/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 +80 -8
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +181 -10
- 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 +35 -2
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +112 -70
- 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/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/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 +15 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +29 -8
- package/dist/synchronizer/DocSynchronizer.d.ts +7 -0
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +14 -0
- 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/package.json +3 -3
- package/src/AutomergeUrl.ts +101 -26
- package/src/DocHandle.ts +245 -20
- package/src/RemoteHeadsSubscriptions.ts +11 -9
- package/src/Repo.ts +163 -68
- package/src/entrypoints/fullfat.ts +1 -2
- package/src/helpers/bufferFromHex.ts +14 -0
- package/src/helpers/headsAreSame.ts +2 -2
- 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 +42 -9
- package/src/synchronizer/DocSynchronizer.ts +15 -0
- package/src/synchronizer/Synchronizer.ts +14 -0
- package/src/types.ts +4 -1
- package/test/AutomergeUrl.test.ts +130 -0
- package/test/DocHandle.test.ts +209 -2
- package/test/DocSynchronizer.test.ts +10 -3
- package/test/Repo.test.ts +228 -3
- package/test/StorageSubsystem.test.ts +17 -0
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,6 +83,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
72
83
|
this.emit("delete", { handle: this })
|
|
73
84
|
return { doc: A.init() }
|
|
74
85
|
}),
|
|
86
|
+
onUnload: assign(() => {
|
|
87
|
+
return { doc: A.init() }
|
|
88
|
+
}),
|
|
75
89
|
onUnavailable: () => {
|
|
76
90
|
this.emit("unavailable", { handle: this })
|
|
77
91
|
},
|
|
@@ -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
|
*
|
|
@@ -257,6 +292,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
257
292
|
// if we timed out, return undefined
|
|
258
293
|
return undefined
|
|
259
294
|
}
|
|
295
|
+
// If we have fixed heads, return a view at those heads
|
|
296
|
+
if (this.#fixedHeads) {
|
|
297
|
+
const doc = this.#doc
|
|
298
|
+
if (!doc || this.isUnavailable()) return undefined
|
|
299
|
+
return A.view(doc, decodeHeads(this.#fixedHeads))
|
|
300
|
+
}
|
|
260
301
|
// Return the document
|
|
261
302
|
return !this.isUnavailable() ? this.#doc : undefined
|
|
262
303
|
}
|
|
@@ -276,7 +317,11 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
276
317
|
*/
|
|
277
318
|
docSync() {
|
|
278
319
|
if (!this.isReady()) return undefined
|
|
279
|
-
|
|
320
|
+
if (this.#fixedHeads) {
|
|
321
|
+
const doc = this.#doc
|
|
322
|
+
return doc ? A.view(doc, decodeHeads(this.#fixedHeads)) : undefined
|
|
323
|
+
}
|
|
324
|
+
return this.#doc
|
|
280
325
|
}
|
|
281
326
|
|
|
282
327
|
/**
|
|
@@ -284,11 +329,142 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
284
329
|
* This precisely defines the state of a document.
|
|
285
330
|
* @returns the current document's heads, or undefined if the document is not ready
|
|
286
331
|
*/
|
|
287
|
-
heads():
|
|
332
|
+
heads(): UrlHeads | undefined {
|
|
333
|
+
if (!this.isReady()) return undefined
|
|
334
|
+
if (this.#fixedHeads) {
|
|
335
|
+
return this.#fixedHeads
|
|
336
|
+
}
|
|
337
|
+
return encodeHeads(A.getHeads(this.#doc))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
begin() {
|
|
341
|
+
this.#machine.send({ type: BEGIN })
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Returns an array of all past "heads" for the document in topological order.
|
|
346
|
+
*
|
|
347
|
+
* @remarks
|
|
348
|
+
* A point-in-time in an automerge document is an *array* of heads since there may be
|
|
349
|
+
* concurrent edits. This API just returns a topologically sorted history of all edits
|
|
350
|
+
* so every previous entry will be (in some sense) before later ones, but the set of all possible
|
|
351
|
+
* history views would be quite large under concurrency (every thing in each branch against each other).
|
|
352
|
+
* There might be a clever way to think about this, but we haven't found it yet, so for now at least
|
|
353
|
+
* we present a single traversable view which excludes concurrency.
|
|
354
|
+
* @returns UrlHeads[] - The individual heads for every change in the document. Each item is a tagged string[1].
|
|
355
|
+
*/
|
|
356
|
+
history(): UrlHeads[] | undefined {
|
|
288
357
|
if (!this.isReady()) {
|
|
289
358
|
return undefined
|
|
290
359
|
}
|
|
291
|
-
|
|
360
|
+
// This just returns all the heads as individual strings.
|
|
361
|
+
|
|
362
|
+
return A.topoHistoryTraversal(this.#doc).map(h =>
|
|
363
|
+
encodeHeads([h])
|
|
364
|
+
) as UrlHeads[]
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Creates a new DocHandle with a fixed "view" at the given point in time represented
|
|
369
|
+
* by the `heads` passed in. The return value is the same type as docSync() and will return
|
|
370
|
+
* undefined if the object hasn't finished loading.
|
|
371
|
+
*
|
|
372
|
+
* @remarks
|
|
373
|
+
* Note that our Typescript types do not consider change over time and the current version
|
|
374
|
+
* of Automerge doesn't check types at runtime, so if you go back to an old set of heads
|
|
375
|
+
* that doesn't match the heads here, Typescript will not save you.
|
|
376
|
+
*
|
|
377
|
+
* @argument heads - The heads to view the document at. See history().
|
|
378
|
+
* @returns DocHandle<T> at the time of `heads`
|
|
379
|
+
*/
|
|
380
|
+
view(heads: UrlHeads): DocHandle<T> {
|
|
381
|
+
if (!this.isReady()) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
// Create a new handle with the same documentId but fixed heads
|
|
387
|
+
const handle = new DocHandle<T>(this.documentId, {
|
|
388
|
+
heads,
|
|
389
|
+
timeoutDelay: this.#timeoutDelay,
|
|
390
|
+
})
|
|
391
|
+
handle.update(() => A.clone(this.#doc))
|
|
392
|
+
handle.doneLoading()
|
|
393
|
+
|
|
394
|
+
return handle
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Returns a set of Patch operations that will move a materialized document from one state to another
|
|
399
|
+
* if applied.
|
|
400
|
+
*
|
|
401
|
+
* @remarks
|
|
402
|
+
* We allow specifying either:
|
|
403
|
+
* - Two sets of heads to compare directly
|
|
404
|
+
* - A single set of heads to compare against our current heads
|
|
405
|
+
* - Another DocHandle to compare against (which must share history with this document)
|
|
406
|
+
*
|
|
407
|
+
* @throws Error if the documents don't share history or if either document is not ready
|
|
408
|
+
* @returns Automerge patches that go from one document state to the other
|
|
409
|
+
*/
|
|
410
|
+
diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
|
|
411
|
+
if (!this.isReady()) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const doc = this.#doc
|
|
418
|
+
if (!doc) throw new Error("Document not available")
|
|
419
|
+
|
|
420
|
+
// If first argument is a DocHandle
|
|
421
|
+
if (first instanceof DocHandle) {
|
|
422
|
+
if (!first.isReady()) {
|
|
423
|
+
throw new Error("Cannot diff against a handle that isn't ready")
|
|
424
|
+
}
|
|
425
|
+
const otherHeads = first.heads()
|
|
426
|
+
if (!otherHeads) throw new Error("Other document's heads not available")
|
|
427
|
+
|
|
428
|
+
// Create a temporary merged doc to verify shared history and compute diff
|
|
429
|
+
const mergedDoc = A.merge(A.clone(doc), first.docSync()!)
|
|
430
|
+
// Use the merged doc to compute the diff
|
|
431
|
+
return A.diff(
|
|
432
|
+
mergedDoc,
|
|
433
|
+
decodeHeads(this.heads()!),
|
|
434
|
+
decodeHeads(otherHeads)
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Otherwise treat as heads
|
|
439
|
+
const from = second ? first : ((this.heads() || []) as UrlHeads)
|
|
440
|
+
const to = second ? second : first
|
|
441
|
+
return A.diff(doc, decodeHeads(from), decodeHeads(to))
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* `metadata(head?)` allows you to look at the metadata for a change
|
|
446
|
+
* this can be used to build history graphs to find commit messages and edit times.
|
|
447
|
+
* this interface.
|
|
448
|
+
*
|
|
449
|
+
* @remarks
|
|
450
|
+
* I'm really not convinced this is the right way to surface this information so
|
|
451
|
+
* I'm leaving this API "hidden".
|
|
452
|
+
*
|
|
453
|
+
* @hidden
|
|
454
|
+
*/
|
|
455
|
+
metadata(change?: string): A.DecodedChange | undefined {
|
|
456
|
+
if (!this.isReady()) {
|
|
457
|
+
return undefined
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!change) {
|
|
461
|
+
change = this.heads()![0]
|
|
462
|
+
}
|
|
463
|
+
// we return undefined instead of null by convention in this API
|
|
464
|
+
return (
|
|
465
|
+
A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
|
|
466
|
+
undefined
|
|
467
|
+
)
|
|
292
468
|
}
|
|
293
469
|
|
|
294
470
|
/**
|
|
@@ -314,13 +490,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
314
490
|
* Called by the repo either when a doc handle changes or we receive new remote heads.
|
|
315
491
|
* @hidden
|
|
316
492
|
*/
|
|
317
|
-
setRemoteHeads(storageId: StorageId, heads:
|
|
493
|
+
setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
|
|
318
494
|
this.#remoteHeads[storageId] = heads
|
|
319
495
|
this.emit("remote-heads", { storageId, heads })
|
|
320
496
|
}
|
|
321
497
|
|
|
322
498
|
/** Returns the heads of the storageId. */
|
|
323
|
-
getRemoteHeads(storageId: StorageId):
|
|
499
|
+
getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
|
|
324
500
|
return this.#remoteHeads[storageId]
|
|
325
501
|
}
|
|
326
502
|
|
|
@@ -345,6 +521,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
345
521
|
`DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
346
522
|
)
|
|
347
523
|
}
|
|
524
|
+
|
|
525
|
+
if (this.#fixedHeads) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
348
531
|
this.#machine.send({
|
|
349
532
|
type: UPDATE,
|
|
350
533
|
payload: { callback: doc => A.change(doc, options, callback) },
|
|
@@ -356,22 +539,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
356
539
|
* @returns A set of heads representing the concurrent change that was made.
|
|
357
540
|
*/
|
|
358
541
|
changeAt(
|
|
359
|
-
heads:
|
|
542
|
+
heads: UrlHeads,
|
|
360
543
|
callback: A.ChangeFn<T>,
|
|
361
544
|
options: A.ChangeOptions<T> = {}
|
|
362
|
-
):
|
|
545
|
+
): UrlHeads[] | undefined {
|
|
363
546
|
if (!this.isReady()) {
|
|
364
547
|
throw new Error(
|
|
365
548
|
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
366
549
|
)
|
|
367
550
|
}
|
|
368
|
-
|
|
551
|
+
if (this.#fixedHeads) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
let resultHeads: UrlHeads | undefined = undefined
|
|
369
557
|
this.#machine.send({
|
|
370
558
|
type: UPDATE,
|
|
371
559
|
payload: {
|
|
372
560
|
callback: doc => {
|
|
373
|
-
const result = A.changeAt(doc, heads, options, callback)
|
|
374
|
-
resultHeads = result.newHeads
|
|
561
|
+
const result = A.changeAt(doc, decodeHeads(heads), options, callback)
|
|
562
|
+
resultHeads = result.newHeads
|
|
563
|
+
? encodeHeads(result.newHeads)
|
|
564
|
+
: undefined
|
|
375
565
|
return result.newDoc
|
|
376
566
|
},
|
|
377
567
|
},
|
|
@@ -396,6 +586,11 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
396
586
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
397
587
|
throw new Error("Both handles must be ready to merge")
|
|
398
588
|
}
|
|
589
|
+
if (this.#fixedHeads) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
592
|
+
)
|
|
593
|
+
}
|
|
399
594
|
const mergingDoc = otherHandle.docSync()
|
|
400
595
|
if (!mergingDoc) {
|
|
401
596
|
throw new Error("The document to be merged in is falsy, aborting.")
|
|
@@ -421,6 +616,16 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
421
616
|
if (this.#state === "loading") this.#machine.send({ type: REQUEST })
|
|
422
617
|
}
|
|
423
618
|
|
|
619
|
+
/** Called by the repo to free memory used by the document. */
|
|
620
|
+
unload() {
|
|
621
|
+
this.#machine.send({ type: UNLOAD })
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Called by the repo to reuse an unloaded handle. */
|
|
625
|
+
reload() {
|
|
626
|
+
this.#machine.send({ type: RELOAD })
|
|
627
|
+
}
|
|
628
|
+
|
|
424
629
|
/** Called by the repo when the document is deleted. */
|
|
425
630
|
delete() {
|
|
426
631
|
this.#machine.send({ type: DELETE })
|
|
@@ -439,6 +644,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
439
644
|
data: encode(message),
|
|
440
645
|
})
|
|
441
646
|
}
|
|
647
|
+
|
|
648
|
+
metrics(): { numOps: number; numChanges: number } {
|
|
649
|
+
return A.stats(this.#doc)
|
|
650
|
+
}
|
|
442
651
|
}
|
|
443
652
|
|
|
444
653
|
// TYPES
|
|
@@ -457,6 +666,9 @@ export type DocHandleOptions<T> =
|
|
|
457
666
|
| {
|
|
458
667
|
isNew?: false
|
|
459
668
|
|
|
669
|
+
// An optional point in time to lock the document to.
|
|
670
|
+
heads?: UrlHeads
|
|
671
|
+
|
|
460
672
|
/** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
|
|
461
673
|
timeoutDelay?: number
|
|
462
674
|
}
|
|
@@ -520,7 +732,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
|
520
732
|
/** Emitted when we have new remote heads for this document */
|
|
521
733
|
export interface DocHandleRemoteHeadsPayload {
|
|
522
734
|
storageId: StorageId
|
|
523
|
-
heads:
|
|
735
|
+
heads: UrlHeads
|
|
524
736
|
}
|
|
525
737
|
|
|
526
738
|
// STATE MACHINE TYPES & CONSTANTS
|
|
@@ -539,6 +751,8 @@ export const HandleState = {
|
|
|
539
751
|
REQUESTING: "requesting",
|
|
540
752
|
/** The document is available */
|
|
541
753
|
READY: "ready",
|
|
754
|
+
/** The document has been unloaded from the handle, to free memory usage */
|
|
755
|
+
UNLOADED: "unloaded",
|
|
542
756
|
/** The document has been deleted from the repo */
|
|
543
757
|
DELETED: "deleted",
|
|
544
758
|
/** The document was not available in storage or from any connected peers */
|
|
@@ -546,8 +760,15 @@ export const HandleState = {
|
|
|
546
760
|
} as const
|
|
547
761
|
export type HandleState = (typeof HandleState)[keyof typeof HandleState]
|
|
548
762
|
|
|
549
|
-
export const {
|
|
550
|
-
|
|
763
|
+
export const {
|
|
764
|
+
IDLE,
|
|
765
|
+
LOADING,
|
|
766
|
+
REQUESTING,
|
|
767
|
+
READY,
|
|
768
|
+
UNLOADED,
|
|
769
|
+
DELETED,
|
|
770
|
+
UNAVAILABLE,
|
|
771
|
+
} = HandleState
|
|
551
772
|
|
|
552
773
|
// context
|
|
553
774
|
|
|
@@ -567,14 +788,18 @@ type DocHandleEvent<T> =
|
|
|
567
788
|
type: typeof UPDATE
|
|
568
789
|
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
|
|
569
790
|
}
|
|
570
|
-
| { type: typeof
|
|
791
|
+
| { type: typeof UNLOAD }
|
|
792
|
+
| { type: typeof RELOAD }
|
|
571
793
|
| { type: typeof DELETE }
|
|
794
|
+
| { type: typeof TIMEOUT }
|
|
572
795
|
| { type: typeof DOC_UNAVAILABLE }
|
|
573
796
|
|
|
574
797
|
const BEGIN = "BEGIN"
|
|
575
798
|
const REQUEST = "REQUEST"
|
|
576
799
|
const DOC_READY = "DOC_READY"
|
|
577
800
|
const UPDATE = "UPDATE"
|
|
801
|
+
const UNLOAD = "UNLOAD"
|
|
802
|
+
const RELOAD = "RELOAD"
|
|
578
803
|
const DELETE = "DELETE"
|
|
579
804
|
const TIMEOUT = "TIMEOUT"
|
|
580
805
|
const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
|
|
@@ -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
|
}
|