@automerge/automerge-repo 2.0.0-alpha.14 → 2.0.0-alpha.17
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 +19 -4
- package/dist/AutomergeUrl.d.ts.map +1 -1
- package/dist/AutomergeUrl.js +71 -24
- package/dist/DocHandle.d.ts +21 -17
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +83 -26
- package/dist/RemoteHeadsSubscriptions.d.ts +4 -4
- package/dist/RemoteHeadsSubscriptions.d.ts.map +1 -1
- package/dist/RemoteHeadsSubscriptions.js +4 -1
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +17 -12
- 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.map +1 -1
- package/dist/helpers/tests/storage-adapter-tests.js +6 -9
- package/dist/storage/StorageSubsystem.d.ts.map +1 -1
- package/dist/storage/StorageSubsystem.js +2 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/AutomergeUrl.ts +103 -26
- package/src/DocHandle.ts +130 -37
- package/src/RemoteHeadsSubscriptions.ts +11 -8
- package/src/Repo.ts +22 -11
- package/src/helpers/bufferFromHex.ts +14 -0
- package/src/helpers/headsAreSame.ts +2 -2
- package/src/helpers/tests/storage-adapter-tests.ts +13 -24
- package/src/storage/StorageSubsystem.ts +3 -1
- package/src/types.ts +4 -1
- package/test/AutomergeUrl.test.ts +130 -0
- package/test/DocHandle.test.ts +70 -4
- package/test/DocSynchronizer.test.ts +10 -3
- package/test/Repo.test.ts +117 -3
package/src/DocHandle.ts
CHANGED
|
@@ -2,7 +2,12 @@ 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
|
+
UrlHeads,
|
|
10
|
+
} from "./AutomergeUrl.js"
|
|
6
11
|
import { encode } from "./helpers/cbor.js"
|
|
7
12
|
import { headsAreSame } from "./helpers/headsAreSame.js"
|
|
8
13
|
import { withTimeout } from "./helpers/withTimeout.js"
|
|
@@ -28,6 +33,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
28
33
|
/** The XState actor running our state machine. */
|
|
29
34
|
#machine
|
|
30
35
|
|
|
36
|
+
/** If set, this handle will only show the document at these heads */
|
|
37
|
+
#fixedHeads?: UrlHeads
|
|
38
|
+
|
|
31
39
|
/** The last known state of our document. */
|
|
32
40
|
#prevDocState: T = A.init<T>()
|
|
33
41
|
|
|
@@ -36,7 +44,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
36
44
|
#timeoutDelay = 60_000
|
|
37
45
|
|
|
38
46
|
/** A dictionary mapping each peer to the last heads we know they have. */
|
|
39
|
-
#remoteHeads: Record<StorageId,
|
|
47
|
+
#remoteHeads: Record<StorageId, UrlHeads> = {}
|
|
40
48
|
|
|
41
49
|
/** @hidden */
|
|
42
50
|
constructor(
|
|
@@ -49,6 +57,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
49
57
|
this.#timeoutDelay = options.timeoutDelay
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
if ("heads" in options) {
|
|
61
|
+
this.#fixedHeads = options.heads
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
const doc = A.init<T>()
|
|
53
65
|
|
|
54
66
|
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)
|
|
@@ -176,7 +188,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
176
188
|
#checkForChanges(before: A.Doc<T>, after: A.Doc<T>) {
|
|
177
189
|
const beforeHeads = A.getHeads(before)
|
|
178
190
|
const afterHeads = A.getHeads(after)
|
|
179
|
-
const docChanged = !headsAreSame(
|
|
191
|
+
const docChanged = !headsAreSame(
|
|
192
|
+
encodeHeads(afterHeads),
|
|
193
|
+
encodeHeads(beforeHeads)
|
|
194
|
+
)
|
|
180
195
|
if (docChanged) {
|
|
181
196
|
this.emit("heads-changed", { handle: this, doc: after })
|
|
182
197
|
|
|
@@ -202,7 +217,10 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
202
217
|
/** Our documentId in Automerge URL form.
|
|
203
218
|
*/
|
|
204
219
|
get url(): AutomergeUrl {
|
|
205
|
-
return stringifyAutomergeUrl({
|
|
220
|
+
return stringifyAutomergeUrl({
|
|
221
|
+
documentId: this.documentId,
|
|
222
|
+
heads: this.#fixedHeads,
|
|
223
|
+
})
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
/**
|
|
@@ -275,6 +293,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
275
293
|
// if we timed out, return undefined
|
|
276
294
|
return undefined
|
|
277
295
|
}
|
|
296
|
+
// If we have fixed heads, return a view at those heads
|
|
297
|
+
if (this.#fixedHeads) {
|
|
298
|
+
const doc = this.#doc
|
|
299
|
+
if (!doc || this.isUnavailable()) return undefined
|
|
300
|
+
return A.view(doc, decodeHeads(this.#fixedHeads))
|
|
301
|
+
}
|
|
278
302
|
// Return the document
|
|
279
303
|
return !this.isUnavailable() ? this.#doc : undefined
|
|
280
304
|
}
|
|
@@ -294,7 +318,11 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
294
318
|
*/
|
|
295
319
|
docSync() {
|
|
296
320
|
if (!this.isReady()) return undefined
|
|
297
|
-
|
|
321
|
+
if (this.#fixedHeads) {
|
|
322
|
+
const doc = this.#doc
|
|
323
|
+
return doc ? A.view(doc, decodeHeads(this.#fixedHeads)) : undefined
|
|
324
|
+
}
|
|
325
|
+
return this.#doc
|
|
298
326
|
}
|
|
299
327
|
|
|
300
328
|
/**
|
|
@@ -302,11 +330,12 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
302
330
|
* This precisely defines the state of a document.
|
|
303
331
|
* @returns the current document's heads, or undefined if the document is not ready
|
|
304
332
|
*/
|
|
305
|
-
heads():
|
|
306
|
-
if (!this.isReady())
|
|
307
|
-
|
|
333
|
+
heads(): UrlHeads | undefined {
|
|
334
|
+
if (!this.isReady()) return undefined
|
|
335
|
+
if (this.#fixedHeads) {
|
|
336
|
+
return this.#fixedHeads
|
|
308
337
|
}
|
|
309
|
-
return A.getHeads(this.#doc)
|
|
338
|
+
return encodeHeads(A.getHeads(this.#doc))
|
|
310
339
|
}
|
|
311
340
|
|
|
312
341
|
begin() {
|
|
@@ -314,9 +343,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
314
343
|
}
|
|
315
344
|
|
|
316
345
|
/**
|
|
317
|
-
*
|
|
318
|
-
* by the `heads` passed in. The return value is the same type as docSync() and will return
|
|
319
|
-
* undefined if the object hasn't finished loading.
|
|
346
|
+
* Returns an array of all past "heads" for the document in topological order.
|
|
320
347
|
*
|
|
321
348
|
* @remarks
|
|
322
349
|
* A point-in-time in an automerge document is an *array* of heads since there may be
|
|
@@ -325,19 +352,21 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
325
352
|
* history views would be quite large under concurrency (every thing in each branch against each other).
|
|
326
353
|
* There might be a clever way to think about this, but we haven't found it yet, so for now at least
|
|
327
354
|
* we present a single traversable view which excludes concurrency.
|
|
328
|
-
* @returns The individual heads for every change in the document.
|
|
355
|
+
* @returns UrlHeads[] - The individual heads for every change in the document. Each item is a tagged string[1].
|
|
329
356
|
*/
|
|
330
|
-
history():
|
|
357
|
+
history(): UrlHeads[] | undefined {
|
|
331
358
|
if (!this.isReady()) {
|
|
332
359
|
return undefined
|
|
333
360
|
}
|
|
334
361
|
// This just returns all the heads as individual strings.
|
|
335
362
|
|
|
336
|
-
return A.topoHistoryTraversal(this.#doc).map(h =>
|
|
363
|
+
return A.topoHistoryTraversal(this.#doc).map(h =>
|
|
364
|
+
encodeHeads([h])
|
|
365
|
+
) as UrlHeads[]
|
|
337
366
|
}
|
|
338
367
|
|
|
339
368
|
/**
|
|
340
|
-
* Creates a fixed "view"
|
|
369
|
+
* Creates a new DocHandle with a fixed "view" at the given point in time represented
|
|
341
370
|
* by the `heads` passed in. The return value is the same type as docSync() and will return
|
|
342
371
|
* undefined if the object hasn't finished loading.
|
|
343
372
|
*
|
|
@@ -346,13 +375,24 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
346
375
|
* of Automerge doesn't check types at runtime, so if you go back to an old set of heads
|
|
347
376
|
* that doesn't match the heads here, Typescript will not save you.
|
|
348
377
|
*
|
|
349
|
-
* @
|
|
378
|
+
* @argument heads - The heads to view the document at. See history().
|
|
379
|
+
* @returns DocHandle<T> at the time of `heads`
|
|
350
380
|
*/
|
|
351
|
-
view(heads:
|
|
381
|
+
view(heads: UrlHeads): DocHandle<T> {
|
|
352
382
|
if (!this.isReady()) {
|
|
353
|
-
|
|
383
|
+
throw new Error(
|
|
384
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling view().`
|
|
385
|
+
)
|
|
354
386
|
}
|
|
355
|
-
|
|
387
|
+
// Create a new handle with the same documentId but fixed heads
|
|
388
|
+
const handle = new DocHandle<T>(this.documentId, {
|
|
389
|
+
heads,
|
|
390
|
+
timeoutDelay: this.#timeoutDelay,
|
|
391
|
+
})
|
|
392
|
+
handle.update(() => A.clone(this.#doc))
|
|
393
|
+
handle.doneLoading()
|
|
394
|
+
|
|
395
|
+
return handle
|
|
356
396
|
}
|
|
357
397
|
|
|
358
398
|
/**
|
|
@@ -360,19 +400,46 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
360
400
|
* if applied.
|
|
361
401
|
*
|
|
362
402
|
* @remarks
|
|
363
|
-
* We allow specifying
|
|
364
|
-
*
|
|
403
|
+
* We allow specifying either:
|
|
404
|
+
* - Two sets of heads to compare directly
|
|
405
|
+
* - A single set of heads to compare against our current heads
|
|
406
|
+
* - Another DocHandle to compare against (which must share history with this document)
|
|
365
407
|
*
|
|
366
|
-
* @
|
|
408
|
+
* @throws Error if the documents don't share history or if either document is not ready
|
|
409
|
+
* @returns Automerge patches that go from one document state to the other
|
|
367
410
|
*/
|
|
368
|
-
diff(first:
|
|
411
|
+
diff(first: UrlHeads | DocHandle<T>, second?: UrlHeads): A.Patch[] {
|
|
369
412
|
if (!this.isReady()) {
|
|
370
|
-
|
|
413
|
+
throw new Error(
|
|
414
|
+
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before calling diff().`
|
|
415
|
+
)
|
|
371
416
|
}
|
|
372
|
-
|
|
373
|
-
const
|
|
417
|
+
|
|
418
|
+
const doc = this.#doc
|
|
419
|
+
if (!doc) throw new Error("Document not available")
|
|
420
|
+
|
|
421
|
+
// If first argument is a DocHandle
|
|
422
|
+
if (first instanceof DocHandle) {
|
|
423
|
+
if (!first.isReady()) {
|
|
424
|
+
throw new Error("Cannot diff against a handle that isn't ready")
|
|
425
|
+
}
|
|
426
|
+
const otherHeads = first.heads()
|
|
427
|
+
if (!otherHeads) throw new Error("Other document's heads not available")
|
|
428
|
+
|
|
429
|
+
// Create a temporary merged doc to verify shared history and compute diff
|
|
430
|
+
const mergedDoc = A.merge(A.clone(doc), first.docSync()!)
|
|
431
|
+
// Use the merged doc to compute the diff
|
|
432
|
+
return A.diff(
|
|
433
|
+
mergedDoc,
|
|
434
|
+
decodeHeads(this.heads()!),
|
|
435
|
+
decodeHeads(otherHeads)
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Otherwise treat as heads
|
|
440
|
+
const from = second ? first : ((this.heads() || []) as UrlHeads)
|
|
374
441
|
const to = second ? second : first
|
|
375
|
-
return A.diff(
|
|
442
|
+
return A.diff(doc, decodeHeads(from), decodeHeads(to))
|
|
376
443
|
}
|
|
377
444
|
|
|
378
445
|
/**
|
|
@@ -390,11 +457,15 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
390
457
|
if (!this.isReady()) {
|
|
391
458
|
return undefined
|
|
392
459
|
}
|
|
460
|
+
|
|
393
461
|
if (!change) {
|
|
394
462
|
change = this.heads()![0]
|
|
395
463
|
}
|
|
396
464
|
// we return undefined instead of null by convention in this API
|
|
397
|
-
return
|
|
465
|
+
return (
|
|
466
|
+
A.inspectChange(this.#doc, decodeHeads([change] as UrlHeads)[0]) ||
|
|
467
|
+
undefined
|
|
468
|
+
)
|
|
398
469
|
}
|
|
399
470
|
|
|
400
471
|
/**
|
|
@@ -420,13 +491,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
420
491
|
* Called by the repo either when a doc handle changes or we receive new remote heads.
|
|
421
492
|
* @hidden
|
|
422
493
|
*/
|
|
423
|
-
setRemoteHeads(storageId: StorageId, heads:
|
|
494
|
+
setRemoteHeads(storageId: StorageId, heads: UrlHeads) {
|
|
424
495
|
this.#remoteHeads[storageId] = heads
|
|
425
496
|
this.emit("remote-heads", { storageId, heads })
|
|
426
497
|
}
|
|
427
498
|
|
|
428
499
|
/** Returns the heads of the storageId. */
|
|
429
|
-
getRemoteHeads(storageId: StorageId):
|
|
500
|
+
getRemoteHeads(storageId: StorageId): UrlHeads | undefined {
|
|
430
501
|
return this.#remoteHeads[storageId]
|
|
431
502
|
}
|
|
432
503
|
|
|
@@ -451,6 +522,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
451
522
|
`DocHandle#${this.documentId} is in ${this.state} and not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
452
523
|
)
|
|
453
524
|
}
|
|
525
|
+
|
|
526
|
+
if (this.#fixedHeads) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
454
532
|
this.#machine.send({
|
|
455
533
|
type: UPDATE,
|
|
456
534
|
payload: { callback: doc => A.change(doc, options, callback) },
|
|
@@ -462,22 +540,29 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
462
540
|
* @returns A set of heads representing the concurrent change that was made.
|
|
463
541
|
*/
|
|
464
542
|
changeAt(
|
|
465
|
-
heads:
|
|
543
|
+
heads: UrlHeads,
|
|
466
544
|
callback: A.ChangeFn<T>,
|
|
467
545
|
options: A.ChangeOptions<T> = {}
|
|
468
|
-
):
|
|
546
|
+
): UrlHeads[] | undefined {
|
|
469
547
|
if (!this.isReady()) {
|
|
470
548
|
throw new Error(
|
|
471
549
|
`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`
|
|
472
550
|
)
|
|
473
551
|
}
|
|
474
|
-
|
|
552
|
+
if (this.#fixedHeads) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
let resultHeads: UrlHeads | undefined = undefined
|
|
475
558
|
this.#machine.send({
|
|
476
559
|
type: UPDATE,
|
|
477
560
|
payload: {
|
|
478
561
|
callback: doc => {
|
|
479
|
-
const result = A.changeAt(doc, heads, options, callback)
|
|
480
|
-
resultHeads = result.newHeads
|
|
562
|
+
const result = A.changeAt(doc, decodeHeads(heads), options, callback)
|
|
563
|
+
resultHeads = result.newHeads
|
|
564
|
+
? encodeHeads(result.newHeads)
|
|
565
|
+
: undefined
|
|
481
566
|
return result.newDoc
|
|
482
567
|
},
|
|
483
568
|
},
|
|
@@ -502,6 +587,11 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
|
|
|
502
587
|
if (!this.isReady() || !otherHandle.isReady()) {
|
|
503
588
|
throw new Error("Both handles must be ready to merge")
|
|
504
589
|
}
|
|
590
|
+
if (this.#fixedHeads) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`DocHandle#${this.documentId} is in view-only mode at specific heads. Use clone() to create a new document from this state.`
|
|
593
|
+
)
|
|
594
|
+
}
|
|
505
595
|
const mergingDoc = otherHandle.docSync()
|
|
506
596
|
if (!mergingDoc) {
|
|
507
597
|
throw new Error("The document to be merged in is falsy, aborting.")
|
|
@@ -577,6 +667,9 @@ export type DocHandleOptions<T> =
|
|
|
577
667
|
| {
|
|
578
668
|
isNew?: false
|
|
579
669
|
|
|
670
|
+
// An optional point in time to lock the document to.
|
|
671
|
+
heads?: UrlHeads
|
|
672
|
+
|
|
580
673
|
/** The number of milliseconds before we mark this document as unavailable if we don't have it and nobody shares it with us. */
|
|
581
674
|
timeoutDelay?: number
|
|
582
675
|
}
|
|
@@ -640,7 +733,7 @@ export interface DocHandleOutboundEphemeralMessagePayload<T> {
|
|
|
640
733
|
/** Emitted when we have new remote heads for this document */
|
|
641
734
|
export interface DocHandleRemoteHeadsPayload {
|
|
642
735
|
storageId: StorageId
|
|
643
|
-
heads:
|
|
736
|
+
heads: UrlHeads
|
|
644
737
|
}
|
|
645
738
|
|
|
646
739
|
// STATE MACHINE TYPES & CONSTANTS
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { next as A } from "@automerge/automerge/slim"
|
|
2
1
|
import { EventEmitter } from "eventemitter3"
|
|
3
2
|
import { DocumentId, PeerId } from "./types.js"
|
|
4
3
|
import {
|
|
@@ -7,12 +6,13 @@ import {
|
|
|
7
6
|
} from "./network/messages.js"
|
|
8
7
|
import { StorageId } from "./index.js"
|
|
9
8
|
import debug from "debug"
|
|
9
|
+
import { UrlHeads } from "./AutomergeUrl.js"
|
|
10
10
|
|
|
11
11
|
// Notify a DocHandle that remote heads have changed
|
|
12
12
|
export type RemoteHeadsSubscriptionEventPayload = {
|
|
13
13
|
documentId: DocumentId
|
|
14
14
|
storageId: StorageId
|
|
15
|
-
remoteHeads:
|
|
15
|
+
remoteHeads: UrlHeads
|
|
16
16
|
timestamp: number
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -21,7 +21,7 @@ export type NotifyRemoteHeadsPayload = {
|
|
|
21
21
|
targetId: PeerId
|
|
22
22
|
documentId: DocumentId
|
|
23
23
|
storageId: StorageId
|
|
24
|
-
heads:
|
|
24
|
+
heads: UrlHeads
|
|
25
25
|
timestamp: number
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -216,7 +216,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
216
216
|
handleImmediateRemoteHeadsChanged(
|
|
217
217
|
documentId: DocumentId,
|
|
218
218
|
storageId: StorageId,
|
|
219
|
-
heads:
|
|
219
|
+
heads: UrlHeads
|
|
220
220
|
) {
|
|
221
221
|
this.#log("handleLocalHeadsChanged", documentId, storageId, heads)
|
|
222
222
|
const remote = this.#knownHeads.get(documentId)
|
|
@@ -334,7 +334,7 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
334
334
|
#changedHeads(msg: RemoteHeadsChanged): {
|
|
335
335
|
documentId: DocumentId
|
|
336
336
|
storageId: StorageId
|
|
337
|
-
remoteHeads:
|
|
337
|
+
remoteHeads: UrlHeads
|
|
338
338
|
timestamp: number
|
|
339
339
|
}[] {
|
|
340
340
|
const changedHeads = []
|
|
@@ -356,11 +356,14 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
356
356
|
if (docRemote && docRemote.timestamp >= timestamp) {
|
|
357
357
|
continue
|
|
358
358
|
} else {
|
|
359
|
-
remote.set(storageId as StorageId, {
|
|
359
|
+
remote.set(storageId as StorageId, {
|
|
360
|
+
timestamp,
|
|
361
|
+
heads: heads as UrlHeads,
|
|
362
|
+
})
|
|
360
363
|
changedHeads.push({
|
|
361
364
|
documentId,
|
|
362
365
|
storageId: storageId as StorageId,
|
|
363
|
-
remoteHeads: heads,
|
|
366
|
+
remoteHeads: heads as UrlHeads,
|
|
364
367
|
timestamp,
|
|
365
368
|
})
|
|
366
369
|
}
|
|
@@ -371,5 +374,5 @@ export class RemoteHeadsSubscriptions extends EventEmitter<RemoteHeadsSubscripti
|
|
|
371
374
|
|
|
372
375
|
type LastHeads = {
|
|
373
376
|
timestamp: number
|
|
374
|
-
heads:
|
|
377
|
+
heads: UrlHeads
|
|
375
378
|
}
|
package/src/Repo.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { next as Automerge } from "@automerge/automerge/slim"
|
|
|
2
2
|
import debug from "debug"
|
|
3
3
|
import { EventEmitter } from "eventemitter3"
|
|
4
4
|
import {
|
|
5
|
+
encodeHeads,
|
|
5
6
|
generateAutomergeUrl,
|
|
6
7
|
interpretAsDocumentId,
|
|
8
|
+
isValidAutomergeUrl,
|
|
7
9
|
parseAutomergeUrl,
|
|
8
10
|
} from "./AutomergeUrl.js"
|
|
9
11
|
import {
|
|
@@ -193,16 +195,20 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
193
195
|
const heads = handle.getRemoteHeads(storageId)
|
|
194
196
|
const haveHeadsChanged =
|
|
195
197
|
message.syncState.theirHeads &&
|
|
196
|
-
(!heads ||
|
|
198
|
+
(!heads ||
|
|
199
|
+
!headsAreSame(heads, encodeHeads(message.syncState.theirHeads)))
|
|
197
200
|
|
|
198
201
|
if (haveHeadsChanged && message.syncState.theirHeads) {
|
|
199
|
-
handle.setRemoteHeads(
|
|
202
|
+
handle.setRemoteHeads(
|
|
203
|
+
storageId,
|
|
204
|
+
encodeHeads(message.syncState.theirHeads)
|
|
205
|
+
)
|
|
200
206
|
|
|
201
207
|
if (storageId && this.#remoteHeadsGossipingEnabled) {
|
|
202
208
|
this.#remoteHeadsSubscriptions.handleImmediateRemoteHeadsChanged(
|
|
203
209
|
message.documentId,
|
|
204
210
|
storageId,
|
|
205
|
-
message.syncState.theirHeads
|
|
211
|
+
encodeHeads(message.syncState.theirHeads)
|
|
206
212
|
)
|
|
207
213
|
}
|
|
208
214
|
}
|
|
@@ -430,19 +436,22 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
430
436
|
/** The url or documentId of the handle to retrieve */
|
|
431
437
|
id: AnyDocumentId
|
|
432
438
|
): DocHandle<T> {
|
|
433
|
-
const documentId =
|
|
439
|
+
const { documentId, heads } = isValidAutomergeUrl(id)
|
|
440
|
+
? parseAutomergeUrl(id)
|
|
441
|
+
: { documentId: interpretAsDocumentId(id), heads: undefined }
|
|
434
442
|
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
if (
|
|
443
|
+
const cachedHandle = this.#handleCache[documentId]
|
|
444
|
+
if (cachedHandle) {
|
|
445
|
+
if (cachedHandle.isUnavailable()) {
|
|
438
446
|
// this ensures that the event fires after the handle has been returned
|
|
439
447
|
setTimeout(() => {
|
|
440
|
-
|
|
441
|
-
handle:
|
|
448
|
+
cachedHandle.emit("unavailable", {
|
|
449
|
+
handle: cachedHandle,
|
|
442
450
|
})
|
|
443
451
|
})
|
|
444
452
|
}
|
|
445
|
-
return
|
|
453
|
+
// If we already have the handle, return it immediately (or a view of the handle if heads are specified)
|
|
454
|
+
return heads ? cachedHandle.view(heads) : cachedHandle
|
|
446
455
|
}
|
|
447
456
|
|
|
448
457
|
// If we don't already have the handle, make an empty one and try loading it
|
|
@@ -473,7 +482,9 @@ export class Repo extends EventEmitter<RepoEvents> {
|
|
|
473
482
|
.catch(err => {
|
|
474
483
|
this.#log("error waiting for network", { err })
|
|
475
484
|
})
|
|
476
|
-
|
|
485
|
+
|
|
486
|
+
// If we already have the handle, return it immediately (or a view of the handle if heads are specified)
|
|
487
|
+
return heads ? handle.view(heads) : handle
|
|
477
488
|
}
|
|
478
489
|
|
|
479
490
|
delete(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const uint8ArrayFromHexString = (hexString: string): Uint8Array => {
|
|
2
|
+
if (hexString.length % 2 !== 0) {
|
|
3
|
+
throw new Error("Hex string must have an even length")
|
|
4
|
+
}
|
|
5
|
+
const bytes = new Uint8Array(hexString.length / 2)
|
|
6
|
+
for (let i = 0; i < hexString.length; i += 2) {
|
|
7
|
+
bytes[i >> 1] = parseInt(hexString.slice(i, i + 2), 16)
|
|
8
|
+
}
|
|
9
|
+
return bytes
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const uint8ArrayToHexString = (data: Uint8Array): string => {
|
|
13
|
+
return Array.from(data, byte => byte.toString(16).padStart(2, "0")).join("")
|
|
14
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Heads } from "@automerge/automerge/slim/next"
|
|
2
1
|
import { arraysAreEqual } from "./arraysAreEqual.js"
|
|
2
|
+
import { UrlHeads } from "../AutomergeUrl.js"
|
|
3
3
|
|
|
4
|
-
export const headsAreSame = (a:
|
|
4
|
+
export const headsAreSame = (a: UrlHeads, b: UrlHeads) => {
|
|
5
5
|
return arraysAreEqual(a, b)
|
|
6
6
|
}
|
|
@@ -67,20 +67,16 @@ export function runStorageAdapterTests(setup: SetupFn, title?: string): void {
|
|
|
67
67
|
await adapter.save(["AAAAA", "snapshot", "yyyyy"], PAYLOAD_B())
|
|
68
68
|
await adapter.save(["AAAAA", "sync-state", "zzzzz"], PAYLOAD_C())
|
|
69
69
|
|
|
70
|
-
expect(await adapter.loadRange(["AAAAA"])).toStrictEqual(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
|
|
81
|
-
{ key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
|
|
82
|
-
])
|
|
83
|
-
)
|
|
70
|
+
expect(await adapter.loadRange(["AAAAA"])).toStrictEqual([
|
|
71
|
+
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
|
|
72
|
+
{ key: ["AAAAA", "snapshot", "yyyyy"], data: PAYLOAD_B() },
|
|
73
|
+
{ key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
expect(await adapter.loadRange(["AAAAA", "sync-state"])).toStrictEqual([
|
|
77
|
+
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
|
|
78
|
+
{ key: ["AAAAA", "sync-state", "zzzzz"], data: PAYLOAD_C() },
|
|
79
|
+
])
|
|
84
80
|
})
|
|
85
81
|
|
|
86
82
|
it("should only load values that match they key", async ({ adapter }) => {
|
|
@@ -88,16 +84,9 @@ export function runStorageAdapterTests(setup: SetupFn, title?: string): void {
|
|
|
88
84
|
await adapter.save(["BBBBB", "sync-state", "zzzzz"], PAYLOAD_C())
|
|
89
85
|
|
|
90
86
|
const actual = await adapter.loadRange(["AAAAA"])
|
|
91
|
-
expect(actual).toStrictEqual(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
])
|
|
95
|
-
)
|
|
96
|
-
expect(actual).toStrictEqual(
|
|
97
|
-
expect.not.arrayContaining([
|
|
98
|
-
{ key: ["BBBBB", "sync-state", "zzzzz"], data: PAYLOAD_C() },
|
|
99
|
-
])
|
|
100
|
-
)
|
|
87
|
+
expect(actual).toStrictEqual([
|
|
88
|
+
{ key: ["AAAAA", "sync-state", "xxxxx"], data: PAYLOAD_A() },
|
|
89
|
+
])
|
|
101
90
|
})
|
|
102
91
|
})
|
|
103
92
|
|
|
@@ -9,6 +9,7 @@ import { keyHash, headsHash } from "./keyHash.js"
|
|
|
9
9
|
import { chunkTypeFromKey } from "./chunkTypeFromKey.js"
|
|
10
10
|
import * as Uuid from "uuid"
|
|
11
11
|
import { EventEmitter } from "eventemitter3"
|
|
12
|
+
import { encodeHeads } from "../AutomergeUrl.js"
|
|
12
13
|
|
|
13
14
|
type StorageSubsystemEvents = {
|
|
14
15
|
"document-loaded": (arg: {
|
|
@@ -173,6 +174,7 @@ export class StorageSubsystem extends EventEmitter<StorageSubsystemEvents> {
|
|
|
173
174
|
} else {
|
|
174
175
|
await this.#saveIncremental(documentId, doc)
|
|
175
176
|
}
|
|
177
|
+
|
|
176
178
|
this.#storedHeads.set(documentId, A.getHeads(doc))
|
|
177
179
|
}
|
|
178
180
|
|
|
@@ -279,7 +281,7 @@ export class StorageSubsystem extends EventEmitter<StorageSubsystemEvents> {
|
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
const newHeads = A.getHeads(doc)
|
|
282
|
-
if (headsAreSame(newHeads, oldHeads)) {
|
|
284
|
+
if (headsAreSame(encodeHeads(newHeads), encodeHeads(oldHeads))) {
|
|
283
285
|
// the document hasn't changed
|
|
284
286
|
return false
|
|
285
287
|
}
|
package/src/types.ts
CHANGED
|
@@ -27,8 +27,11 @@ export type AnyDocumentId =
|
|
|
27
27
|
| BinaryDocumentId
|
|
28
28
|
| LegacyDocumentId
|
|
29
29
|
|
|
30
|
+
// We need to define our own version of heads because the AutomergeHeads type is not bs58check encoded
|
|
31
|
+
export type UrlHeads = string[] & { __automergeUrlHeads: unknown }
|
|
32
|
+
|
|
30
33
|
/** A branded type for peer IDs */
|
|
31
34
|
export type PeerId = string & { __peerId: true }
|
|
32
35
|
|
|
33
36
|
/** A randomly generated string created when the {@link Repo} starts up */
|
|
34
|
-
export type SessionId = string & {
|
|
37
|
+
export type SessionId = string & { __sessionId: true }
|