1 | var __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 **/
|
13 | export { /** @hidden */ uuid } from "./uuid.js";
|
14 | import { rootProxy } from "./proxies.js";
|
15 | import { STATE } from "./constants.js";
|
16 | import { Counter, } from "./types.js";
|
17 | export { Counter, Int, Uint, Float64, } from "./types.js";
|
18 | import { Text } from "./text.js";
|
19 | export { Text } from "./text.js";
|
20 | const SyncStateSymbol = Symbol("_syncstate");
|
21 | import { ApiHandler, UseApi } from "./low_level.js";
|
22 | import { RawString } from "./raw_string.js";
|
23 | import { _state, _is_proxy, _trace, _obj } from "./internal_state.js";
|
24 | import { 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 | */
|
31 | export 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 | */
|
44 | export 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 **/
|
52 | export function use(api) {
|
53 | UseApi(api);
|
54 | }
|
55 | import * as wasm from "@automerge/automerge-wasm";
|
56 | use(wasm);
|
57 | /** @hidden */
|
58 | export function getBackend(doc) {
|
59 | return _state(doc).handle;
|
60 | }
|
61 | function 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 | */
|
78 | export 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 | */
|
119 | export 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 | */
|
140 | export 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 | */
|
156 | export 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 | */
|
175 | export 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 | */
|
225 | export 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 | */
|
295 | export 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 | }
|
309 | function 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 | }
|
331 | function _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 | */
|
393 | export 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 | */
|
426 | export 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 | */
|
475 | export 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 | */
|
501 | export 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 | */
|
518 | export 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 | */
|
536 | export 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 | */
|
550 | export 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 | */
|
598 | export 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 | */
|
618 | export 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
|
629 | export 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 | */
|
648 | export 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 | */
|
659 | export 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 | */
|
674 | export 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 */
|
693 | export 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 | */
|
711 | export 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 | }
|
722 | function 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 | }
|
733 | function 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?
|
741 | export 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 | * */
|
760 | export 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 | */
|
771 | export 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 | */
|
788 | export 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 | */
|
812 | export 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 | */
|
841 | export function initSyncState() {
|
842 | return ApiHandler.exportSyncState(ApiHandler.initSyncState());
|
843 | }
|
844 | /** @hidden */
|
845 | export function encodeChange(change) {
|
846 | return ApiHandler.encodeChange(change);
|
847 | }
|
848 | /** @hidden */
|
849 | export function decodeChange(data) {
|
850 | return ApiHandler.decodeChange(data);
|
851 | }
|
852 | /** @hidden */
|
853 | export function encodeSyncMessage(message) {
|
854 | return ApiHandler.encodeSyncMessage(message);
|
855 | }
|
856 | /** @hidden */
|
857 | export function decodeSyncMessage(message) {
|
858 | return ApiHandler.decodeSyncMessage(message);
|
859 | }
|
860 | /**
|
861 | * Get any changes in `doc` which are not dependencies of `heads`
|
862 | */
|
863 | export 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 | */
|
870 | export function getHeads(doc) {
|
871 | const state = _state(doc);
|
872 | return state.heads || state.handle.getHeads();
|
873 | }
|
874 | /** @hidden */
|
875 | export function dump(doc) {
|
876 | const state = _state(doc);
|
877 | state.handle.dump();
|
878 | }
|
879 | /** @hidden */
|
880 | export 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 | }
|
887 | export 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 | }
|
895 | function isObject(obj) {
|
896 | return typeof obj === "object" && obj !== null;
|
897 | }
|
898 | export function saveSince(doc, heads) {
|
899 | const state = _state(doc);
|
900 | const result = state.handle.saveSince(heads);
|
901 | return result;
|
902 | }
|