UNPKG

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