-
Notifications
You must be signed in to change notification settings - Fork 160
/
stable.ts
1224 lines (1139 loc) · 35.3 KB
/
stable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/** @hidden **/
export { /** @hidden */ uuid } from "./uuid.js"
import { rootProxy } from "./proxies.js"
import { STATE } from "./constants.js"
import {
type AutomergeValue,
Counter,
type Doc,
type PatchCallback,
type Patch,
type PatchSource,
} from "./types.js"
export {
type AutomergeValue,
Counter,
type Doc,
Int,
Uint,
Float64,
type Patch,
type PatchCallback,
type ScalarValue,
type PatchInfo,
type PatchSource,
} from "./types.js"
import { Text } from "./text.js"
export { Text } from "./text.js"
import type {
API as WasmAPI,
Actor as ActorId,
Prop,
ObjID,
Change,
DecodedChange,
Heads,
MaterializeValue,
JsSyncState,
SyncMessage,
DecodedSyncMessage,
} from "@automerge/automerge-wasm"
export type {
PutPatch,
DelPatch,
SpliceTextPatch,
InsertPatch,
IncPatch,
SyncMessage,
} from "@automerge/automerge-wasm"
/** @hidden **/
type API = WasmAPI
const SyncStateSymbol = Symbol("_syncstate")
/**
* An opaque type tracking the state of sync with a remote peer
*/
type SyncState = JsSyncState & {
/** @hidden */
_opaque: typeof SyncStateSymbol
}
import { ApiHandler, type ChangeToEncode, UseApi } from "./low_level.js"
import { Automerge } from "@automerge/automerge-wasm"
import { RawString } from "./raw_string.js"
import { _state, _is_proxy, _trace, _obj } from "./internal_state.js"
import { stableConflictAt } from "./conflicts.js"
/** Options passed to {@link change}, and {@link emptyChange}
* @typeParam T - The type of value contained in the document
*/
export type ChangeOptions<T> = {
/** A message which describes the changes */
message?: string
/** The unix timestamp of the change (purely advisory, not used in conflict resolution) */
time?: number
/** A callback which will be called to notify the caller of any changes to the document */
patchCallback?: PatchCallback<T>
}
/** Options passed to {@link loadIncremental}, {@link applyChanges}, and {@link receiveSyncMessage}
* @typeParam T - The type of value contained in the document
*/
export type ApplyOptions<T> = { patchCallback?: PatchCallback<T> }
/**
* A List is an extended Array that adds the two helper methods `deleteAt` and `insertAt`.
*/
export interface List<T> extends Array<T> {
insertAt(index: number, ...args: T[]): List<T>
deleteAt(index: number, numDelete?: number): List<T>
}
/**
* Function for use in {@link change} which inserts values into a list at a given index
* @param list
* @param index
* @param values
*/
export function insertAt<T>(list: T[], index: number, ...values: T[]) {
if (!_is_proxy(list)) {
throw new RangeError("object cannot be modified outside of a change block")
}
(list as List<T>).insertAt(index, ...values)
}
/**
* Function for use in {@link change} which deletes values from a list at a given index
* @param list
* @param index
* @param numDelete
*/
export function deleteAt<T>(list: T[], index: number, numDelete?: number) {
if (!_is_proxy(list)) {
throw new RangeError("object cannot be modified outside of a change block")
}
(list as List<T>).deleteAt(index, numDelete)
}
/**
* Function which is called by {@link change} when making changes to a `Doc<T>`
* @typeParam T - The type of value contained in the document
*
* This function may mutate `doc`
*/
export type ChangeFn<T> = (doc: T) => void
/** @hidden **/
export interface State<T> {
change: DecodedChange
snapshot: T
}
/** @hidden **/
export function use(api: API) {
UseApi(api)
}
import * as wasm from "@automerge/automerge-wasm"
use(wasm)
/**
* Options to be passed to {@link init} or {@link load}
* @typeParam T - The type of the value the document contains
*/
export type InitOptions<T> = {
/** The actor ID to use for this document, a random one will be generated if `null` is passed */
actor?: ActorId
freeze?: boolean
/** A callback which will be called with the initial patch once the document has finished loading */
patchCallback?: PatchCallback<T>
/** @hidden */
enableTextV2?: boolean
/** @hidden */
unchecked?: boolean
/** Allow loading a document with missing changes */
allowMissingChanges?: boolean
/** @hidden */
convertRawStringsToText?: boolean
}
/** @hidden */
export function getBackend<T>(doc: Doc<T>): Automerge {
return _state(doc).handle
}
function importOpts<T>(_actor?: ActorId | InitOptions<T>): InitOptions<T> {
if (typeof _actor === "object") {
return _actor
} else {
return { actor: _actor }
}
}
/**
* Create a new automerge document
*
* @typeParam T - The type of value contained in the document. This will be the
* type that is passed to the change closure in {@link change}
* @param _opts - Either an actorId or an {@link InitOptions} (which may
* contain an actorId). If this is null the document will be initialised with a
* random actor ID
*/
export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
const opts = importOpts(_opts)
const freeze = !!opts.freeze
const patchCallback = opts.patchCallback
const text_v1 = !(opts.enableTextV2 || false)
const actor = opts.actor
const handle = ApiHandler.create({ actor, text_v1 })
handle.enableFreeze(!!opts.freeze)
const textV2 = opts.enableTextV2 || false
registerDatatypes(handle, textV2)
const doc = handle.materialize("/", undefined, {
handle,
heads: undefined,
freeze,
patchCallback,
textV2,
}) as Doc<T>
return doc
}
/**
* Make an immutable view of an automerge document as at `heads`
*
* @remarks
* The document returned from this function cannot be passed to {@link change}.
* This is because it shares the same underlying memory as `doc`, but it is
* consequently a very cheap copy.
*
* Note that this function will throw an error if any of the hashes in `heads`
* are not in the document.
*
* @typeParam T - The type of the value contained in the document
* @param doc - The document to create a view of
* @param heads - The hashes of the heads to create a view at
*/
export function view<T>(doc: Doc<T>, heads: Heads): Doc<T> {
const state = _state(doc)
const handle = state.handle
return state.handle.materialize("/", heads, {
...state,
handle,
heads,
}) as Doc<T>
}
/**
* Make a full writable copy of an automerge document
*
* @remarks
* Unlike {@link view} this function makes a full copy of the memory backing
* the document and can thus be passed to {@link change}. It also generates a
* new actor ID so that changes made in the new document do not create duplicate
* sequence numbers with respect to the old document. If you need control over
* the actor ID which is generated you can pass the actor ID as the second
* argument
*
* @typeParam T - The type of the value contained in the document
* @param doc - The document to clone
* @param _opts - Either an actor ID to use for the new doc or an {@link InitOptions}
*/
export function clone<T>(
doc: Doc<T>,
_opts?: ActorId | InitOptions<T>,
): Doc<T> {
const state = _state(doc)
const heads = state.heads
const opts = importOpts(_opts)
const handle = state.handle.fork(opts.actor, heads)
handle.updateDiffCursor()
// `change` uses the presence of state.heads to determine if we are in a view
// set it to undefined to indicate that this is a full fat document
const { heads: _oldHeads, ...stateSansHeads } = state
stateSansHeads.patchCallback = opts.patchCallback
return handle.applyPatches(doc, { ...stateSansHeads, handle })
}
/** Explicity free the memory backing a document. Note that this is note
* necessary in environments which support
* [`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)
*/
export function free<T>(doc: Doc<T>) {
return _state(doc).handle.free()
}
/**
* Create an automerge document from a POJO
*
* @param initialState - The initial state which will be copied into the document
* @typeParam T - The type of the value passed to `from` _and_ the type the resulting document will contain
* @typeParam actor - The actor ID of the resulting document, if this is null a random actor ID will be used
*
* @example
* ```
* const doc = automerge.from({
* tasks: [
* {description: "feed dogs", done: false}
* ]
* })
* ```
*/
export function from<T extends Record<string, unknown>>(
initialState: T | Doc<T>,
_opts?: ActorId | InitOptions<T>,
): Doc<T> {
return _change(init(_opts), "from", {}, d => Object.assign(d, initialState))
.newDoc
}
/**
* Update the contents of an automerge document
* @typeParam T - The type of the value contained in the document
* @param doc - The document to update
* @param options - Either a message, an {@link ChangeOptions}, or a {@link ChangeFn}
* @param callback - A `ChangeFn` to be used if `options` was a `string`
*
* Note that if the second argument is a function it will be used as the `ChangeFn` regardless of what the third argument is.
*
* @example A simple change
* ```
* let doc1 = automerge.init()
* doc1 = automerge.change(doc1, d => {
* d.key = "value"
* })
* assert.equal(doc1.key, "value")
* ```
*
* @example A change with a message
*
* ```
* doc1 = automerge.change(doc1, "add another value", d => {
* d.key2 = "value2"
* })
* ```
*
* @example A change with a message and a timestamp
*
* ```
* doc1 = automerge.change(doc1, {message: "add another value", time: 1640995200}, d => {
* d.key2 = "value2"
* })
* ```
*
* @example responding to a patch callback
* ```
* let patchedPath
* let patchCallback = patch => {
* patchedPath = patch.path
* }
* doc1 = automerge.change(doc1, {message: "add another value", time: 1640995200, patchCallback}, d => {
* d.key2 = "value2"
* })
* assert.equal(patchedPath, ["key2"])
* ```
*/
export function change<T>(
doc: Doc<T>,
options: string | ChangeOptions<T> | ChangeFn<T>,
callback?: ChangeFn<T>,
): Doc<T> {
if (typeof options === "function") {
return _change(doc, "change", {}, options).newDoc
} else if (typeof callback === "function") {
if (typeof options === "string") {
options = { message: options }
}
return _change(doc, "change", options, callback).newDoc
} else {
throw RangeError("Invalid args for change")
}
}
/**
* The type returned from {@link changeAt}
*/
export type ChangeAtResult<T> = {
/** The updated document **/
newDoc: Doc<T>
/**
* The heads resulting from the change
*
* @remarks
* Note that this is _not_ the same as the heads of `newDoc`. The newly created
* change will be added to the history of `newDoc` as if it was _concurrent_
* with whatever the heads of the document were at the time of the change.
* This means that `newHeads` will be the same as the heads of a fork of
* `newDoc` at the given heads to which the change was applied.
*
* This field will be `null` if no change was made
*/
newHeads: Heads | null
}
/**
* Make a change to the document as it was at a particular point in history
* @typeParam T - The type of the value contained in the document
* @param doc - The document to update
* @param scope - The heads representing the point in history to make the change
* @param options - Either a message or a {@link ChangeOptions} for the new change
* @param callback - A `ChangeFn` to be used if `options` was a `string`
*
* @remarks
* This function is similar to {@link change} but allows you to make changes to
* the document as if it were at a particular point in time. To understand this
* imagine a document created with the following history:
*
* ```ts
* let doc = automerge.from({..})
* doc = automerge.change(doc, () => {...})
*
* const heads = automerge.getHeads(doc)
*
* // fork the document make a change
* let fork = automerge.fork(doc)
* fork = automerge.change(fork, () => {...})
* const headsOnFork = automerge.getHeads(fork)
*
* // make a change on the original doc
* doc = automerge.change(doc, () => {...})
* const headsOnOriginal = automerge.getHeads(doc)
*
* // now merge the changes back to the original document
* doc = automerge.merge(doc, fork)
*
* // The heads of the document will now be (headsOnFork, headsOnOriginal)
* ```
*
* {@link ChangeAt} produces an equivalent history, but without having to
* create a fork of the document. In particular the `newHeads` field of the
* returned {@link ChangeAtResult} will be the same as `headsOnFork`.
*
* Why would you want this? It's typically used in conjunction with {@link diff}
* to reconcile state which is managed concurrently with the document. For
* example, if you have a text editor component which the user is modifying
* and you can't send the changes to the document synchronously you might follow
* a workflow like this:
*
* * On initialization save the current heads of the document in the text editor state
* * Every time the user makes a change record the change in the text editor state
*
* Now from time to time reconcile the editor state and the document
* * Load the last saved heads from the text editor state, call them `oldHeads`
* * Apply all the unreconciled changes to the document using `changeAt(doc, oldHeads, ...)`
* * Get the diff from the resulting document to the current document using {@link diff}
* passing the {@link ChangeAtResult.newHeads} as the `before` argument and the
* heads of the entire document as the `after` argument.
* * Apply the diff to the text editor state
* * Save the current heads of the document in the text editor state
*/
export function changeAt<T>(
doc: Doc<T>,
scope: Heads,
options: string | ChangeOptions<T> | ChangeFn<T>,
callback?: ChangeFn<T>,
): ChangeAtResult<T> {
if (typeof options === "function") {
return _change(doc, "changeAt", {}, options, scope)
} else if (typeof callback === "function") {
if (typeof options === "string") {
options = { message: options }
}
return _change(doc, "changeAt", options, callback, scope)
} else {
throw RangeError("Invalid args for changeAt")
}
}
function progressDocument<T>(
doc: Doc<T>,
source: PatchSource,
heads: Heads | null,
callback?: PatchCallback<T>,
): Doc<T> {
if (heads == null) {
return doc
}
const state = _state(doc)
const nextState = { ...state, heads: undefined }
const { value: nextDoc, patches } = state.handle.applyAndReturnPatches(
doc,
nextState,
)
if (patches.length > 0) {
if (callback != null) {
callback(patches, { before: doc, after: nextDoc, source })
}
const newState = _state(nextDoc)
newState.mostRecentPatch = {
before: _state(doc).heads,
after: newState.handle.getHeads(),
patches,
}
}
state.heads = heads
return nextDoc
}
function _change<T>(
doc: Doc<T>,
source: PatchSource,
options: ChangeOptions<T>,
callback: ChangeFn<T>,
scope?: Heads,
): { newDoc: Doc<T>; newHeads: Heads | null } {
if (typeof callback !== "function") {
throw new RangeError("invalid change function")
}
const state = _state(doc)
if (doc === undefined || state === undefined) {
throw new RangeError("must be the document root")
}
if (state.heads) {
throw new RangeError(
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.",
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
let heads = state.handle.getHeads()
if (scope && headsEqual(scope, heads)) {
scope = undefined
}
if (scope) {
state.handle.isolate(scope)
heads = scope
}
try {
state.heads = heads
const root: T = rootProxy(state.handle, state.textV2)
callback(root)
if (state.handle.pendingOps() === 0) {
state.heads = undefined
if (scope) {
state.handle.integrate()
}
return {
newDoc: doc,
newHeads: null,
}
} else {
const newHead = state.handle.commit(options.message, options.time)
state.handle.integrate()
return {
newDoc: progressDocument(
doc,
source,
heads,
options.patchCallback || state.patchCallback,
),
newHeads: newHead != null ? [newHead] : null,
}
}
} catch (e) {
state.heads = undefined
state.handle.rollback()
throw e
}
}
/**
* Make a change to a document which does not modify the document
*
* @param doc - The doc to add the empty change to
* @param options - Either a message or a {@link ChangeOptions} for the new change
*
* Why would you want to do this? One reason might be that you have merged
* changes from some other peers and you want to generate a change which
* depends on those merged changes so that you can sign the new change with all
* of the merged changes as part of the new change.
*/
export function emptyChange<T>(
doc: Doc<T>,
options: string | ChangeOptions<T> | void,
) {
if (options === undefined) {
options = {}
}
if (typeof options === "string") {
options = { message: options }
}
const state = _state(doc)
if (state.heads) {
throw new RangeError(
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.",
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.emptyChange(options.message, options.time)
return progressDocument(doc, "emptyChange", heads)
}
/**
* Load an automerge document from a compressed document produce by {@link save}
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
* @param data - The compressed document
* @param _opts - Either an actor ID or some {@link InitOptions}, if the actor
* ID is null a random actor ID will be created
*
* Note that `load` will throw an error if passed incomplete content (for
* example if you are receiving content over the network and don't know if you
* have the complete document yet). If you need to handle incomplete content use
* {@link init} followed by {@link loadIncremental}.
*/
export function load<T>(
data: Uint8Array,
_opts?: ActorId | InitOptions<T>,
): Doc<T> {
const opts = importOpts(_opts)
const actor = opts.actor
const patchCallback = opts.patchCallback
const text_v1 = !(opts.enableTextV2 || false)
const unchecked = opts.unchecked || false
const allowMissingDeps = opts.allowMissingChanges || false
const convertRawStringsToText = opts.convertRawStringsToText || false
const handle = ApiHandler.load(data, {
text_v1,
actor,
unchecked,
allowMissingDeps,
convertRawStringsToText,
})
handle.enableFreeze(!!opts.freeze)
const textV2 = opts.enableTextV2 || false
registerDatatypes(handle, textV2)
const doc = handle.materialize("/", undefined, {
handle,
heads: undefined,
patchCallback,
textV2,
}) as Doc<T>
return doc
}
/**
* Load changes produced by {@link saveIncremental}, or partial changes
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
* @param data - The compressedchanges
* @param opts - an {@link ApplyOptions}
*
* This function is useful when staying up to date with a connected peer.
* Perhaps the other end sent you a full compresed document which you loaded
* with {@link load} and they're sending you the result of
* {@link getLastLocalChange} every time they make a change.
*
* Note that this function will succesfully load the results of {@link save} as
* well as {@link getLastLocalChange} or any other incremental change.
*/
export function loadIncremental<T>(
doc: Doc<T>,
data: Uint8Array,
opts?: ApplyOptions<T>,
): Doc<T> {
if (!opts) {
opts = {}
}
const state = _state(doc)
if (state.heads) {
throw new RangeError(
"Attempting to change an out of date document - set at: " + _trace(doc),
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.loadIncremental(data)
return progressDocument(
doc,
"loadIncremental",
heads,
opts.patchCallback || state.patchCallback,
)
}
/**
* Create binary save data to be appended to a save file or fed into {@link loadIncremental}
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
*
* This function is useful for incrementally saving state. The data can be appended to a
* automerge save file, or passed to a document replicating its state.
*
*/
export function saveIncremental<T>(doc: Doc<T>): Uint8Array {
const state = _state(doc)
if (state.heads) {
throw new RangeError(
"Attempting to change an out of date document - set at: " + _trace(doc),
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
return state.handle.saveIncremental()
}
/**
* Export the contents of a document to a compressed format
*
* @param doc - The doc to save
*
* The returned bytes can be passed to {@link load} or {@link loadIncremental}
*/
export function save<T>(doc: Doc<T>): Uint8Array {
return _state(doc).handle.save()
}
/**
* Merge `remote` into `local`
* @typeParam T - The type of values contained in each document
* @param local - The document to merge changes into
* @param remote - The document to merge changes from
*
* @returns - The merged document
*
* Often when you are merging documents you will also need to clone them. Both
* arguments to `merge` are frozen after the call so you can no longer call
* mutating methods (such as {@link change}) on them. The symtom of this will be
* an error which says "Attempting to change an out of date document". To
* overcome this call {@link clone} on the argument before passing it to {@link
* merge}.
*/
export function merge<T>(local: Doc<T>, remote: Doc<T>): Doc<T> {
const localState = _state(local)
if (localState.heads) {
throw new RangeError(
"Attempting to change an out of date document - set at: " + _trace(local),
)
}
const heads = localState.handle.getHeads()
const remoteState = _state(remote)
const changes = localState.handle.getChangesAdded(remoteState.handle)
localState.handle.applyChanges(changes)
return progressDocument(local, "merge", heads, localState.patchCallback)
}
/**
* Get the actor ID associated with the document
*/
export function getActorId<T>(doc: Doc<T>): ActorId {
const state = _state(doc)
return state.handle.getActorId()
}
/**
* The type of conflicts for particular key or index
*
* Maps and sequences in automerge can contain conflicting values for a
* particular key or index. In this case {@link getConflicts} can be used to
* obtain a `Conflicts` representing the multiple values present for the property
*
* A `Conflicts` is a map from a unique (per property or index) key to one of
* the possible conflicting values for the given property.
*/
type Conflicts = { [key: string]: AutomergeValue }
/**
* Get the conflicts associated with a property
*
* The values of properties in a map in automerge can be conflicted if there
* are concurrent "put" operations to the same key. Automerge chooses one value
* arbitrarily (but deterministically, any two nodes who have the same set of
* changes will choose the same value) from the set of conflicting values to
* present as the value of the key.
*
* Sometimes you may want to examine these conflicts, in this case you can use
* {@link getConflicts} to get the conflicts for the key.
*
* @example
* ```
* import * as automerge from "@automerge/automerge"
*
* type Profile = {
* pets: Array<{name: string, type: string}>
* }
*
* let doc1 = automerge.init<Profile>("aaaa")
* doc1 = automerge.change(doc1, d => {
* d.pets = [{name: "Lassie", type: "dog"}]
* })
* let doc2 = automerge.init<Profile>("bbbb")
* doc2 = automerge.merge(doc2, automerge.clone(doc1))
*
* doc2 = automerge.change(doc2, d => {
* d.pets[0].name = "Beethoven"
* })
*
* doc1 = automerge.change(doc1, d => {
* d.pets[0].name = "Babe"
* })
*
* const doc3 = automerge.merge(doc1, doc2)
*
* // Note that here we pass `doc3.pets`, not `doc3`
* let conflicts = automerge.getConflicts(doc3.pets[0], "name")
*
* // The two conflicting values are the keys of the conflicts object
* assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
* ```
*/
export function getConflicts<T>(
doc: Doc<T>,
prop: Prop,
): Conflicts | undefined {
const state = _state(doc, false)
if (state.textV2) {
throw new Error("use unstable.getConflicts for an unstable document")
}
const objectId = _obj(doc)
if (objectId != null) {
return stableConflictAt(state.handle, objectId, prop)
} else {
return undefined
}
}
/**
* Get the binary representation of the last change which was made to this doc
*
* This is most useful when staying in sync with other peers, every time you
* make a change locally via {@link change} you immediately call {@link
* getLastLocalChange} and send the result over the network to other peers.
*/
export function getLastLocalChange<T>(doc: Doc<T>): Change | undefined {
const state = _state(doc)
return state.handle.getLastLocalChange() || undefined
}
/**
* Return the object ID of an arbitrary javascript value
*
* This is useful to determine if something is actually an automerge document,
* if `doc` is not an automerge document this will return null.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getObjectId(doc: any, prop?: Prop): ObjID | null {
if (prop) {
const state = _state(doc, false)
const objectId = _obj(doc)
if (!state || !objectId) {
return null
}
return state.handle.get(objectId, prop) as ObjID
} else {
return _obj(doc)
}
}
/**
* Get the changes which are in `newState` but not in `oldState`. The returned
* changes can be loaded in `oldState` via {@link applyChanges}.
*
* Note that this will crash if there are changes in `oldState` which are not in `newState`.
*/
export function getChanges<T>(oldState: Doc<T>, newState: Doc<T>): Change[] {
const n = _state(newState)
return n.handle.getChanges(getHeads(oldState))
}
/**
* Get all the changes in a document
*
* This is different to {@link save} because the output is an array of changes
* which can be individually applied via {@link applyChanges}`
*
*/
export function getAllChanges<T>(doc: Doc<T>): Change[] {
const state = _state(doc)
return state.handle.getChanges([])
}
/**
* Apply changes received from another document
*
* `doc` will be updated to reflect the `changes`. If there are changes which
* we do not have dependencies for yet those will be stored in the document and
* applied when the depended on changes arrive.
*
* You can use the {@link ApplyOptions} to pass a patchcallback which will be
* informed of any changes which occur as a result of applying the changes
*
*/
export function applyChanges<T>(
doc: Doc<T>,
changes: Change[],
opts?: ApplyOptions<T>,
): [Doc<T>] {
const state = _state(doc)
if (!opts) {
opts = {}
}
if (state.heads) {
throw new RangeError(
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.",
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.applyChanges(changes)
state.heads = heads
return [
progressDocument(
doc,
"applyChanges",
heads,
opts.patchCallback || state.patchCallback,
),
]
}
/** @hidden */
export function getHistory<T>(doc: Doc<T>): State<T>[] {
const textV2 = _state(doc).textV2
const history = getAllChanges(doc)
return history.map((change, index) => ({
get change() {
return decodeChange(change)
},
get snapshot() {
const [state] = applyChanges(
init({ enableTextV2: textV2 }),
history.slice(0, index + 1),
)
return <T>state
},
}))
}
/**
* Create a set of patches representing the change from one set of heads to another
*
* If either of the heads are missing from the document the returned set of patches will be empty
*/
export function diff(doc: Doc<unknown>, before: Heads, after: Heads): Patch[] {
checkHeads(before, "before")
checkHeads(after, "after")
const state = _state(doc)
if (
state.mostRecentPatch &&
equals(state.mostRecentPatch.before, before) &&
equals(state.mostRecentPatch.after, after)
) {
return state.mostRecentPatch.patches
}
return state.handle.diff(before, after)
}
function headsEqual(heads1: Heads, heads2: Heads): boolean {
if (heads1.length !== heads2.length) {
return false
}
for (let i = 0; i < heads1.length; i++) {
if (heads1[i] !== heads2[i]) {
return false
}
}
return true
}
function checkHeads(heads: Heads, fieldname: string) {
if (!Array.isArray(heads)) {
throw new Error(`${fieldname} must be an array`)
}
}
/** @hidden */
// FIXME : no tests
// FIXME can we just use deep equals now?
export function equals(val1: unknown, val2: unknown): boolean {
if (!isObject(val1) || !isObject(val2)) return val1 === val2
const keys1 = Object.keys(val1).sort(),
keys2 = Object.keys(val2).sort()
if (keys1.length !== keys2.length) return false
for (let i = 0; i < keys1.length; i++) {
if (keys1[i] !== keys2[i]) return false
if (!equals(val1[keys1[i]], val2[keys2[i]])) return false
}
return true
}
/**
* encode a {@link SyncState} into binary to send over the network