@automerge/automerge
Advanced tools
Comparing version 2.0.1-alpha.2 to 2.0.1-alpha.3
"use strict"; | ||
// Properties of the document root object | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.TEXT = exports.COUNTER = exports.F64 = exports.INT = exports.UINT = exports.FROZEN = exports.READ_ONLY = exports.OBJECT_ID = exports.TRACE = exports.HEADS = exports.STATE = void 0; | ||
// Properties of the document root object | ||
//const OPTIONS = Symbol('_options') // object containing options passed to init() | ||
//const CACHE = Symbol('_cache') // map from objectId to immutable object | ||
//export const STATE = Symbol.for('_am_state') // object containing metadata about current state (e.g. sequence numbers) | ||
exports.STATE = Symbol.for('_am_meta'); // object containing metadata about current state (e.g. sequence numbers) | ||
exports.HEADS = Symbol.for('_am_heads'); // object containing metadata about current state (e.g. sequence numbers) | ||
exports.TRACE = Symbol.for('_am_trace'); // object containing metadata about current state (e.g. sequence numbers) | ||
exports.OBJECT_ID = Symbol.for('_am_objectId'); // object containing metadata about current state (e.g. sequence numbers) | ||
exports.READ_ONLY = Symbol.for('_am_readOnly'); // object containing metadata about current state (e.g. sequence numbers) | ||
exports.FROZEN = Symbol.for('_am_frozen'); // object containing metadata about current state (e.g. sequence numbers) | ||
exports.UINT = Symbol.for('_am_uint'); | ||
exports.INT = Symbol.for('_am_int'); | ||
exports.F64 = Symbol.for('_am_f64'); | ||
exports.COUNTER = Symbol.for('_am_counter'); | ||
exports.TEXT = Symbol.for('_am_text'); | ||
// Properties of all Automerge objects | ||
//const OBJECT_ID = Symbol('_objectId') // the object ID of the current object (string) | ||
//const CONFLICTS = Symbol('_conflicts') // map or list (depending on object type) of conflicts | ||
//const CHANGE = Symbol('_change') // the context object on proxy objects used in change callback | ||
//const ELEM_IDS = Symbol('_elemIds') // list containing the element ID of each list element | ||
exports.TEXT = exports.COUNTER = exports.F64 = exports.INT = exports.UINT = exports.IS_PROXY = exports.OBJECT_ID = exports.TRACE = exports.STATE = void 0; | ||
exports.STATE = Symbol.for("_am_meta"); // symbol used to hide application metadata on automerge objects | ||
exports.TRACE = Symbol.for("_am_trace"); // used for debugging | ||
exports.OBJECT_ID = Symbol.for("_am_objectId"); // synbol used to hide the object id on automerge objects | ||
exports.IS_PROXY = Symbol.for("_am_isProxy"); // symbol used to test if the document is a proxy object | ||
exports.UINT = Symbol.for("_am_uint"); | ||
exports.INT = Symbol.for("_am_int"); | ||
exports.F64 = Symbol.for("_am_f64"); | ||
exports.COUNTER = Symbol.for("_am_counter"); | ||
exports.TEXT = Symbol.for("_am_text"); |
@@ -60,3 +60,3 @@ "use strict"; | ||
increment(delta) { | ||
delta = typeof delta === 'number' ? delta : 1; | ||
delta = typeof delta === "number" ? delta : 1; | ||
this.context.increment(this.objectId, this.key, delta); | ||
@@ -71,3 +71,3 @@ this.value += delta; | ||
decrement(delta) { | ||
return this.increment(typeof delta === 'number' ? -delta : -1); | ||
return this.increment(typeof delta === "number" ? -delta : -1); | ||
} | ||
@@ -81,3 +81,3 @@ } | ||
* located. | ||
*/ | ||
*/ | ||
function getWriteableCounter(value, context, path, objectId, key) { | ||
@@ -84,0 +84,0 @@ return new WriteableCounter(value, context, path, objectId, key); |
"use strict"; | ||
var __rest = (this && this.__rest) || function (s, e) { | ||
var t = {}; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) | ||
t[p] = s[p]; | ||
if (s != null && typeof Object.getOwnPropertySymbols === "function") | ||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { | ||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) | ||
t[p[i]] = s[p[i]]; | ||
} | ||
return t; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isAutomerge = exports.toJS = exports.dump = exports.getHeads = exports.getMissingDeps = exports.decodeSyncMessage = exports.encodeSyncMessage = exports.decodeChange = exports.encodeChange = exports.initSyncState = exports.receiveSyncMessage = exports.generateSyncMessage = exports.decodeSyncState = exports.encodeSyncState = exports.equals = exports.getHistory = exports.applyChanges = exports.getAllChanges = exports.getChanges = exports.getObjectId = exports.getLastLocalChange = exports.getConflicts = exports.getActorId = exports.merge = exports.save = exports.loadIncremental = exports.load = exports.emptyChange = exports.change = exports.from = exports.free = exports.clone = exports.view = exports.init = exports.getBackend = exports.use = exports.Float64 = exports.Uint = exports.Int = exports.Counter = exports.Text = exports.uuid = void 0; | ||
/** @hidden **/ | ||
var uuid_1 = require("./uuid"); | ||
Object.defineProperty(exports, "uuid", { enumerable: true, get: function () { return uuid_1.uuid; } }); | ||
const proxies_1 = require("./proxies"); | ||
const constants_1 = require("./constants"); | ||
const types_1 = require("./types"); | ||
var types_2 = require("./types"); | ||
Object.defineProperty(exports, "Text", { enumerable: true, get: function () { return types_2.Text; } }); | ||
Object.defineProperty(exports, "Counter", { enumerable: true, get: function () { return types_2.Counter; } }); | ||
Object.defineProperty(exports, "Int", { enumerable: true, get: function () { return types_2.Int; } }); | ||
Object.defineProperty(exports, "Uint", { enumerable: true, get: function () { return types_2.Uint; } }); | ||
Object.defineProperty(exports, "Float64", { enumerable: true, get: function () { return types_2.Float64; } }); | ||
const low_level_1 = require("./low_level"); | ||
/** @hidden **/ | ||
function use(api) { | ||
(0, low_level_1.UseApi)(api); | ||
} | ||
exports.use = use; | ||
const wasm = require("@automerge/automerge-wasm"); | ||
use(wasm); | ||
/** @hidden */ | ||
function getBackend(doc) { | ||
return _state(doc).handle; | ||
} | ||
exports.getBackend = getBackend; | ||
function _state(doc, checkroot = true) { | ||
if (typeof doc !== 'object') { | ||
throw new RangeError("must be the document root"); | ||
} | ||
const state = Reflect.get(doc, constants_1.STATE); | ||
if (state === undefined || state == null || (checkroot && _obj(doc) !== "_root")) { | ||
throw new RangeError("must be the document root"); | ||
} | ||
return state; | ||
} | ||
function _frozen(doc) { | ||
return Reflect.get(doc, constants_1.FROZEN) === true; | ||
} | ||
function _trace(doc) { | ||
return Reflect.get(doc, constants_1.TRACE); | ||
} | ||
function _set_heads(doc, heads) { | ||
_state(doc).heads = heads; | ||
} | ||
function _clear_heads(doc) { | ||
Reflect.set(doc, constants_1.HEADS, undefined); | ||
Reflect.set(doc, constants_1.TRACE, undefined); | ||
} | ||
function _obj(doc) { | ||
if (!(typeof doc === 'object') || doc === null) { | ||
return null; | ||
} | ||
return Reflect.get(doc, constants_1.OBJECT_ID); | ||
} | ||
function _readonly(doc) { | ||
return Reflect.get(doc, constants_1.READ_ONLY) !== false; | ||
} | ||
function importOpts(_actor) { | ||
if (typeof _actor === 'object') { | ||
return _actor; | ||
} | ||
else { | ||
return { actor: _actor }; | ||
} | ||
} | ||
exports.unstable = void 0; | ||
/** | ||
* Create a new automerge document | ||
* # Automerge | ||
* | ||
* @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 | ||
*/ | ||
function init(_opts) { | ||
let opts = importOpts(_opts); | ||
let freeze = !!opts.freeze; | ||
let patchCallback = opts.patchCallback; | ||
const handle = low_level_1.ApiHandler.create(opts.actor); | ||
handle.enablePatches(true); | ||
handle.enableFreeze(!!opts.freeze); | ||
handle.registerDatatype("counter", (n) => new types_1.Counter(n)); | ||
handle.registerDatatype("text", (n) => new types_1.Text(n)); | ||
const doc = handle.materialize("/", undefined, { handle, heads: undefined, freeze, patchCallback }); | ||
return doc; | ||
} | ||
exports.init = init; | ||
/** | ||
* Make an immutable view of an automerge document as at `heads` | ||
* This library provides the core automerge data structure and sync algorithms. | ||
* Other libraries can be built on top of this one which provide IO and | ||
* persistence. | ||
* | ||
* @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. | ||
* An automerge document can be though of an immutable POJO (plain old javascript | ||
* object) which `automerge` tracks the history of, allowing it to be merged with | ||
* any other automerge document. | ||
* | ||
* Note that this function will throw an error if any of the hashes in `heads` | ||
* are not in the document. | ||
* ## Creating and modifying a 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 | ||
*/ | ||
function view(doc, heads) { | ||
const state = _state(doc); | ||
const handle = state.handle; | ||
return state.handle.materialize("/", heads, Object.assign(Object.assign({}, state), { handle, heads })); | ||
} | ||
exports.view = view; | ||
/** | ||
* Make a full writable copy of an automerge document | ||
* You can create a document with {@link init} or {@link from} and then make | ||
* changes to it with {@link change}, you can merge two documents with {@link | ||
* merge}. | ||
* | ||
* @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 | ||
* ```ts | ||
* import * as automerge from "@automerge/automerge" | ||
* | ||
* @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} | ||
*/ | ||
function clone(doc, _opts) { | ||
const state = _state(doc); | ||
const heads = state.heads; | ||
const opts = importOpts(_opts); | ||
const handle = state.handle.fork(opts.actor, heads); | ||
// `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 } = state, stateSansHeads = __rest(state, ["heads"]); | ||
return handle.applyPatches(doc, Object.assign(Object.assign({}, stateSansHeads), { handle })); | ||
} | ||
exports.clone = clone; | ||
/** 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) | ||
*/ | ||
function free(doc) { | ||
return _state(doc).handle.free(); | ||
} | ||
exports.free = free; | ||
/** | ||
* Create an automerge document from a POJO | ||
* type DocType = {ideas: Array<automerge.Text>} | ||
* | ||
* @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 | ||
* let doc1 = automerge.init<DocType>() | ||
* doc1 = automerge.change(doc1, d => { | ||
* d.ideas = [new automerge.Text("an immutable document")] | ||
* }) | ||
* | ||
* @example | ||
* ``` | ||
* const doc = automerge.from({ | ||
* tasks: [ | ||
* {description: "feed dogs", done: false} | ||
* ] | ||
* let doc2 = automerge.init<DocType>() | ||
* doc2 = automerge.merge(doc2, automerge.clone(doc1)) | ||
* doc2 = automerge.change<DocType>(doc2, d => { | ||
* d.ideas.push(new automerge.Text("which records it's history")) | ||
* }) | ||
* ``` | ||
*/ | ||
function from(initialState, actor) { | ||
return change(init(actor), (d) => Object.assign(d, initialState)); | ||
} | ||
exports.from = from; | ||
/** | ||
* 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() | ||
* // Note the `automerge.clone` call, see the "cloning" section of this readme for | ||
* // more detail | ||
* doc1 = automerge.merge(doc1, automerge.clone(doc2)) | ||
* doc1 = automerge.change(doc1, d => { | ||
* d.key = "value" | ||
* d.ideas[0].deleteAt(13, 8) | ||
* d.ideas[0].insertAt(13, "object") | ||
* }) | ||
* assert.equal(doc1.key, "value") | ||
* | ||
* let doc3 = automerge.merge(doc1, doc2) | ||
* // doc3 is now {ideas: ["an immutable object", "which records it's history"]} | ||
* ``` | ||
* | ||
* @example A change with a message | ||
* ## Applying changes from another document | ||
* | ||
* ``` | ||
* doc1 = automerge.change(doc1, "add another value", d => { | ||
* d.key2 = "value2" | ||
* }) | ||
* ``` | ||
* You can get a representation of the result of the last {@link change} you made | ||
* to a document with {@link getLastLocalChange} and you can apply that change to | ||
* another document using {@link applyChanges}. | ||
* | ||
* @example A change with a message and a timestamp | ||
* If you need to get just the changes which are in one document but not in another | ||
* you can use {@link getHeads} to get the heads of the document without the | ||
* changes and then {@link getMissingDeps}, passing the result of {@link getHeads} | ||
* on the document with the changes. | ||
* | ||
* ``` | ||
* doc1 = automerge.change(doc1, {message: "add another value", timestamp: 1640995200}, d => { | ||
* d.key2 = "value2" | ||
* ## Saving and loading documents | ||
* | ||
* You can {@link save} a document to generate a compresed binary representation of | ||
* the document which can be loaded with {@link load}. If you have a document which | ||
* you have recently made changes to you can generate recent changes with {@link | ||
* saveIncremental}, this will generate all the changes since you last called | ||
* `saveIncremental`, the changes generated can be applied to another document with | ||
* {@link loadIncremental}. | ||
* | ||
* ## Viewing different versions of a document | ||
* | ||
* Occasionally you may wish to explicitly step to a different point in a document | ||
* history. One common reason to do this is if you need to obtain a set of changes | ||
* which take the document from one state to another in order to send those changes | ||
* to another peer (or to save them somewhere). You can use {@link view} to do this. | ||
* | ||
* ```ts | ||
* import * as automerge from "@automerge/automerge" | ||
* import * as assert from "assert" | ||
* | ||
* let doc = automerge.from({ | ||
* key1: "value1", | ||
* }) | ||
* ``` | ||
* | ||
* @example responding to a patch callback | ||
* ``` | ||
* let patchedPath | ||
* let patchCallback = patch => { | ||
* patchedPath = patch.path | ||
* } | ||
* doc1 = automerge.change(doc1, {message, "add another value", timestamp: 1640995200, patchCallback}, d => { | ||
* // Make a clone of the document at this point, maybe this is actually on another | ||
* // peer. | ||
* let doc2 = automerge.clone < any > doc | ||
* | ||
* let heads = automerge.getHeads(doc) | ||
* | ||
* doc = | ||
* automerge.change < | ||
* any > | ||
* (doc, | ||
* d => { | ||
* d.key2 = "value2" | ||
* }) | ||
* assert.equal(patchedPath, ["key2"]) | ||
* ``` | ||
*/ | ||
function change(doc, options, callback) { | ||
if (typeof options === 'function') { | ||
return _change(doc, {}, options); | ||
} | ||
else if (typeof callback === 'function') { | ||
if (typeof options === "string") { | ||
options = { message: options }; | ||
} | ||
return _change(doc, options, callback); | ||
} | ||
else { | ||
throw RangeError("Invalid args for change"); | ||
} | ||
} | ||
exports.change = change; | ||
function progressDocument(doc, heads, callback) { | ||
if (heads == null) { | ||
return doc; | ||
} | ||
let state = _state(doc); | ||
let nextState = Object.assign(Object.assign({}, state), { heads: undefined }); | ||
let nextDoc = state.handle.applyPatches(doc, nextState, callback); | ||
state.heads = heads; | ||
return nextDoc; | ||
} | ||
function _change(doc, options, callback) { | ||
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 (_readonly(doc) === false) { | ||
throw new RangeError("Calls to Automerge.change cannot be nested"); | ||
} | ||
const heads = state.handle.getHeads(); | ||
try { | ||
state.heads = heads; | ||
const root = (0, proxies_1.rootProxy)(state.handle); | ||
callback(root); | ||
if (state.handle.pendingOps() === 0) { | ||
state.heads = undefined; | ||
return doc; | ||
} | ||
else { | ||
state.handle.commit(options.message, options.time); | ||
return progressDocument(doc, heads, options.patchCallback || state.patchCallback); | ||
} | ||
} | ||
catch (e) { | ||
//console.log("ERROR: ",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 | ||
* doc = | ||
* automerge.change < | ||
* any > | ||
* (doc, | ||
* d => { | ||
* d.key3 = "value3" | ||
* }) | ||
* | ||
* 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. | ||
*/ | ||
function emptyChange(doc, options) { | ||
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 (_readonly(doc) === false) { | ||
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, heads); | ||
} | ||
exports.emptyChange = emptyChange; | ||
/** | ||
* Load an automerge document from a compressed document produce by {@link save} | ||
* // At this point we've generated two separate changes, now we want to send | ||
* // just those changes to someone else | ||
* | ||
* @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 | ||
* // view is a cheap reference based copy of a document at a given set of heads | ||
* let before = automerge.view(doc, heads) | ||
* | ||
* 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}. | ||
*/ | ||
function load(data, _opts) { | ||
const opts = importOpts(_opts); | ||
const actor = opts.actor; | ||
const patchCallback = opts.patchCallback; | ||
const handle = low_level_1.ApiHandler.load(data, actor); | ||
handle.enablePatches(true); | ||
handle.enableFreeze(!!opts.freeze); | ||
handle.registerDatatype("counter", (n) => new types_1.Counter(n)); | ||
handle.registerDatatype("text", (n) => new types_1.Text(n)); | ||
const doc = handle.materialize("/", undefined, { handle, heads: undefined, patchCallback }); | ||
return doc; | ||
} | ||
exports.load = load; | ||
/** | ||
* Load changes produced by {@link saveIncremental}, or partial changes | ||
* // This view doesn't show the last two changes in the document state | ||
* assert.deepEqual(before, { | ||
* key1: "value1", | ||
* }) | ||
* | ||
* @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} | ||
* // Get the changes to send to doc2 | ||
* let changes = automerge.getChanges(before, doc) | ||
* | ||
* 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. | ||
* // Apply the changes at doc2 | ||
* doc2 = automerge.applyChanges < any > (doc2, changes)[0] | ||
* assert.deepEqual(doc2, { | ||
* key1: "value1", | ||
* key2: "value2", | ||
* key3: "value3", | ||
* }) | ||
* ``` | ||
* | ||
* Note that this function will succesfully load the results of {@link save} as | ||
* well as {@link getLastLocalChange} or any other incremental change. | ||
*/ | ||
function loadIncremental(doc, data, opts) { | ||
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 (_readonly(doc) === false) { | ||
throw new RangeError("Calls to Automerge.change cannot be nested"); | ||
} | ||
const heads = state.handle.getHeads(); | ||
state.handle.loadIncremental(data); | ||
return progressDocument(doc, heads, opts.patchCallback || state.patchCallback); | ||
} | ||
exports.loadIncremental = loadIncremental; | ||
/** | ||
* Export the contents of a document to a compressed format | ||
* If you have a {@link view} of a document which you want to make changes to you | ||
* can {@link clone} the viewed document. | ||
* | ||
* @param doc - The doc to save | ||
* ## Syncing | ||
* | ||
* The returned bytes can be passed to {@link load} or {@link loadIncremental} | ||
*/ | ||
function save(doc) { | ||
return _state(doc).handle.save(); | ||
} | ||
exports.save = save; | ||
/** | ||
* Merge `local` into `remote` | ||
* @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 | ||
* The sync protocol is stateful. This means that we start by creating a {@link | ||
* SyncState} for each peer we are communicating with using {@link initSyncState}. | ||
* Then we generate a message to send to the peer by calling {@link | ||
* generateSyncMessage}. When we receive a message from the peer we call {@link | ||
* receiveSyncMessage}. Here's a simple example of a loop which just keeps two | ||
* peers in sync. | ||
* | ||
* @returns - The merged document | ||
* ```ts | ||
* let sync1 = automerge.initSyncState() | ||
* let msg: Uint8Array | null | ||
* ;[sync1, msg] = automerge.generateSyncMessage(doc1, sync1) | ||
* | ||
* 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}. | ||
*/ | ||
function merge(local, remote) { | ||
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, heads, localState.patchCallback); | ||
} | ||
exports.merge = merge; | ||
/** | ||
* Get the actor ID associated with the document | ||
*/ | ||
function getActorId(doc) { | ||
const state = _state(doc); | ||
return state.handle.getActorId(); | ||
} | ||
exports.getActorId = getActorId; | ||
function conflictAt(context, objectId, prop) { | ||
const values = context.getAll(objectId, prop); | ||
if (values.length <= 1) { | ||
return; | ||
} | ||
const result = {}; | ||
for (const fullVal of values) { | ||
switch (fullVal[0]) { | ||
case "map": | ||
result[fullVal[1]] = (0, proxies_1.mapProxy)(context, fullVal[1], [prop], true); | ||
break; | ||
case "list": | ||
result[fullVal[1]] = (0, proxies_1.listProxy)(context, fullVal[1], [prop], true); | ||
break; | ||
case "text": | ||
result[fullVal[1]] = (0, proxies_1.textProxy)(context, fullVal[1], [prop], true); | ||
break; | ||
//case "table": | ||
//case "cursor": | ||
case "str": | ||
case "uint": | ||
case "int": | ||
case "f64": | ||
case "boolean": | ||
case "bytes": | ||
case "null": | ||
result[fullVal[2]] = fullVal[1]; | ||
break; | ||
case "counter": | ||
result[fullVal[2]] = new types_1.Counter(fullVal[1]); | ||
break; | ||
case "timestamp": | ||
result[fullVal[2]] = new Date(fullVal[1]); | ||
break; | ||
default: | ||
throw RangeError(`datatype ${fullVal[0]} unimplemented`); | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* Get the conflicts associated with a property | ||
* while (true) { | ||
* if (msg != null) { | ||
* network.send(msg) | ||
* } | ||
* let resp: Uint8Array = | ||
* (network.receive()[(doc1, sync1, _ignore)] = | ||
* automerge.receiveSyncMessage(doc1, sync1, resp)[(sync1, msg)] = | ||
* automerge.generateSyncMessage(doc1, sync1)) | ||
* } | ||
* ``` | ||
* | ||
* 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. | ||
* ## Conflicts | ||
* | ||
* Sometimes you may want to examine these conflicts, in this case you can use | ||
* {@link getConflicts} to get the conflicts for the key. | ||
* The only time conflicts occur in automerge documents is in concurrent | ||
* assignments to the same key in an object. In this case automerge | ||
* deterministically chooses an arbitrary value to present to the application but | ||
* you can examine the conflicts using {@link getConflicts}. | ||
* | ||
* @example | ||
* ``` | ||
@@ -503,267 +208,53 @@ * import * as automerge from "@automerge/automerge" | ||
* ``` | ||
*/ | ||
function getConflicts(doc, prop) { | ||
const state = _state(doc, false); | ||
const objectId = _obj(doc); | ||
if (objectId != null) { | ||
return conflictAt(state.handle, objectId, prop); | ||
} | ||
else { | ||
return undefined; | ||
} | ||
} | ||
exports.getConflicts = getConflicts; | ||
/** | ||
* 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. | ||
*/ | ||
function getLastLocalChange(doc) { | ||
const state = _state(doc); | ||
return state.handle.getLastLocalChange() || undefined; | ||
} | ||
exports.getLastLocalChange = getLastLocalChange; | ||
/** | ||
* Return the object ID of an arbitrary javascript value | ||
* ## Actor IDs | ||
* | ||
* This is useful to determine if something is actually an automerge document, | ||
* if `doc` is not an automerge document this will return null. | ||
*/ | ||
function getObjectId(doc) { | ||
return _obj(doc); | ||
} | ||
exports.getObjectId = getObjectId; | ||
/** | ||
* Get the changes which are in `newState` but not in `oldState`. The returned | ||
* changes can be loaded in `oldState` via {@link applyChanges}. | ||
* By default automerge will generate a random actor ID for you, but most methods | ||
* for creating a document allow you to set the actor ID. You can get the actor ID | ||
* associated with the document by calling {@link getActorId}. Actor IDs must not | ||
* be used in concurrent threads of executiong - all changes by a given actor ID | ||
* are expected to be sequential. | ||
* | ||
* Note that this will crash if there are changes in `oldState` which are not in `newState`. | ||
*/ | ||
function getChanges(oldState, newState) { | ||
const o = _state(oldState); | ||
const n = _state(newState); | ||
return n.handle.getChanges(getHeads(oldState)); | ||
} | ||
exports.getChanges = getChanges; | ||
/** | ||
* Get all the changes in a document | ||
* ## Listening to patches | ||
* | ||
* This is different to {@link save} because the output is an array of changes | ||
* which can be individually applied via {@link applyChanges}` | ||
* Sometimes you want to respond to changes made to an automerge document. In this | ||
* case you can use the {@link PatchCallback} type to receive notifications when | ||
* changes have been made. | ||
* | ||
*/ | ||
function getAllChanges(doc) { | ||
const state = _state(doc); | ||
return state.handle.getChanges([]); | ||
} | ||
exports.getAllChanges = getAllChanges; | ||
/** | ||
* Apply changes received from another document | ||
* ## Cloning | ||
* | ||
* `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. | ||
* Currently you cannot make mutating changes (i.e. call {@link change}) to a | ||
* document which you have two pointers to. For example, in this code: | ||
* | ||
* 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 | ||
* ```javascript | ||
* let doc1 = automerge.init() | ||
* let doc2 = automerge.change(doc1, d => (d.key = "value")) | ||
* ``` | ||
* | ||
*/ | ||
function applyChanges(doc, changes, opts) { | ||
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 (_readonly(doc) === false) { | ||
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, heads, opts.patchCallback || state.patchCallback)]; | ||
} | ||
exports.applyChanges = applyChanges; | ||
/** @hidden */ | ||
function getHistory(doc) { | ||
const history = getAllChanges(doc); | ||
return history.map((change, index) => ({ | ||
get change() { | ||
return decodeChange(change); | ||
}, | ||
get snapshot() { | ||
const [state] = applyChanges(init(), history.slice(0, index + 1)); | ||
return state; | ||
} | ||
})); | ||
} | ||
exports.getHistory = getHistory; | ||
/** @hidden */ | ||
// FIXME : no tests | ||
// FIXME can we just use deep equals now? | ||
function equals(val1, val2) { | ||
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; | ||
} | ||
exports.equals = equals; | ||
/** | ||
* encode a {@link SyncState} into binary to send over the network | ||
* `doc1` and `doc2` are both pointers to the same state. Any attempt to call | ||
* mutating methods on `doc1` will now result in an error like | ||
* | ||
* @group sync | ||
* */ | ||
function encodeSyncState(state) { | ||
const sync = low_level_1.ApiHandler.importSyncState(state); | ||
const result = low_level_1.ApiHandler.encodeSyncState(sync); | ||
sync.free(); | ||
return result; | ||
} | ||
exports.encodeSyncState = encodeSyncState; | ||
/** | ||
* Decode some binary data into a {@link SyncState} | ||
* Attempting to change an out of date document | ||
* | ||
* @group sync | ||
*/ | ||
function decodeSyncState(state) { | ||
let sync = low_level_1.ApiHandler.decodeSyncState(state); | ||
let result = low_level_1.ApiHandler.exportSyncState(sync); | ||
sync.free(); | ||
return result; | ||
} | ||
exports.decodeSyncState = decodeSyncState; | ||
/** | ||
* Generate a sync message to send to the peer represented by `inState` | ||
* @param doc - The doc to generate messages about | ||
* @param inState - The {@link SyncState} representing the peer we are talking to | ||
* If you encounter this you need to clone the original document, the above sample | ||
* would work as: | ||
* | ||
* @group sync | ||
* ```javascript | ||
* let doc1 = automerge.init() | ||
* let doc2 = automerge.change(automerge.clone(doc1), d => (d.key = "value")) | ||
* ``` | ||
* @packageDocumentation | ||
* | ||
* @returns An array of `[newSyncState, syncMessage | null]` where | ||
* `newSyncState` should replace `inState` and `syncMessage` should be sent to | ||
* the peer if it is not null. If `syncMessage` is null then we are up to date. | ||
*/ | ||
function generateSyncMessage(doc, inState) { | ||
const state = _state(doc); | ||
const syncState = low_level_1.ApiHandler.importSyncState(inState); | ||
const message = state.handle.generateSyncMessage(syncState); | ||
const outState = low_level_1.ApiHandler.exportSyncState(syncState); | ||
return [outState, message]; | ||
} | ||
exports.generateSyncMessage = generateSyncMessage; | ||
/** | ||
* Update a document and our sync state on receiving a sync message | ||
* ## The {@link unstable} module | ||
* | ||
* @group sync | ||
* | ||
* @param doc - The doc the sync message is about | ||
* @param inState - The {@link SyncState} for the peer we are communicating with | ||
* @param message - The message which was received | ||
* @param opts - Any {@link ApplyOption}s, used for passing a | ||
* {@link PatchCallback} which will be informed of any changes | ||
* in `doc` which occur because of the received sync message. | ||
* | ||
* @returns An array of `[newDoc, newSyncState, syncMessage | null]` where | ||
* `newDoc` is the updated state of `doc`, `newSyncState` should replace | ||
* `inState` and `syncMessage` should be sent to the peer if it is not null. If | ||
* `syncMessage` is null then we are up to date. | ||
* We are working on some changes to automerge which are not yet complete and | ||
* will result in backwards incompatible API changes. Once these changes are | ||
* ready for production use we will release a new major version of automerge. | ||
* However, until that point you can use the {@link unstable} module to try out | ||
* the new features, documents from the {@link unstable} module are | ||
* interoperable with documents from the main module. Please see the docs for | ||
* the {@link unstable} module for more details. | ||
*/ | ||
function receiveSyncMessage(doc, inState, message, opts) { | ||
const syncState = low_level_1.ApiHandler.importSyncState(inState); | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
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 (_readonly(doc) === false) { | ||
throw new RangeError("Calls to Automerge.change cannot be nested"); | ||
} | ||
const heads = state.handle.getHeads(); | ||
state.handle.receiveSyncMessage(syncState, message); | ||
const outSyncState = low_level_1.ApiHandler.exportSyncState(syncState); | ||
return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback), outSyncState, null]; | ||
} | ||
exports.receiveSyncMessage = receiveSyncMessage; | ||
/** | ||
* Create a new, blank {@link SyncState} | ||
* | ||
* When communicating with a peer for the first time use this to generate a new | ||
* {@link SyncState} for them | ||
* | ||
* @group sync | ||
*/ | ||
function initSyncState() { | ||
return low_level_1.ApiHandler.exportSyncState(low_level_1.ApiHandler.initSyncState()); | ||
} | ||
exports.initSyncState = initSyncState; | ||
/** @hidden */ | ||
function encodeChange(change) { | ||
return low_level_1.ApiHandler.encodeChange(change); | ||
} | ||
exports.encodeChange = encodeChange; | ||
/** @hidden */ | ||
function decodeChange(data) { | ||
return low_level_1.ApiHandler.decodeChange(data); | ||
} | ||
exports.decodeChange = decodeChange; | ||
/** @hidden */ | ||
function encodeSyncMessage(message) { | ||
return low_level_1.ApiHandler.encodeSyncMessage(message); | ||
} | ||
exports.encodeSyncMessage = encodeSyncMessage; | ||
/** @hidden */ | ||
function decodeSyncMessage(message) { | ||
return low_level_1.ApiHandler.decodeSyncMessage(message); | ||
} | ||
exports.decodeSyncMessage = decodeSyncMessage; | ||
/** | ||
* Get any changes in `doc` which are not dependencies of `heads` | ||
*/ | ||
function getMissingDeps(doc, heads) { | ||
const state = _state(doc); | ||
return state.handle.getMissingDeps(heads); | ||
} | ||
exports.getMissingDeps = getMissingDeps; | ||
/** | ||
* Get the hashes of the heads of this document | ||
*/ | ||
function getHeads(doc) { | ||
const state = _state(doc); | ||
return state.heads || state.handle.getHeads(); | ||
} | ||
exports.getHeads = getHeads; | ||
/** @hidden */ | ||
function dump(doc) { | ||
const state = _state(doc); | ||
state.handle.dump(); | ||
} | ||
exports.dump = dump; | ||
/** @hidden */ | ||
function toJS(doc) { | ||
const state = _state(doc); | ||
const enabled = state.handle.enableFreeze(false); | ||
const result = state.handle.materialize(); | ||
state.handle.enableFreeze(enabled); | ||
return result; | ||
} | ||
exports.toJS = toJS; | ||
function isAutomerge(doc) { | ||
return getObjectId(doc) === "_root" && !!Reflect.get(doc, constants_1.STATE); | ||
} | ||
exports.isAutomerge = isAutomerge; | ||
function isObject(obj) { | ||
return typeof obj === 'object' && obj !== null; | ||
} | ||
__exportStar(require("./stable"), exports); | ||
const unstable = require("./unstable"); | ||
exports.unstable = unstable; |
@@ -12,14 +12,36 @@ "use strict"; | ||
exports.ApiHandler = { | ||
create(actor) { throw new RangeError("Automerge.use() not called"); }, | ||
load(data, actor) { throw new RangeError("Automerge.use() not called (load)"); }, | ||
encodeChange(change) { throw new RangeError("Automerge.use() not called (encodeChange)"); }, | ||
decodeChange(change) { throw new RangeError("Automerge.use() not called (decodeChange)"); }, | ||
initSyncState() { throw new RangeError("Automerge.use() not called (initSyncState)"); }, | ||
encodeSyncMessage(message) { throw new RangeError("Automerge.use() not called (encodeSyncMessage)"); }, | ||
decodeSyncMessage(msg) { throw new RangeError("Automerge.use() not called (decodeSyncMessage)"); }, | ||
encodeSyncState(state) { throw new RangeError("Automerge.use() not called (encodeSyncState)"); }, | ||
decodeSyncState(data) { throw new RangeError("Automerge.use() not called (decodeSyncState)"); }, | ||
exportSyncState(state) { throw new RangeError("Automerge.use() not called (exportSyncState)"); }, | ||
importSyncState(state) { throw new RangeError("Automerge.use() not called (importSyncState)"); }, | ||
create(textV2, actor) { | ||
throw new RangeError("Automerge.use() not called"); | ||
}, | ||
load(data, textV2, actor) { | ||
throw new RangeError("Automerge.use() not called (load)"); | ||
}, | ||
encodeChange(change) { | ||
throw new RangeError("Automerge.use() not called (encodeChange)"); | ||
}, | ||
decodeChange(change) { | ||
throw new RangeError("Automerge.use() not called (decodeChange)"); | ||
}, | ||
initSyncState() { | ||
throw new RangeError("Automerge.use() not called (initSyncState)"); | ||
}, | ||
encodeSyncMessage(message) { | ||
throw new RangeError("Automerge.use() not called (encodeSyncMessage)"); | ||
}, | ||
decodeSyncMessage(msg) { | ||
throw new RangeError("Automerge.use() not called (decodeSyncMessage)"); | ||
}, | ||
encodeSyncState(state) { | ||
throw new RangeError("Automerge.use() not called (encodeSyncState)"); | ||
}, | ||
decodeSyncState(data) { | ||
throw new RangeError("Automerge.use() not called (decodeSyncState)"); | ||
}, | ||
exportSyncState(state) { | ||
throw new RangeError("Automerge.use() not called (exportSyncState)"); | ||
}, | ||
importSyncState(state) { | ||
throw new RangeError("Automerge.use() not called (importSyncState)"); | ||
}, | ||
}; | ||
/* eslint-enable */ |
@@ -8,3 +8,5 @@ "use strict"; | ||
constructor(value) { | ||
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER)) { | ||
if (!(Number.isInteger(value) && | ||
value <= Number.MAX_SAFE_INTEGER && | ||
value >= Number.MIN_SAFE_INTEGER)) { | ||
throw new RangeError(`Value ${value} cannot be a uint`); | ||
@@ -20,3 +22,5 @@ } | ||
constructor(value) { | ||
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= 0)) { | ||
if (!(Number.isInteger(value) && | ||
value <= Number.MAX_SAFE_INTEGER && | ||
value >= 0)) { | ||
throw new RangeError(`Value ${value} cannot be a uint`); | ||
@@ -32,3 +36,3 @@ } | ||
constructor(value) { | ||
if (typeof value !== 'number') { | ||
if (typeof value !== "number") { | ||
throw new RangeError(`Value ${value} cannot be a float64`); | ||
@@ -35,0 +39,0 @@ } |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.rootProxy = exports.textProxy = exports.listProxy = exports.mapProxy = void 0; | ||
const text_1 = require("./text"); | ||
const counter_1 = require("./counter"); | ||
const text_1 = require("./text"); | ||
const constants_1 = require("./constants"); | ||
const raw_string_1 = require("./raw_string"); | ||
function parseListIndex(key) { | ||
if (typeof key === 'string' && /^[0-9]+$/.test(key)) | ||
if (typeof key === "string" && /^[0-9]+$/.test(key)) | ||
key = parseInt(key, 10); | ||
if (typeof key !== 'number') { | ||
// throw new TypeError('A list index must be a number, but you passed ' + JSON.stringify(key)) | ||
if (typeof key !== "number") { | ||
return key; | ||
} | ||
if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) { | ||
throw new RangeError('A list index must be positive, but you passed ' + key); | ||
throw new RangeError("A list index must be positive, but you passed " + key); | ||
} | ||
@@ -20,3 +20,3 @@ return key; | ||
function valueAt(target, prop) { | ||
const { context, objectId, path, readonly, heads } = target; | ||
const { context, objectId, path, readonly, heads, textV2 } = target; | ||
const value = context.getWithType(objectId, prop, heads); | ||
@@ -29,16 +29,31 @@ if (value === null) { | ||
switch (datatype) { | ||
case undefined: return; | ||
case "map": return mapProxy(context, val, [...path, prop], readonly, heads); | ||
case "list": return listProxy(context, val, [...path, prop], readonly, heads); | ||
case "text": return textProxy(context, val, [...path, prop], readonly, heads); | ||
//case "table": | ||
//case "cursor": | ||
case "str": return val; | ||
case "uint": return val; | ||
case "int": return val; | ||
case "f64": return val; | ||
case "boolean": return val; | ||
case "null": return null; | ||
case "bytes": return val; | ||
case "timestamp": return val; | ||
case undefined: | ||
return; | ||
case "map": | ||
return mapProxy(context, val, textV2, [...path, prop], readonly, heads); | ||
case "list": | ||
return listProxy(context, val, textV2, [...path, prop], readonly, heads); | ||
case "text": | ||
if (textV2) { | ||
return context.text(val, heads); | ||
} | ||
else { | ||
return textProxy(context, val, [...path, prop], readonly, heads); | ||
} | ||
case "str": | ||
return val; | ||
case "uint": | ||
return val; | ||
case "int": | ||
return val; | ||
case "f64": | ||
return val; | ||
case "boolean": | ||
return val; | ||
case "null": | ||
return null; | ||
case "bytes": | ||
return val; | ||
case "timestamp": | ||
return val; | ||
case "counter": { | ||
@@ -56,5 +71,5 @@ if (readonly) { | ||
} | ||
function import_value(value) { | ||
function import_value(value, textV2) { | ||
switch (typeof value) { | ||
case 'object': | ||
case "object": | ||
if (value == null) { | ||
@@ -75,8 +90,11 @@ return [null, "null"]; | ||
} | ||
else if (value[constants_1.TEXT]) { | ||
return [value, "text"]; | ||
} | ||
else if (value instanceof Date) { | ||
return [value.getTime(), "timestamp"]; | ||
} | ||
else if (value instanceof raw_string_1.RawString) { | ||
return [value.val, "str"]; | ||
} | ||
else if (value instanceof text_1.Text) { | ||
return [value, "text"]; | ||
} | ||
else if (value instanceof Uint8Array) { | ||
@@ -92,3 +110,3 @@ return [value, "bytes"]; | ||
else if (value[constants_1.OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
@@ -98,6 +116,5 @@ else { | ||
} | ||
break; | ||
case 'boolean': | ||
case "boolean": | ||
return [value, "boolean"]; | ||
case 'number': | ||
case "number": | ||
if (Number.isInteger(value)) { | ||
@@ -109,6 +126,9 @@ return [value, "int"]; | ||
} | ||
break; | ||
case 'string': | ||
return [value]; | ||
break; | ||
case "string": | ||
if (textV2) { | ||
return [value, "text"]; | ||
} | ||
else { | ||
return [value, "str"]; | ||
} | ||
default: | ||
@@ -120,3 +140,3 @@ throw new RangeError(`Unsupported type of value: ${typeof value}`); | ||
get(target, key) { | ||
const { context, objectId, readonly, frozen, heads, cache } = target; | ||
const { context, objectId, cache } = target; | ||
if (key === Symbol.toStringTag) { | ||
@@ -127,12 +147,8 @@ return target[Symbol.toStringTag]; | ||
return objectId; | ||
if (key === constants_1.READ_ONLY) | ||
return readonly; | ||
if (key === constants_1.FROZEN) | ||
return frozen; | ||
if (key === constants_1.HEADS) | ||
return heads; | ||
if (key === constants_1.IS_PROXY) | ||
return true; | ||
if (key === constants_1.TRACE) | ||
return target.trace; | ||
if (key === constants_1.STATE) | ||
return context; | ||
return { handle: context }; | ||
if (!cache[key]) { | ||
@@ -144,15 +160,7 @@ cache[key] = valueAt(target, key); | ||
set(target, key, val) { | ||
const { context, objectId, path, readonly, frozen } = target; | ||
const { context, objectId, path, readonly, frozen, textV2 } = target; | ||
target.cache = {}; // reset cache on set | ||
if (val && val[constants_1.OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
if (key === constants_1.FROZEN) { | ||
target.frozen = val; | ||
return true; | ||
} | ||
if (key === constants_1.HEADS) { | ||
target.heads = val; | ||
return true; | ||
} | ||
if (key === constants_1.TRACE) { | ||
@@ -162,3 +170,3 @@ target.trace = val; | ||
} | ||
const [value, datatype] = import_value(val); | ||
const [value, datatype] = import_value(val, textV2); | ||
if (frozen) { | ||
@@ -173,3 +181,3 @@ throw new RangeError("Attempting to use an outdated Automerge document"); | ||
const list = context.putObject(objectId, key, []); | ||
const proxyList = listProxy(context, list, [...path, key], readonly); | ||
const proxyList = listProxy(context, list, textV2, [...path, key], readonly); | ||
for (let i = 0; i < value.length; i++) { | ||
@@ -181,7 +189,12 @@ proxyList[i] = value[i]; | ||
case "text": { | ||
const text = context.putObject(objectId, key, "", "text"); | ||
const proxyText = textProxy(context, text, [...path, key], readonly); | ||
for (let i = 0; i < value.length; i++) { | ||
proxyText[i] = value.get(i); | ||
if (textV2) { | ||
context.putObject(objectId, key, value); | ||
} | ||
else { | ||
const text = context.putObject(objectId, key, ""); | ||
const proxyText = textProxy(context, text, [...path, key], readonly); | ||
for (let i = 0; i < value.length; i++) { | ||
proxyText[i] = value.get(i); | ||
} | ||
} | ||
break; | ||
@@ -191,3 +204,3 @@ } | ||
const map = context.putObject(objectId, key, {}); | ||
const proxyMap = mapProxy(context, map, [...path, key], readonly); | ||
const proxyMap = mapProxy(context, map, textV2, [...path, key], readonly); | ||
for (const key in value) { | ||
@@ -219,5 +232,7 @@ proxyMap[key] = value[key]; | ||
const value = this.get(target, key); | ||
if (typeof value !== 'undefined') { | ||
if (typeof value !== "undefined") { | ||
return { | ||
configurable: true, enumerable: true, value | ||
configurable: true, | ||
enumerable: true, | ||
value, | ||
}; | ||
@@ -235,6 +250,8 @@ } | ||
get(target, index) { | ||
const { context, objectId, readonly, frozen, heads } = target; | ||
const { context, objectId, heads } = target; | ||
index = parseListIndex(index); | ||
if (index === Symbol.hasInstance) { | ||
return (instance) => { return Array.isArray(instance); }; | ||
return instance => { | ||
return Array.isArray(instance); | ||
}; | ||
} | ||
@@ -246,15 +263,11 @@ if (index === Symbol.toStringTag) { | ||
return objectId; | ||
if (index === constants_1.READ_ONLY) | ||
return readonly; | ||
if (index === constants_1.FROZEN) | ||
return frozen; | ||
if (index === constants_1.HEADS) | ||
return heads; | ||
if (index === constants_1.IS_PROXY) | ||
return true; | ||
if (index === constants_1.TRACE) | ||
return target.trace; | ||
if (index === constants_1.STATE) | ||
return context; | ||
if (index === 'length') | ||
return { handle: context }; | ||
if (index === "length") | ||
return context.length(objectId, heads); | ||
if (typeof index === 'number') { | ||
if (typeof index === "number") { | ||
return valueAt(target, index); | ||
@@ -267,15 +280,7 @@ } | ||
set(target, index, val) { | ||
const { context, objectId, path, readonly, frozen } = target; | ||
const { context, objectId, path, readonly, frozen, textV2 } = target; | ||
index = parseListIndex(index); | ||
if (val && val[constants_1.OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
if (index === constants_1.FROZEN) { | ||
target.frozen = val; | ||
return true; | ||
} | ||
if (index === constants_1.HEADS) { | ||
target.heads = val; | ||
return true; | ||
} | ||
if (index === constants_1.TRACE) { | ||
@@ -286,5 +291,5 @@ target.trace = val; | ||
if (typeof index == "string") { | ||
throw new RangeError('list index must be a number'); | ||
throw new RangeError("list index must be a number"); | ||
} | ||
const [value, datatype] = import_value(val); | ||
const [value, datatype] = import_value(val, textV2); | ||
if (frozen) { | ||
@@ -305,3 +310,3 @@ throw new RangeError("Attempting to use an outdated Automerge document"); | ||
} | ||
const proxyList = listProxy(context, list, [...path, index], readonly); | ||
const proxyList = listProxy(context, list, textV2, [...path, index], readonly); | ||
proxyList.splice(0, 0, ...value); | ||
@@ -311,11 +316,21 @@ break; | ||
case "text": { | ||
let text; | ||
if (index >= context.length(objectId)) { | ||
text = context.insertObject(objectId, index, "", "text"); | ||
if (textV2) { | ||
if (index >= context.length(objectId)) { | ||
context.insertObject(objectId, index, value); | ||
} | ||
else { | ||
context.putObject(objectId, index, value); | ||
} | ||
} | ||
else { | ||
text = context.putObject(objectId, index, "", "text"); | ||
let text; | ||
if (index >= context.length(objectId)) { | ||
text = context.insertObject(objectId, index, ""); | ||
} | ||
else { | ||
text = context.putObject(objectId, index, ""); | ||
} | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
} | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
break; | ||
@@ -331,3 +346,3 @@ } | ||
} | ||
const proxyMap = mapProxy(context, map, [...path, index], readonly); | ||
const proxyMap = mapProxy(context, map, textV2, [...path, index], readonly); | ||
for (const key in value) { | ||
@@ -351,4 +366,5 @@ proxyMap[key] = value[key]; | ||
index = parseListIndex(index); | ||
if (context.get(objectId, index)[0] == "counter") { | ||
throw new TypeError('Unsupported operation: deleting a counter from a list'); | ||
const elem = context.get(objectId, index); | ||
if (elem != null && elem[0] == "counter") { | ||
throw new TypeError("Unsupported operation: deleting a counter from a list"); | ||
} | ||
@@ -361,10 +377,10 @@ context.delete(objectId, index); | ||
index = parseListIndex(index); | ||
if (typeof index === 'number') { | ||
if (typeof index === "number") { | ||
return index < context.length(objectId, heads); | ||
} | ||
return index === 'length'; | ||
return index === "length"; | ||
}, | ||
getOwnPropertyDescriptor(target, index) { | ||
const { context, objectId, heads } = target; | ||
if (index === 'length') | ||
if (index === "length") | ||
return { writable: true, value: context.length(objectId, heads) }; | ||
@@ -377,3 +393,5 @@ if (index === constants_1.OBJECT_ID) | ||
}, | ||
getPrototypeOf(target) { return Object.getPrototypeOf(target); }, | ||
getPrototypeOf(target) { | ||
return Object.getPrototypeOf(target); | ||
}, | ||
ownKeys( /*target*/) { | ||
@@ -387,30 +405,27 @@ const keys = []; | ||
return keys; | ||
} | ||
}, | ||
}; | ||
const TextHandler = Object.assign({}, ListHandler, { | ||
get(target, index) { | ||
// FIXME this is a one line change from ListHandler.get() | ||
const { context, objectId, readonly, frozen, heads } = target; | ||
const { context, objectId, heads } = target; | ||
index = parseListIndex(index); | ||
if (index === Symbol.hasInstance) { | ||
return (instance) => { | ||
return Array.isArray(instance); | ||
}; | ||
} | ||
if (index === Symbol.toStringTag) { | ||
return target[Symbol.toStringTag]; | ||
} | ||
if (index === Symbol.hasInstance) { | ||
return (instance) => { return Array.isArray(instance); }; | ||
} | ||
if (index === constants_1.OBJECT_ID) | ||
return objectId; | ||
if (index === constants_1.READ_ONLY) | ||
return readonly; | ||
if (index === constants_1.FROZEN) | ||
return frozen; | ||
if (index === constants_1.HEADS) | ||
return heads; | ||
if (index === constants_1.IS_PROXY) | ||
return true; | ||
if (index === constants_1.TRACE) | ||
return target.trace; | ||
if (index === constants_1.STATE) | ||
return context; | ||
if (index === 'length') | ||
return { handle: context }; | ||
if (index === "length") | ||
return context.length(objectId, heads); | ||
if (typeof index === 'number') { | ||
if (typeof index === "number") { | ||
return valueAt(target, index); | ||
@@ -426,28 +441,61 @@ } | ||
}); | ||
function mapProxy(context, objectId, path, readonly, heads) { | ||
return new Proxy({ context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {} }, MapHandler); | ||
function mapProxy(context, objectId, textV2, path, readonly, heads) { | ||
const target = { | ||
context, | ||
objectId, | ||
path: path || [], | ||
readonly: !!readonly, | ||
frozen: false, | ||
heads, | ||
cache: {}, | ||
textV2, | ||
}; | ||
const proxied = {}; | ||
Object.assign(proxied, target); | ||
let result = new Proxy(proxied, MapHandler); | ||
// conversion through unknown is necessary because the types are so different | ||
return result; | ||
} | ||
exports.mapProxy = mapProxy; | ||
function listProxy(context, objectId, path, readonly, heads) { | ||
const target = []; | ||
Object.assign(target, { context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {} }); | ||
return new Proxy(target, ListHandler); | ||
function listProxy(context, objectId, textV2, path, readonly, heads) { | ||
const target = { | ||
context, | ||
objectId, | ||
path: path || [], | ||
readonly: !!readonly, | ||
frozen: false, | ||
heads, | ||
cache: {}, | ||
textV2, | ||
}; | ||
const proxied = []; | ||
Object.assign(proxied, target); | ||
// @ts-ignore | ||
return new Proxy(proxied, ListHandler); | ||
} | ||
exports.listProxy = listProxy; | ||
function textProxy(context, objectId, path, readonly, heads) { | ||
const target = []; | ||
Object.assign(target, { context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {} }); | ||
const target = { | ||
context, | ||
objectId, | ||
path: path || [], | ||
readonly: !!readonly, | ||
frozen: false, | ||
heads, | ||
cache: {}, | ||
textV2: false, | ||
}; | ||
return new Proxy(target, TextHandler); | ||
} | ||
exports.textProxy = textProxy; | ||
function rootProxy(context, readonly) { | ||
function rootProxy(context, textV2, readonly) { | ||
/* eslint-disable-next-line */ | ||
return mapProxy(context, "_root", [], !!readonly); | ||
return mapProxy(context, "_root", textV2, [], !!readonly); | ||
} | ||
exports.rootProxy = rootProxy; | ||
function listMethods(target) { | ||
const { context, objectId, path, readonly, frozen, heads } = target; | ||
const { context, objectId, path, readonly, frozen, heads, textV2 } = target; | ||
const methods = { | ||
deleteAt(index, numDelete) { | ||
if (typeof numDelete === 'number') { | ||
if (typeof numDelete === "number") { | ||
context.splice(objectId, index, numDelete); | ||
@@ -461,3 +509,3 @@ } | ||
fill(val, start, end) { | ||
const [value, datatype] = import_value(val); | ||
const [value, datatype] = import_value(val, textV2); | ||
const length = context.length(objectId); | ||
@@ -467,3 +515,8 @@ start = parseListIndex(start || 0); | ||
for (let i = start; i < Math.min(end, length); i++) { | ||
context.put(objectId, i, value, datatype); | ||
if (datatype === "text" || datatype === "list" || datatype === "map") { | ||
context.putObject(objectId, i, value); | ||
} | ||
else { | ||
context.put(objectId, i, value, datatype); | ||
} | ||
} | ||
@@ -476,3 +529,3 @@ return this; | ||
const value = context.getWithType(objectId, i, heads); | ||
if (value && value[1] === o[constants_1.OBJECT_ID] || value[1] === o) { | ||
if (value && (value[1] === o[constants_1.OBJECT_ID] || value[1] === o)) { | ||
return i; | ||
@@ -513,3 +566,3 @@ } | ||
if (val && val[constants_1.OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
@@ -531,3 +584,3 @@ } | ||
} | ||
const values = vals.map((val) => import_value(val)); | ||
const values = vals.map(val => import_value(val, textV2)); | ||
for (const [value, datatype] of values) { | ||
@@ -537,3 +590,3 @@ switch (datatype) { | ||
const list = context.insertObject(objectId, index, []); | ||
const proxyList = listProxy(context, list, [...path, index], readonly); | ||
const proxyList = listProxy(context, list, textV2, [...path, index], readonly); | ||
proxyList.splice(0, 0, ...value); | ||
@@ -543,5 +596,10 @@ break; | ||
case "text": { | ||
const text = context.insertObject(objectId, index, "", "text"); | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
if (textV2) { | ||
context.insertObject(objectId, index, value); | ||
} | ||
else { | ||
const text = context.insertObject(objectId, index, ""); | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
} | ||
break; | ||
@@ -551,3 +609,3 @@ } | ||
const map = context.insertObject(objectId, index, {}); | ||
const proxyMap = mapProxy(context, map, [...path, index], readonly); | ||
const proxyMap = mapProxy(context, map, textV2, [...path, index], readonly); | ||
for (const key in value) { | ||
@@ -580,3 +638,3 @@ proxyMap[key] = value[key]; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -596,3 +654,3 @@ return iterator; | ||
return { value, done: true }; | ||
} | ||
}, | ||
}; | ||
@@ -612,3 +670,3 @@ return iterator; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -652,3 +710,3 @@ return iterator; | ||
let index = 0; | ||
for (let v of this) { | ||
for (const v of this) { | ||
if (f(v, index)) { | ||
@@ -662,3 +720,3 @@ return v; | ||
let index = 0; | ||
for (let v of this) { | ||
for (const v of this) { | ||
if (f(v, index)) { | ||
@@ -672,3 +730,3 @@ return index; | ||
includes(elem) { | ||
return this.find((e) => e === elem) !== undefined; | ||
return this.find(e => e === elem) !== undefined; | ||
}, | ||
@@ -695,3 +753,3 @@ join(sep) { | ||
let index = 0; | ||
for (let v of this) { | ||
for (const v of this) { | ||
if (f(v, index)) { | ||
@@ -712,3 +770,3 @@ return true; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -721,3 +779,3 @@ return methods; | ||
set(index, value) { | ||
return this[index] = value; | ||
return (this[index] = value); | ||
}, | ||
@@ -728,11 +786,11 @@ get(index) { | ||
toString() { | ||
return context.text(objectId, heads).replace(//g, ''); | ||
return context.text(objectId, heads).replace(//g, ""); | ||
}, | ||
toSpans() { | ||
const spans = []; | ||
let chars = ''; | ||
let chars = ""; | ||
const length = context.length(objectId); | ||
for (let i = 0; i < length; i++) { | ||
const value = this[i]; | ||
if (typeof value === 'string') { | ||
if (typeof value === "string") { | ||
chars += value; | ||
@@ -743,3 +801,3 @@ } | ||
spans.push(chars); | ||
chars = ''; | ||
chars = ""; | ||
} | ||
@@ -760,5 +818,5 @@ spans.push(value); | ||
return text.indexOf(o, start); | ||
} | ||
}, | ||
}; | ||
return methods; | ||
} |
@@ -7,3 +7,3 @@ "use strict"; | ||
constructor(text) { | ||
if (typeof text === 'string') { | ||
if (typeof text === "string") { | ||
this.elems = [...text]; | ||
@@ -44,3 +44,3 @@ } | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -57,8 +57,8 @@ } | ||
// https://jsperf.com/join-vs-loop-w-type-test | ||
this.str = ''; | ||
this.str = ""; | ||
for (const elem of this.elems) { | ||
if (typeof elem === 'string') | ||
if (typeof elem === "string") | ||
this.str += elem; | ||
else | ||
this.str += '\uFFFC'; | ||
this.str += "\uFFFC"; | ||
} | ||
@@ -72,4 +72,4 @@ } | ||
* | ||
* For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans: | ||
* => ['ab', {x: 3}, 'cd'] | ||
* For example, the value `['a', 'b', {x: 3}, 'c', 'd']` has spans: | ||
* `=> ['ab', {x: 3}, 'cd']` | ||
*/ | ||
@@ -79,5 +79,5 @@ toSpans() { | ||
this.spans = []; | ||
let chars = ''; | ||
let chars = ""; | ||
for (const elem of this.elems) { | ||
if (typeof elem === 'string') { | ||
if (typeof elem === "string") { | ||
chars += elem; | ||
@@ -88,3 +88,3 @@ } | ||
this.spans.push(chars); | ||
chars = ''; | ||
chars = ""; | ||
} | ||
@@ -91,0 +91,0 @@ this.spans.push(elem); |
@@ -6,3 +6,3 @@ "use strict"; | ||
function defaultFactory() { | ||
return (0, uuid_1.v4)().replace(/-/g, ''); | ||
return (0, uuid_1.v4)().replace(/-/g, ""); | ||
} | ||
@@ -14,3 +14,7 @@ let factory = defaultFactory; | ||
exports.uuid = uuid; | ||
exports.uuid.setFactory = newFactory => { factory = newFactory; }; | ||
exports.uuid.reset = () => { factory = defaultFactory; }; | ||
exports.uuid.setFactory = newFactory => { | ||
factory = newFactory; | ||
}; | ||
exports.uuid.reset = () => { | ||
factory = defaultFactory; | ||
}; |
export declare const STATE: unique symbol; | ||
export declare const HEADS: unique symbol; | ||
export declare const TRACE: unique symbol; | ||
export declare const OBJECT_ID: unique symbol; | ||
export declare const READ_ONLY: unique symbol; | ||
export declare const FROZEN: unique symbol; | ||
export declare const IS_PROXY: unique symbol; | ||
export declare const UINT: unique symbol; | ||
@@ -8,0 +6,0 @@ export declare const INT: unique symbol; |
@@ -37,6 +37,6 @@ import { Automerge, ObjID, Prop } from "@automerge/automerge-wasm"; | ||
context: Automerge; | ||
path: string[]; | ||
path: Prop[]; | ||
objectId: ObjID; | ||
key: Prop; | ||
constructor(value: number, context: Automerge, path: string[], objectId: ObjID, key: Prop); | ||
constructor(value: number, context: Automerge, path: Prop[], objectId: ObjID, key: Prop); | ||
/** | ||
@@ -59,4 +59,4 @@ * Increases the value of the counter by `delta`. If `delta` is not given, | ||
* located. | ||
*/ | ||
export declare function getWriteableCounter(value: number, context: Automerge, path: string[], objectId: ObjID, key: Prop): WriteableCounter; | ||
*/ | ||
export declare function getWriteableCounter(value: number, context: Automerge, path: Prop[], objectId: ObjID, key: Prop): WriteableCounter; | ||
export {}; |
@@ -1,282 +0,161 @@ | ||
/** @hidden **/ | ||
export { /** @hidden */ uuid } from './uuid'; | ||
import { AutomergeValue } from "./types"; | ||
export { AutomergeValue, Text, Counter, Int, Uint, Float64, ScalarValue } from "./types"; | ||
import { type API, type Patch } from "@automerge/automerge-wasm"; | ||
export { type Patch, PutPatch, DelPatch, SplicePatch, IncPatch, SyncMessage, } from "@automerge/automerge-wasm"; | ||
import { Actor as ActorId, Prop, ObjID, Change, DecodedChange, Heads, Automerge, MaterializeValue } from "@automerge/automerge-wasm"; | ||
import { JsSyncState as SyncState, SyncMessage, DecodedSyncMessage } from "@automerge/automerge-wasm"; | ||
/** 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>; | ||
}; | ||
/** | ||
* An automerge document. | ||
* @typeParam T - The type of the value contained in this document | ||
* # Automerge | ||
* | ||
* Note that this provides read only access to the fields of the value. To | ||
* modify the value use {@link change} | ||
*/ | ||
export type Doc<T> = { | ||
readonly [P in keyof T]: T[P]; | ||
}; | ||
/** | ||
* 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 library provides the core automerge data structure and sync algorithms. | ||
* Other libraries can be built on top of this one which provide IO and | ||
* persistence. | ||
* | ||
* This function may mutate `doc` | ||
*/ | ||
export type ChangeFn<T> = (doc: T) => void; | ||
/** | ||
* Callback which is called by various methods in this library to notify the | ||
* user of what changes have been made. | ||
* @param patch - A description of the changes made | ||
* @param before - The document before the change was made | ||
* @param after - The document after the change was made | ||
*/ | ||
export type PatchCallback<T> = (patch: Patch, before: Doc<T>, after: Doc<T>) => void; | ||
/** @hidden **/ | ||
export interface State<T> { | ||
change: DecodedChange; | ||
snapshot: T; | ||
} | ||
/** @hidden **/ | ||
export declare function use(api: API): void; | ||
/** | ||
* 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 */ | ||
export declare function getBackend<T>(doc: Doc<T>): Automerge; | ||
/** | ||
* Create a new automerge document | ||
* An automerge document can be though of an immutable POJO (plain old javascript | ||
* object) which `automerge` tracks the history of, allowing it to be merged with | ||
* any other 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 declare function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T>; | ||
/** | ||
* Make an immutable view of an automerge document as at `heads` | ||
* ## Creating and modifying a document | ||
* | ||
* @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. | ||
* You can create a document with {@link init} or {@link from} and then make | ||
* changes to it with {@link change}, you can merge two documents with {@link | ||
* merge}. | ||
* | ||
* Note that this function will throw an error if any of the hashes in `heads` | ||
* are not in the document. | ||
* ```ts | ||
* import * as automerge from "@automerge/automerge" | ||
* | ||
* @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 declare function view<T>(doc: Doc<T>, heads: Heads): Doc<T>; | ||
/** | ||
* Make a full writable copy of an automerge document | ||
* type DocType = {ideas: Array<automerge.Text>} | ||
* | ||
* @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 | ||
* let doc1 = automerge.init<DocType>() | ||
* doc1 = automerge.change(doc1, d => { | ||
* d.ideas = [new automerge.Text("an immutable document")] | ||
* }) | ||
* | ||
* @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 declare function clone<T>(doc: Doc<T>, _opts?: ActorId | InitOptions<T>): Doc<T>; | ||
/** 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 declare function free<T>(doc: Doc<T>): void; | ||
/** | ||
* 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} | ||
* ] | ||
* let doc2 = automerge.init<DocType>() | ||
* doc2 = automerge.merge(doc2, automerge.clone(doc1)) | ||
* doc2 = automerge.change<DocType>(doc2, d => { | ||
* d.ideas.push(new automerge.Text("which records it's history")) | ||
* }) | ||
* ``` | ||
*/ | ||
export declare function from<T extends Record<string, unknown>>(initialState: T | Doc<T>, actor?: ActorId): Doc<T>; | ||
/** | ||
* 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() | ||
* // Note the `automerge.clone` call, see the "cloning" section of this readme for | ||
* // more detail | ||
* doc1 = automerge.merge(doc1, automerge.clone(doc2)) | ||
* doc1 = automerge.change(doc1, d => { | ||
* d.key = "value" | ||
* d.ideas[0].deleteAt(13, 8) | ||
* d.ideas[0].insertAt(13, "object") | ||
* }) | ||
* assert.equal(doc1.key, "value") | ||
* | ||
* let doc3 = automerge.merge(doc1, doc2) | ||
* // doc3 is now {ideas: ["an immutable object", "which records it's history"]} | ||
* ``` | ||
* | ||
* @example A change with a message | ||
* ## Applying changes from another document | ||
* | ||
* ``` | ||
* doc1 = automerge.change(doc1, "add another value", d => { | ||
* d.key2 = "value2" | ||
* }) | ||
* ``` | ||
* You can get a representation of the result of the last {@link change} you made | ||
* to a document with {@link getLastLocalChange} and you can apply that change to | ||
* another document using {@link applyChanges}. | ||
* | ||
* @example A change with a message and a timestamp | ||
* If you need to get just the changes which are in one document but not in another | ||
* you can use {@link getHeads} to get the heads of the document without the | ||
* changes and then {@link getMissingDeps}, passing the result of {@link getHeads} | ||
* on the document with the changes. | ||
* | ||
* ``` | ||
* doc1 = automerge.change(doc1, {message: "add another value", timestamp: 1640995200}, d => { | ||
* d.key2 = "value2" | ||
* }) | ||
* ``` | ||
* ## Saving and loading documents | ||
* | ||
* @example responding to a patch callback | ||
* ``` | ||
* let patchedPath | ||
* let patchCallback = patch => { | ||
* patchedPath = patch.path | ||
* } | ||
* doc1 = automerge.change(doc1, {message, "add another value", timestamp: 1640995200, patchCallback}, d => { | ||
* d.key2 = "value2" | ||
* You can {@link save} a document to generate a compresed binary representation of | ||
* the document which can be loaded with {@link load}. If you have a document which | ||
* you have recently made changes to you can generate recent changes with {@link | ||
* saveIncremental}, this will generate all the changes since you last called | ||
* `saveIncremental`, the changes generated can be applied to another document with | ||
* {@link loadIncremental}. | ||
* | ||
* ## Viewing different versions of a document | ||
* | ||
* Occasionally you may wish to explicitly step to a different point in a document | ||
* history. One common reason to do this is if you need to obtain a set of changes | ||
* which take the document from one state to another in order to send those changes | ||
* to another peer (or to save them somewhere). You can use {@link view} to do this. | ||
* | ||
* ```ts | ||
* import * as automerge from "@automerge/automerge" | ||
* import * as assert from "assert" | ||
* | ||
* let doc = automerge.from({ | ||
* key1: "value1", | ||
* }) | ||
* assert.equal(patchedPath, ["key2"]) | ||
* ``` | ||
*/ | ||
export declare function change<T>(doc: Doc<T>, options: string | ChangeOptions<T> | ChangeFn<T>, callback?: ChangeFn<T>): Doc<T>; | ||
/** | ||
* 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 | ||
* // Make a clone of the document at this point, maybe this is actually on another | ||
* // peer. | ||
* let doc2 = automerge.clone < any > doc | ||
* | ||
* 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 declare function emptyChange<T>(doc: Doc<T>, options: string | ChangeOptions<T> | void): Doc<T>; | ||
/** | ||
* Load an automerge document from a compressed document produce by {@link save} | ||
* let heads = automerge.getHeads(doc) | ||
* | ||
* @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 | ||
* doc = | ||
* automerge.change < | ||
* any > | ||
* (doc, | ||
* d => { | ||
* d.key2 = "value2" | ||
* }) | ||
* | ||
* 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 declare function load<T>(data: Uint8Array, _opts?: ActorId | InitOptions<T>): Doc<T>; | ||
/** | ||
* Load changes produced by {@link saveIncremental}, or partial changes | ||
* doc = | ||
* automerge.change < | ||
* any > | ||
* (doc, | ||
* d => { | ||
* d.key3 = "value3" | ||
* }) | ||
* | ||
* @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} | ||
* // At this point we've generated two separate changes, now we want to send | ||
* // just those changes to someone else | ||
* | ||
* 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. | ||
* // view is a cheap reference based copy of a document at a given set of heads | ||
* let before = automerge.view(doc, heads) | ||
* | ||
* Note that this function will succesfully load the results of {@link save} as | ||
* well as {@link getLastLocalChange} or any other incremental change. | ||
*/ | ||
export declare function loadIncremental<T>(doc: Doc<T>, data: Uint8Array, opts?: ApplyOptions<T>): Doc<T>; | ||
/** | ||
* Export the contents of a document to a compressed format | ||
* // This view doesn't show the last two changes in the document state | ||
* assert.deepEqual(before, { | ||
* key1: "value1", | ||
* }) | ||
* | ||
* @param doc - The doc to save | ||
* // Get the changes to send to doc2 | ||
* let changes = automerge.getChanges(before, doc) | ||
* | ||
* The returned bytes can be passed to {@link load} or {@link loadIncremental} | ||
*/ | ||
export declare function save<T>(doc: Doc<T>): Uint8Array; | ||
/** | ||
* Merge `local` into `remote` | ||
* @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 | ||
* // Apply the changes at doc2 | ||
* doc2 = automerge.applyChanges < any > (doc2, changes)[0] | ||
* assert.deepEqual(doc2, { | ||
* key1: "value1", | ||
* key2: "value2", | ||
* key3: "value3", | ||
* }) | ||
* ``` | ||
* | ||
* @returns - The merged document | ||
* If you have a {@link view} of a document which you want to make changes to you | ||
* can {@link clone} the viewed 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 declare function merge<T>(local: Doc<T>, remote: Doc<T>): Doc<T>; | ||
/** | ||
* Get the actor ID associated with the document | ||
*/ | ||
export declare function getActorId<T>(doc: Doc<T>): ActorId; | ||
/** | ||
* The type of conflicts for particular key or index | ||
* ## Syncing | ||
* | ||
* 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 | ||
* The sync protocol is stateful. This means that we start by creating a {@link | ||
* SyncState} for each peer we are communicating with using {@link initSyncState}. | ||
* Then we generate a message to send to the peer by calling {@link | ||
* generateSyncMessage}. When we receive a message from the peer we call {@link | ||
* receiveSyncMessage}. Here's a simple example of a loop which just keeps two | ||
* peers in sync. | ||
* | ||
* 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 | ||
* ```ts | ||
* let sync1 = automerge.initSyncState() | ||
* let msg: Uint8Array | null | ||
* ;[sync1, msg] = automerge.generateSyncMessage(doc1, sync1) | ||
* | ||
* 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. | ||
* while (true) { | ||
* if (msg != null) { | ||
* network.send(msg) | ||
* } | ||
* let resp: Uint8Array = | ||
* (network.receive()[(doc1, sync1, _ignore)] = | ||
* automerge.receiveSyncMessage(doc1, sync1, resp)[(sync1, msg)] = | ||
* automerge.generateSyncMessage(doc1, sync1)) | ||
* } | ||
* ``` | ||
* | ||
* Sometimes you may want to examine these conflicts, in this case you can use | ||
* {@link getConflicts} to get the conflicts for the key. | ||
* ## Conflicts | ||
* | ||
* @example | ||
* The only time conflicts occur in automerge documents is in concurrent | ||
* assignments to the same key in an object. In this case automerge | ||
* deterministically chooses an arbitrary value to present to the application but | ||
* you can examine the conflicts using {@link getConflicts}. | ||
* | ||
* ``` | ||
@@ -312,122 +191,53 @@ * import * as automerge from "@automerge/automerge" | ||
* ``` | ||
*/ | ||
export declare function getConflicts<T>(doc: Doc<T>, prop: Prop): Conflicts | 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 declare function getLastLocalChange<T>(doc: Doc<T>): Change | undefined; | ||
/** | ||
* Return the object ID of an arbitrary javascript value | ||
* ## Actor IDs | ||
* | ||
* This is useful to determine if something is actually an automerge document, | ||
* if `doc` is not an automerge document this will return null. | ||
*/ | ||
export declare function getObjectId(doc: any): ObjID | null; | ||
/** | ||
* Get the changes which are in `newState` but not in `oldState`. The returned | ||
* changes can be loaded in `oldState` via {@link applyChanges}. | ||
* By default automerge will generate a random actor ID for you, but most methods | ||
* for creating a document allow you to set the actor ID. You can get the actor ID | ||
* associated with the document by calling {@link getActorId}. Actor IDs must not | ||
* be used in concurrent threads of executiong - all changes by a given actor ID | ||
* are expected to be sequential. | ||
* | ||
* Note that this will crash if there are changes in `oldState` which are not in `newState`. | ||
*/ | ||
export declare function getChanges<T>(oldState: Doc<T>, newState: Doc<T>): Change[]; | ||
/** | ||
* Get all the changes in a document | ||
* ## Listening to patches | ||
* | ||
* This is different to {@link save} because the output is an array of changes | ||
* which can be individually applied via {@link applyChanges}` | ||
* Sometimes you want to respond to changes made to an automerge document. In this | ||
* case you can use the {@link PatchCallback} type to receive notifications when | ||
* changes have been made. | ||
* | ||
*/ | ||
export declare function getAllChanges<T>(doc: Doc<T>): Change[]; | ||
/** | ||
* Apply changes received from another document | ||
* ## Cloning | ||
* | ||
* `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. | ||
* Currently you cannot make mutating changes (i.e. call {@link change}) to a | ||
* document which you have two pointers to. For example, in this code: | ||
* | ||
* 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 | ||
* ```javascript | ||
* let doc1 = automerge.init() | ||
* let doc2 = automerge.change(doc1, d => (d.key = "value")) | ||
* ``` | ||
* | ||
*/ | ||
export declare function applyChanges<T>(doc: Doc<T>, changes: Change[], opts?: ApplyOptions<T>): [Doc<T>]; | ||
/** @hidden */ | ||
export declare function getHistory<T>(doc: Doc<T>): State<T>[]; | ||
/** @hidden */ | ||
export declare function equals(val1: unknown, val2: unknown): boolean; | ||
/** | ||
* encode a {@link SyncState} into binary to send over the network | ||
* `doc1` and `doc2` are both pointers to the same state. Any attempt to call | ||
* mutating methods on `doc1` will now result in an error like | ||
* | ||
* @group sync | ||
* */ | ||
export declare function encodeSyncState(state: SyncState): Uint8Array; | ||
/** | ||
* Decode some binary data into a {@link SyncState} | ||
* Attempting to change an out of date document | ||
* | ||
* @group sync | ||
*/ | ||
export declare function decodeSyncState(state: Uint8Array): SyncState; | ||
/** | ||
* Generate a sync message to send to the peer represented by `inState` | ||
* @param doc - The doc to generate messages about | ||
* @param inState - The {@link SyncState} representing the peer we are talking to | ||
* If you encounter this you need to clone the original document, the above sample | ||
* would work as: | ||
* | ||
* @group sync | ||
* ```javascript | ||
* let doc1 = automerge.init() | ||
* let doc2 = automerge.change(automerge.clone(doc1), d => (d.key = "value")) | ||
* ``` | ||
* @packageDocumentation | ||
* | ||
* @returns An array of `[newSyncState, syncMessage | null]` where | ||
* `newSyncState` should replace `inState` and `syncMessage` should be sent to | ||
* the peer if it is not null. If `syncMessage` is null then we are up to date. | ||
*/ | ||
export declare function generateSyncMessage<T>(doc: Doc<T>, inState: SyncState): [SyncState, SyncMessage | null]; | ||
/** | ||
* Update a document and our sync state on receiving a sync message | ||
* ## The {@link unstable} module | ||
* | ||
* @group sync | ||
* | ||
* @param doc - The doc the sync message is about | ||
* @param inState - The {@link SyncState} for the peer we are communicating with | ||
* @param message - The message which was received | ||
* @param opts - Any {@link ApplyOption}s, used for passing a | ||
* {@link PatchCallback} which will be informed of any changes | ||
* in `doc` which occur because of the received sync message. | ||
* | ||
* @returns An array of `[newDoc, newSyncState, syncMessage | null]` where | ||
* `newDoc` is the updated state of `doc`, `newSyncState` should replace | ||
* `inState` and `syncMessage` should be sent to the peer if it is not null. If | ||
* `syncMessage` is null then we are up to date. | ||
* We are working on some changes to automerge which are not yet complete and | ||
* will result in backwards incompatible API changes. Once these changes are | ||
* ready for production use we will release a new major version of automerge. | ||
* However, until that point you can use the {@link unstable} module to try out | ||
* the new features, documents from the {@link unstable} module are | ||
* interoperable with documents from the main module. Please see the docs for | ||
* the {@link unstable} module for more details. | ||
*/ | ||
export declare function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message: SyncMessage, opts?: ApplyOptions<T>): [Doc<T>, SyncState, null]; | ||
/** | ||
* Create a new, blank {@link SyncState} | ||
* | ||
* When communicating with a peer for the first time use this to generate a new | ||
* {@link SyncState} for them | ||
* | ||
* @group sync | ||
*/ | ||
export declare function initSyncState(): SyncState; | ||
/** @hidden */ | ||
export declare function encodeChange(change: DecodedChange): Change; | ||
/** @hidden */ | ||
export declare function decodeChange(data: Change): DecodedChange; | ||
/** @hidden */ | ||
export declare function encodeSyncMessage(message: DecodedSyncMessage): SyncMessage; | ||
/** @hidden */ | ||
export declare function decodeSyncMessage(message: SyncMessage): DecodedSyncMessage; | ||
/** | ||
* Get any changes in `doc` which are not dependencies of `heads` | ||
*/ | ||
export declare function getMissingDeps<T>(doc: Doc<T>, heads: Heads): Heads; | ||
/** | ||
* Get the hashes of the heads of this document | ||
*/ | ||
export declare function getHeads<T>(doc: Doc<T>): Heads; | ||
/** @hidden */ | ||
export declare function dump<T>(doc: Doc<T>): void; | ||
/** @hidden */ | ||
export declare function toJS<T>(doc: Doc<T>): T; | ||
export declare function isAutomerge(doc: unknown): boolean; | ||
export type { API, SyncState, ActorId, Conflicts, Prop, Change, ObjID, DecodedChange, DecodedSyncMessage, Heads, MaterializeValue }; | ||
export * from "./stable"; | ||
import * as unstable from "./unstable"; | ||
export { unstable }; |
@@ -0,3 +1,4 @@ | ||
export { ChangeToEncode } from "@automerge/automerge-wasm"; | ||
import { API } from "@automerge/automerge-wasm"; | ||
export declare function UseApi(api: API): void; | ||
export declare const ApiHandler: API; |
// Properties of the document root object | ||
//const OPTIONS = Symbol('_options') // object containing options passed to init() | ||
//const CACHE = Symbol('_cache') // map from objectId to immutable object | ||
//export const STATE = Symbol.for('_am_state') // object containing metadata about current state (e.g. sequence numbers) | ||
export const STATE = Symbol.for('_am_meta'); // object containing metadata about current state (e.g. sequence numbers) | ||
export const HEADS = Symbol.for('_am_heads'); // object containing metadata about current state (e.g. sequence numbers) | ||
export const TRACE = Symbol.for('_am_trace'); // object containing metadata about current state (e.g. sequence numbers) | ||
export const OBJECT_ID = Symbol.for('_am_objectId'); // object containing metadata about current state (e.g. sequence numbers) | ||
export const READ_ONLY = Symbol.for('_am_readOnly'); // object containing metadata about current state (e.g. sequence numbers) | ||
export const FROZEN = Symbol.for('_am_frozen'); // object containing metadata about current state (e.g. sequence numbers) | ||
export const UINT = Symbol.for('_am_uint'); | ||
export const INT = Symbol.for('_am_int'); | ||
export const F64 = Symbol.for('_am_f64'); | ||
export const COUNTER = Symbol.for('_am_counter'); | ||
export const TEXT = Symbol.for('_am_text'); | ||
// Properties of all Automerge objects | ||
//const OBJECT_ID = Symbol('_objectId') // the object ID of the current object (string) | ||
//const CONFLICTS = Symbol('_conflicts') // map or list (depending on object type) of conflicts | ||
//const CHANGE = Symbol('_change') // the context object on proxy objects used in change callback | ||
//const ELEM_IDS = Symbol('_elemIds') // list containing the element ID of each list element | ||
export const STATE = Symbol.for("_am_meta"); // symbol used to hide application metadata on automerge objects | ||
export const TRACE = Symbol.for("_am_trace"); // used for debugging | ||
export const OBJECT_ID = Symbol.for("_am_objectId"); // synbol used to hide the object id on automerge objects | ||
export const IS_PROXY = Symbol.for("_am_isProxy"); // symbol used to test if the document is a proxy object | ||
export const UINT = Symbol.for("_am_uint"); | ||
export const INT = Symbol.for("_am_int"); | ||
export const F64 = Symbol.for("_am_f64"); | ||
export const COUNTER = Symbol.for("_am_counter"); | ||
export const TEXT = Symbol.for("_am_text"); |
@@ -56,3 +56,3 @@ import { COUNTER } from "./constants"; | ||
increment(delta) { | ||
delta = typeof delta === 'number' ? delta : 1; | ||
delta = typeof delta === "number" ? delta : 1; | ||
this.context.increment(this.objectId, this.key, delta); | ||
@@ -67,3 +67,3 @@ this.value += delta; | ||
decrement(delta) { | ||
return this.increment(typeof delta === 'number' ? -delta : -1); | ||
return this.increment(typeof delta === "number" ? -delta : -1); | ||
} | ||
@@ -77,3 +77,3 @@ } | ||
* located. | ||
*/ | ||
*/ | ||
export function getWriteableCounter(value, context, path, objectId, key) { | ||
@@ -80,0 +80,0 @@ return new WriteableCounter(value, context, path, objectId, key); |
@@ -1,450 +0,161 @@ | ||
var __rest = (this && this.__rest) || function (s, e) { | ||
var t = {}; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) | ||
t[p] = s[p]; | ||
if (s != null && typeof Object.getOwnPropertySymbols === "function") | ||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { | ||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) | ||
t[p[i]] = s[p[i]]; | ||
} | ||
return t; | ||
}; | ||
/** @hidden **/ | ||
export { /** @hidden */ uuid } from './uuid'; | ||
import { rootProxy, listProxy, textProxy, mapProxy } from "./proxies"; | ||
import { STATE, HEADS, TRACE, OBJECT_ID, READ_ONLY, FROZEN } from "./constants"; | ||
import { Text, Counter } from "./types"; | ||
export { Text, Counter, Int, Uint, Float64 } from "./types"; | ||
import { ApiHandler, UseApi } from "./low_level"; | ||
/** @hidden **/ | ||
export function use(api) { | ||
UseApi(api); | ||
} | ||
import * as wasm from "@automerge/automerge-wasm"; | ||
use(wasm); | ||
/** @hidden */ | ||
export function getBackend(doc) { | ||
return _state(doc).handle; | ||
} | ||
function _state(doc, checkroot = true) { | ||
if (typeof doc !== 'object') { | ||
throw new RangeError("must be the document root"); | ||
} | ||
const state = Reflect.get(doc, STATE); | ||
if (state === undefined || state == null || (checkroot && _obj(doc) !== "_root")) { | ||
throw new RangeError("must be the document root"); | ||
} | ||
return state; | ||
} | ||
function _frozen(doc) { | ||
return Reflect.get(doc, FROZEN) === true; | ||
} | ||
function _trace(doc) { | ||
return Reflect.get(doc, TRACE); | ||
} | ||
function _set_heads(doc, heads) { | ||
_state(doc).heads = heads; | ||
} | ||
function _clear_heads(doc) { | ||
Reflect.set(doc, HEADS, undefined); | ||
Reflect.set(doc, TRACE, undefined); | ||
} | ||
function _obj(doc) { | ||
if (!(typeof doc === 'object') || doc === null) { | ||
return null; | ||
} | ||
return Reflect.get(doc, OBJECT_ID); | ||
} | ||
function _readonly(doc) { | ||
return Reflect.get(doc, READ_ONLY) !== false; | ||
} | ||
function importOpts(_actor) { | ||
if (typeof _actor === 'object') { | ||
return _actor; | ||
} | ||
else { | ||
return { actor: _actor }; | ||
} | ||
} | ||
/** | ||
* Create a new automerge document | ||
* # Automerge | ||
* | ||
* @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(_opts) { | ||
let opts = importOpts(_opts); | ||
let freeze = !!opts.freeze; | ||
let patchCallback = opts.patchCallback; | ||
const handle = ApiHandler.create(opts.actor); | ||
handle.enablePatches(true); | ||
handle.enableFreeze(!!opts.freeze); | ||
handle.registerDatatype("counter", (n) => new Counter(n)); | ||
handle.registerDatatype("text", (n) => new Text(n)); | ||
const doc = handle.materialize("/", undefined, { handle, heads: undefined, freeze, patchCallback }); | ||
return doc; | ||
} | ||
/** | ||
* Make an immutable view of an automerge document as at `heads` | ||
* This library provides the core automerge data structure and sync algorithms. | ||
* Other libraries can be built on top of this one which provide IO and | ||
* persistence. | ||
* | ||
* @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. | ||
* An automerge document can be though of an immutable POJO (plain old javascript | ||
* object) which `automerge` tracks the history of, allowing it to be merged with | ||
* any other automerge document. | ||
* | ||
* Note that this function will throw an error if any of the hashes in `heads` | ||
* are not in the document. | ||
* ## Creating and modifying a 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(doc, heads) { | ||
const state = _state(doc); | ||
const handle = state.handle; | ||
return state.handle.materialize("/", heads, Object.assign(Object.assign({}, state), { handle, heads })); | ||
} | ||
/** | ||
* Make a full writable copy of an automerge document | ||
* You can create a document with {@link init} or {@link from} and then make | ||
* changes to it with {@link change}, you can merge two documents with {@link | ||
* merge}. | ||
* | ||
* @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 | ||
* ```ts | ||
* import * as automerge from "@automerge/automerge" | ||
* | ||
* @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(doc, _opts) { | ||
const state = _state(doc); | ||
const heads = state.heads; | ||
const opts = importOpts(_opts); | ||
const handle = state.handle.fork(opts.actor, heads); | ||
// `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 } = state, stateSansHeads = __rest(state, ["heads"]); | ||
return handle.applyPatches(doc, Object.assign(Object.assign({}, 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(doc) { | ||
return _state(doc).handle.free(); | ||
} | ||
/** | ||
* Create an automerge document from a POJO | ||
* type DocType = {ideas: Array<automerge.Text>} | ||
* | ||
* @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 | ||
* let doc1 = automerge.init<DocType>() | ||
* doc1 = automerge.change(doc1, d => { | ||
* d.ideas = [new automerge.Text("an immutable document")] | ||
* }) | ||
* | ||
* @example | ||
* ``` | ||
* const doc = automerge.from({ | ||
* tasks: [ | ||
* {description: "feed dogs", done: false} | ||
* ] | ||
* let doc2 = automerge.init<DocType>() | ||
* doc2 = automerge.merge(doc2, automerge.clone(doc1)) | ||
* doc2 = automerge.change<DocType>(doc2, d => { | ||
* d.ideas.push(new automerge.Text("which records it's history")) | ||
* }) | ||
* ``` | ||
*/ | ||
export function from(initialState, actor) { | ||
return change(init(actor), (d) => Object.assign(d, initialState)); | ||
} | ||
/** | ||
* 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() | ||
* // Note the `automerge.clone` call, see the "cloning" section of this readme for | ||
* // more detail | ||
* doc1 = automerge.merge(doc1, automerge.clone(doc2)) | ||
* doc1 = automerge.change(doc1, d => { | ||
* d.key = "value" | ||
* d.ideas[0].deleteAt(13, 8) | ||
* d.ideas[0].insertAt(13, "object") | ||
* }) | ||
* assert.equal(doc1.key, "value") | ||
* | ||
* let doc3 = automerge.merge(doc1, doc2) | ||
* // doc3 is now {ideas: ["an immutable object", "which records it's history"]} | ||
* ``` | ||
* | ||
* @example A change with a message | ||
* ## Applying changes from another document | ||
* | ||
* ``` | ||
* doc1 = automerge.change(doc1, "add another value", d => { | ||
* d.key2 = "value2" | ||
* }) | ||
* ``` | ||
* You can get a representation of the result of the last {@link change} you made | ||
* to a document with {@link getLastLocalChange} and you can apply that change to | ||
* another document using {@link applyChanges}. | ||
* | ||
* @example A change with a message and a timestamp | ||
* If you need to get just the changes which are in one document but not in another | ||
* you can use {@link getHeads} to get the heads of the document without the | ||
* changes and then {@link getMissingDeps}, passing the result of {@link getHeads} | ||
* on the document with the changes. | ||
* | ||
* ``` | ||
* doc1 = automerge.change(doc1, {message: "add another value", timestamp: 1640995200}, d => { | ||
* d.key2 = "value2" | ||
* ## Saving and loading documents | ||
* | ||
* You can {@link save} a document to generate a compresed binary representation of | ||
* the document which can be loaded with {@link load}. If you have a document which | ||
* you have recently made changes to you can generate recent changes with {@link | ||
* saveIncremental}, this will generate all the changes since you last called | ||
* `saveIncremental`, the changes generated can be applied to another document with | ||
* {@link loadIncremental}. | ||
* | ||
* ## Viewing different versions of a document | ||
* | ||
* Occasionally you may wish to explicitly step to a different point in a document | ||
* history. One common reason to do this is if you need to obtain a set of changes | ||
* which take the document from one state to another in order to send those changes | ||
* to another peer (or to save them somewhere). You can use {@link view} to do this. | ||
* | ||
* ```ts | ||
* import * as automerge from "@automerge/automerge" | ||
* import * as assert from "assert" | ||
* | ||
* let doc = automerge.from({ | ||
* key1: "value1", | ||
* }) | ||
* ``` | ||
* | ||
* @example responding to a patch callback | ||
* ``` | ||
* let patchedPath | ||
* let patchCallback = patch => { | ||
* patchedPath = patch.path | ||
* } | ||
* doc1 = automerge.change(doc1, {message, "add another value", timestamp: 1640995200, patchCallback}, d => { | ||
* // Make a clone of the document at this point, maybe this is actually on another | ||
* // peer. | ||
* let doc2 = automerge.clone < any > doc | ||
* | ||
* let heads = automerge.getHeads(doc) | ||
* | ||
* doc = | ||
* automerge.change < | ||
* any > | ||
* (doc, | ||
* d => { | ||
* d.key2 = "value2" | ||
* }) | ||
* assert.equal(patchedPath, ["key2"]) | ||
* ``` | ||
*/ | ||
export function change(doc, options, callback) { | ||
if (typeof options === 'function') { | ||
return _change(doc, {}, options); | ||
} | ||
else if (typeof callback === 'function') { | ||
if (typeof options === "string") { | ||
options = { message: options }; | ||
} | ||
return _change(doc, options, callback); | ||
} | ||
else { | ||
throw RangeError("Invalid args for change"); | ||
} | ||
} | ||
function progressDocument(doc, heads, callback) { | ||
if (heads == null) { | ||
return doc; | ||
} | ||
let state = _state(doc); | ||
let nextState = Object.assign(Object.assign({}, state), { heads: undefined }); | ||
let nextDoc = state.handle.applyPatches(doc, nextState, callback); | ||
state.heads = heads; | ||
return nextDoc; | ||
} | ||
function _change(doc, options, callback) { | ||
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 (_readonly(doc) === false) { | ||
throw new RangeError("Calls to Automerge.change cannot be nested"); | ||
} | ||
const heads = state.handle.getHeads(); | ||
try { | ||
state.heads = heads; | ||
const root = rootProxy(state.handle); | ||
callback(root); | ||
if (state.handle.pendingOps() === 0) { | ||
state.heads = undefined; | ||
return doc; | ||
} | ||
else { | ||
state.handle.commit(options.message, options.time); | ||
return progressDocument(doc, heads, options.patchCallback || state.patchCallback); | ||
} | ||
} | ||
catch (e) { | ||
//console.log("ERROR: ",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 | ||
* doc = | ||
* automerge.change < | ||
* any > | ||
* (doc, | ||
* d => { | ||
* d.key3 = "value3" | ||
* }) | ||
* | ||
* 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(doc, options) { | ||
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 (_readonly(doc) === false) { | ||
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, heads); | ||
} | ||
/** | ||
* Load an automerge document from a compressed document produce by {@link save} | ||
* // At this point we've generated two separate changes, now we want to send | ||
* // just those changes to someone else | ||
* | ||
* @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 | ||
* // view is a cheap reference based copy of a document at a given set of heads | ||
* let before = automerge.view(doc, heads) | ||
* | ||
* 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(data, _opts) { | ||
const opts = importOpts(_opts); | ||
const actor = opts.actor; | ||
const patchCallback = opts.patchCallback; | ||
const handle = ApiHandler.load(data, actor); | ||
handle.enablePatches(true); | ||
handle.enableFreeze(!!opts.freeze); | ||
handle.registerDatatype("counter", (n) => new Counter(n)); | ||
handle.registerDatatype("text", (n) => new Text(n)); | ||
const doc = handle.materialize("/", undefined, { handle, heads: undefined, patchCallback }); | ||
return doc; | ||
} | ||
/** | ||
* Load changes produced by {@link saveIncremental}, or partial changes | ||
* // This view doesn't show the last two changes in the document state | ||
* assert.deepEqual(before, { | ||
* key1: "value1", | ||
* }) | ||
* | ||
* @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} | ||
* // Get the changes to send to doc2 | ||
* let changes = automerge.getChanges(before, doc) | ||
* | ||
* 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. | ||
* // Apply the changes at doc2 | ||
* doc2 = automerge.applyChanges < any > (doc2, changes)[0] | ||
* assert.deepEqual(doc2, { | ||
* key1: "value1", | ||
* key2: "value2", | ||
* key3: "value3", | ||
* }) | ||
* ``` | ||
* | ||
* 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(doc, data, opts) { | ||
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 (_readonly(doc) === false) { | ||
throw new RangeError("Calls to Automerge.change cannot be nested"); | ||
} | ||
const heads = state.handle.getHeads(); | ||
state.handle.loadIncremental(data); | ||
return progressDocument(doc, heads, opts.patchCallback || state.patchCallback); | ||
} | ||
/** | ||
* Export the contents of a document to a compressed format | ||
* If you have a {@link view} of a document which you want to make changes to you | ||
* can {@link clone} the viewed document. | ||
* | ||
* @param doc - The doc to save | ||
* ## Syncing | ||
* | ||
* The returned bytes can be passed to {@link load} or {@link loadIncremental} | ||
*/ | ||
export function save(doc) { | ||
return _state(doc).handle.save(); | ||
} | ||
/** | ||
* Merge `local` into `remote` | ||
* @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 | ||
* The sync protocol is stateful. This means that we start by creating a {@link | ||
* SyncState} for each peer we are communicating with using {@link initSyncState}. | ||
* Then we generate a message to send to the peer by calling {@link | ||
* generateSyncMessage}. When we receive a message from the peer we call {@link | ||
* receiveSyncMessage}. Here's a simple example of a loop which just keeps two | ||
* peers in sync. | ||
* | ||
* @returns - The merged document | ||
* ```ts | ||
* let sync1 = automerge.initSyncState() | ||
* let msg: Uint8Array | null | ||
* ;[sync1, msg] = automerge.generateSyncMessage(doc1, sync1) | ||
* | ||
* 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(local, remote) { | ||
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, heads, localState.patchCallback); | ||
} | ||
/** | ||
* Get the actor ID associated with the document | ||
*/ | ||
export function getActorId(doc) { | ||
const state = _state(doc); | ||
return state.handle.getActorId(); | ||
} | ||
function conflictAt(context, objectId, prop) { | ||
const values = context.getAll(objectId, prop); | ||
if (values.length <= 1) { | ||
return; | ||
} | ||
const result = {}; | ||
for (const fullVal of values) { | ||
switch (fullVal[0]) { | ||
case "map": | ||
result[fullVal[1]] = mapProxy(context, fullVal[1], [prop], true); | ||
break; | ||
case "list": | ||
result[fullVal[1]] = listProxy(context, fullVal[1], [prop], true); | ||
break; | ||
case "text": | ||
result[fullVal[1]] = textProxy(context, fullVal[1], [prop], true); | ||
break; | ||
//case "table": | ||
//case "cursor": | ||
case "str": | ||
case "uint": | ||
case "int": | ||
case "f64": | ||
case "boolean": | ||
case "bytes": | ||
case "null": | ||
result[fullVal[2]] = fullVal[1]; | ||
break; | ||
case "counter": | ||
result[fullVal[2]] = new Counter(fullVal[1]); | ||
break; | ||
case "timestamp": | ||
result[fullVal[2]] = new Date(fullVal[1]); | ||
break; | ||
default: | ||
throw RangeError(`datatype ${fullVal[0]} unimplemented`); | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* Get the conflicts associated with a property | ||
* while (true) { | ||
* if (msg != null) { | ||
* network.send(msg) | ||
* } | ||
* let resp: Uint8Array = | ||
* (network.receive()[(doc1, sync1, _ignore)] = | ||
* automerge.receiveSyncMessage(doc1, sync1, resp)[(sync1, msg)] = | ||
* automerge.generateSyncMessage(doc1, sync1)) | ||
* } | ||
* ``` | ||
* | ||
* 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. | ||
* ## Conflicts | ||
* | ||
* Sometimes you may want to examine these conflicts, in this case you can use | ||
* {@link getConflicts} to get the conflicts for the key. | ||
* The only time conflicts occur in automerge documents is in concurrent | ||
* assignments to the same key in an object. In this case automerge | ||
* deterministically chooses an arbitrary value to present to the application but | ||
* you can examine the conflicts using {@link getConflicts}. | ||
* | ||
* @example | ||
* ``` | ||
@@ -480,245 +191,53 @@ * import * as automerge from "@automerge/automerge" | ||
* ``` | ||
*/ | ||
export function getConflicts(doc, prop) { | ||
const state = _state(doc, false); | ||
const objectId = _obj(doc); | ||
if (objectId != null) { | ||
return conflictAt(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(doc) { | ||
const state = _state(doc); | ||
return state.handle.getLastLocalChange() || undefined; | ||
} | ||
/** | ||
* Return the object ID of an arbitrary javascript value | ||
* ## Actor IDs | ||
* | ||
* This is useful to determine if something is actually an automerge document, | ||
* if `doc` is not an automerge document this will return null. | ||
*/ | ||
export function getObjectId(doc) { | ||
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}. | ||
* By default automerge will generate a random actor ID for you, but most methods | ||
* for creating a document allow you to set the actor ID. You can get the actor ID | ||
* associated with the document by calling {@link getActorId}. Actor IDs must not | ||
* be used in concurrent threads of executiong - all changes by a given actor ID | ||
* are expected to be sequential. | ||
* | ||
* Note that this will crash if there are changes in `oldState` which are not in `newState`. | ||
*/ | ||
export function getChanges(oldState, newState) { | ||
const o = _state(oldState); | ||
const n = _state(newState); | ||
return n.handle.getChanges(getHeads(oldState)); | ||
} | ||
/** | ||
* Get all the changes in a document | ||
* ## Listening to patches | ||
* | ||
* This is different to {@link save} because the output is an array of changes | ||
* which can be individually applied via {@link applyChanges}` | ||
* Sometimes you want to respond to changes made to an automerge document. In this | ||
* case you can use the {@link PatchCallback} type to receive notifications when | ||
* changes have been made. | ||
* | ||
*/ | ||
export function getAllChanges(doc) { | ||
const state = _state(doc); | ||
return state.handle.getChanges([]); | ||
} | ||
/** | ||
* Apply changes received from another document | ||
* ## Cloning | ||
* | ||
* `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. | ||
* Currently you cannot make mutating changes (i.e. call {@link change}) to a | ||
* document which you have two pointers to. For example, in this code: | ||
* | ||
* 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 | ||
* ```javascript | ||
* let doc1 = automerge.init() | ||
* let doc2 = automerge.change(doc1, d => (d.key = "value")) | ||
* ``` | ||
* | ||
*/ | ||
export function applyChanges(doc, changes, opts) { | ||
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 (_readonly(doc) === false) { | ||
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, heads, opts.patchCallback || state.patchCallback)]; | ||
} | ||
/** @hidden */ | ||
export function getHistory(doc) { | ||
const history = getAllChanges(doc); | ||
return history.map((change, index) => ({ | ||
get change() { | ||
return decodeChange(change); | ||
}, | ||
get snapshot() { | ||
const [state] = applyChanges(init(), history.slice(0, index + 1)); | ||
return state; | ||
} | ||
})); | ||
} | ||
/** @hidden */ | ||
// FIXME : no tests | ||
// FIXME can we just use deep equals now? | ||
export function equals(val1, val2) { | ||
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 | ||
* `doc1` and `doc2` are both pointers to the same state. Any attempt to call | ||
* mutating methods on `doc1` will now result in an error like | ||
* | ||
* @group sync | ||
* */ | ||
export function encodeSyncState(state) { | ||
const sync = ApiHandler.importSyncState(state); | ||
const result = ApiHandler.encodeSyncState(sync); | ||
sync.free(); | ||
return result; | ||
} | ||
/** | ||
* Decode some binary data into a {@link SyncState} | ||
* Attempting to change an out of date document | ||
* | ||
* @group sync | ||
*/ | ||
export function decodeSyncState(state) { | ||
let sync = ApiHandler.decodeSyncState(state); | ||
let result = ApiHandler.exportSyncState(sync); | ||
sync.free(); | ||
return result; | ||
} | ||
/** | ||
* Generate a sync message to send to the peer represented by `inState` | ||
* @param doc - The doc to generate messages about | ||
* @param inState - The {@link SyncState} representing the peer we are talking to | ||
* If you encounter this you need to clone the original document, the above sample | ||
* would work as: | ||
* | ||
* @group sync | ||
* ```javascript | ||
* let doc1 = automerge.init() | ||
* let doc2 = automerge.change(automerge.clone(doc1), d => (d.key = "value")) | ||
* ``` | ||
* @packageDocumentation | ||
* | ||
* @returns An array of `[newSyncState, syncMessage | null]` where | ||
* `newSyncState` should replace `inState` and `syncMessage` should be sent to | ||
* the peer if it is not null. If `syncMessage` is null then we are up to date. | ||
*/ | ||
export function generateSyncMessage(doc, inState) { | ||
const state = _state(doc); | ||
const syncState = ApiHandler.importSyncState(inState); | ||
const message = state.handle.generateSyncMessage(syncState); | ||
const outState = ApiHandler.exportSyncState(syncState); | ||
return [outState, message]; | ||
} | ||
/** | ||
* Update a document and our sync state on receiving a sync message | ||
* ## The {@link unstable} module | ||
* | ||
* @group sync | ||
* | ||
* @param doc - The doc the sync message is about | ||
* @param inState - The {@link SyncState} for the peer we are communicating with | ||
* @param message - The message which was received | ||
* @param opts - Any {@link ApplyOption}s, used for passing a | ||
* {@link PatchCallback} which will be informed of any changes | ||
* in `doc` which occur because of the received sync message. | ||
* | ||
* @returns An array of `[newDoc, newSyncState, syncMessage | null]` where | ||
* `newDoc` is the updated state of `doc`, `newSyncState` should replace | ||
* `inState` and `syncMessage` should be sent to the peer if it is not null. If | ||
* `syncMessage` is null then we are up to date. | ||
* We are working on some changes to automerge which are not yet complete and | ||
* will result in backwards incompatible API changes. Once these changes are | ||
* ready for production use we will release a new major version of automerge. | ||
* However, until that point you can use the {@link unstable} module to try out | ||
* the new features, documents from the {@link unstable} module are | ||
* interoperable with documents from the main module. Please see the docs for | ||
* the {@link unstable} module for more details. | ||
*/ | ||
export function receiveSyncMessage(doc, inState, message, opts) { | ||
const syncState = ApiHandler.importSyncState(inState); | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
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 (_readonly(doc) === false) { | ||
throw new RangeError("Calls to Automerge.change cannot be nested"); | ||
} | ||
const heads = state.handle.getHeads(); | ||
state.handle.receiveSyncMessage(syncState, message); | ||
const outSyncState = ApiHandler.exportSyncState(syncState); | ||
return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback), outSyncState, null]; | ||
} | ||
/** | ||
* Create a new, blank {@link SyncState} | ||
* | ||
* When communicating with a peer for the first time use this to generate a new | ||
* {@link SyncState} for them | ||
* | ||
* @group sync | ||
*/ | ||
export function initSyncState() { | ||
return ApiHandler.exportSyncState(ApiHandler.initSyncState()); | ||
} | ||
/** @hidden */ | ||
export function encodeChange(change) { | ||
return ApiHandler.encodeChange(change); | ||
} | ||
/** @hidden */ | ||
export function decodeChange(data) { | ||
return ApiHandler.decodeChange(data); | ||
} | ||
/** @hidden */ | ||
export function encodeSyncMessage(message) { | ||
return ApiHandler.encodeSyncMessage(message); | ||
} | ||
/** @hidden */ | ||
export function decodeSyncMessage(message) { | ||
return ApiHandler.decodeSyncMessage(message); | ||
} | ||
/** | ||
* Get any changes in `doc` which are not dependencies of `heads` | ||
*/ | ||
export function getMissingDeps(doc, heads) { | ||
const state = _state(doc); | ||
return state.handle.getMissingDeps(heads); | ||
} | ||
/** | ||
* Get the hashes of the heads of this document | ||
*/ | ||
export function getHeads(doc) { | ||
const state = _state(doc); | ||
return state.heads || state.handle.getHeads(); | ||
} | ||
/** @hidden */ | ||
export function dump(doc) { | ||
const state = _state(doc); | ||
state.handle.dump(); | ||
} | ||
/** @hidden */ | ||
export function toJS(doc) { | ||
const state = _state(doc); | ||
const enabled = state.handle.enableFreeze(false); | ||
const result = state.handle.materialize(); | ||
state.handle.enableFreeze(enabled); | ||
return result; | ||
} | ||
export function isAutomerge(doc) { | ||
return getObjectId(doc) === "_root" && !!Reflect.get(doc, STATE); | ||
} | ||
function isObject(obj) { | ||
return typeof obj === 'object' && obj !== null; | ||
} | ||
export * from "./stable"; | ||
import * as unstable from "./unstable"; | ||
export { unstable }; |
@@ -8,14 +8,36 @@ export function UseApi(api) { | ||
export const ApiHandler = { | ||
create(actor) { throw new RangeError("Automerge.use() not called"); }, | ||
load(data, actor) { throw new RangeError("Automerge.use() not called (load)"); }, | ||
encodeChange(change) { throw new RangeError("Automerge.use() not called (encodeChange)"); }, | ||
decodeChange(change) { throw new RangeError("Automerge.use() not called (decodeChange)"); }, | ||
initSyncState() { throw new RangeError("Automerge.use() not called (initSyncState)"); }, | ||
encodeSyncMessage(message) { throw new RangeError("Automerge.use() not called (encodeSyncMessage)"); }, | ||
decodeSyncMessage(msg) { throw new RangeError("Automerge.use() not called (decodeSyncMessage)"); }, | ||
encodeSyncState(state) { throw new RangeError("Automerge.use() not called (encodeSyncState)"); }, | ||
decodeSyncState(data) { throw new RangeError("Automerge.use() not called (decodeSyncState)"); }, | ||
exportSyncState(state) { throw new RangeError("Automerge.use() not called (exportSyncState)"); }, | ||
importSyncState(state) { throw new RangeError("Automerge.use() not called (importSyncState)"); }, | ||
create(textV2, actor) { | ||
throw new RangeError("Automerge.use() not called"); | ||
}, | ||
load(data, textV2, actor) { | ||
throw new RangeError("Automerge.use() not called (load)"); | ||
}, | ||
encodeChange(change) { | ||
throw new RangeError("Automerge.use() not called (encodeChange)"); | ||
}, | ||
decodeChange(change) { | ||
throw new RangeError("Automerge.use() not called (decodeChange)"); | ||
}, | ||
initSyncState() { | ||
throw new RangeError("Automerge.use() not called (initSyncState)"); | ||
}, | ||
encodeSyncMessage(message) { | ||
throw new RangeError("Automerge.use() not called (encodeSyncMessage)"); | ||
}, | ||
decodeSyncMessage(msg) { | ||
throw new RangeError("Automerge.use() not called (decodeSyncMessage)"); | ||
}, | ||
encodeSyncState(state) { | ||
throw new RangeError("Automerge.use() not called (encodeSyncState)"); | ||
}, | ||
decodeSyncState(data) { | ||
throw new RangeError("Automerge.use() not called (decodeSyncState)"); | ||
}, | ||
exportSyncState(state) { | ||
throw new RangeError("Automerge.use() not called (exportSyncState)"); | ||
}, | ||
importSyncState(state) { | ||
throw new RangeError("Automerge.use() not called (importSyncState)"); | ||
}, | ||
}; | ||
/* eslint-enable */ |
@@ -5,3 +5,5 @@ // Convience classes to allow users to stricly specify the number type they want | ||
constructor(value) { | ||
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER)) { | ||
if (!(Number.isInteger(value) && | ||
value <= Number.MAX_SAFE_INTEGER && | ||
value >= Number.MIN_SAFE_INTEGER)) { | ||
throw new RangeError(`Value ${value} cannot be a uint`); | ||
@@ -16,3 +18,5 @@ } | ||
constructor(value) { | ||
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= 0)) { | ||
if (!(Number.isInteger(value) && | ||
value <= Number.MAX_SAFE_INTEGER && | ||
value >= 0)) { | ||
throw new RangeError(`Value ${value} cannot be a uint`); | ||
@@ -27,3 +31,3 @@ } | ||
constructor(value) { | ||
if (typeof value !== 'number') { | ||
if (typeof value !== "number") { | ||
throw new RangeError(`Value ${value} cannot be a float64`); | ||
@@ -30,0 +34,0 @@ } |
@@ -0,13 +1,13 @@ | ||
import { Text } from "./text"; | ||
import { Counter, getWriteableCounter } from "./counter"; | ||
import { Text } from "./text"; | ||
import { STATE, HEADS, TRACE, FROZEN, OBJECT_ID, READ_ONLY, COUNTER, INT, UINT, F64, TEXT } from "./constants"; | ||
import { STATE, TRACE, IS_PROXY, OBJECT_ID, COUNTER, INT, UINT, F64, } from "./constants"; | ||
import { RawString } from "./raw_string"; | ||
function parseListIndex(key) { | ||
if (typeof key === 'string' && /^[0-9]+$/.test(key)) | ||
if (typeof key === "string" && /^[0-9]+$/.test(key)) | ||
key = parseInt(key, 10); | ||
if (typeof key !== 'number') { | ||
// throw new TypeError('A list index must be a number, but you passed ' + JSON.stringify(key)) | ||
if (typeof key !== "number") { | ||
return key; | ||
} | ||
if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) { | ||
throw new RangeError('A list index must be positive, but you passed ' + key); | ||
throw new RangeError("A list index must be positive, but you passed " + key); | ||
} | ||
@@ -17,3 +17,3 @@ return key; | ||
function valueAt(target, prop) { | ||
const { context, objectId, path, readonly, heads } = target; | ||
const { context, objectId, path, readonly, heads, textV2 } = target; | ||
const value = context.getWithType(objectId, prop, heads); | ||
@@ -26,16 +26,31 @@ if (value === null) { | ||
switch (datatype) { | ||
case undefined: return; | ||
case "map": return mapProxy(context, val, [...path, prop], readonly, heads); | ||
case "list": return listProxy(context, val, [...path, prop], readonly, heads); | ||
case "text": return textProxy(context, val, [...path, prop], readonly, heads); | ||
//case "table": | ||
//case "cursor": | ||
case "str": return val; | ||
case "uint": return val; | ||
case "int": return val; | ||
case "f64": return val; | ||
case "boolean": return val; | ||
case "null": return null; | ||
case "bytes": return val; | ||
case "timestamp": return val; | ||
case undefined: | ||
return; | ||
case "map": | ||
return mapProxy(context, val, textV2, [...path, prop], readonly, heads); | ||
case "list": | ||
return listProxy(context, val, textV2, [...path, prop], readonly, heads); | ||
case "text": | ||
if (textV2) { | ||
return context.text(val, heads); | ||
} | ||
else { | ||
return textProxy(context, val, [...path, prop], readonly, heads); | ||
} | ||
case "str": | ||
return val; | ||
case "uint": | ||
return val; | ||
case "int": | ||
return val; | ||
case "f64": | ||
return val; | ||
case "boolean": | ||
return val; | ||
case "null": | ||
return null; | ||
case "bytes": | ||
return val; | ||
case "timestamp": | ||
return val; | ||
case "counter": { | ||
@@ -53,5 +68,5 @@ if (readonly) { | ||
} | ||
function import_value(value) { | ||
function import_value(value, textV2) { | ||
switch (typeof value) { | ||
case 'object': | ||
case "object": | ||
if (value == null) { | ||
@@ -72,8 +87,11 @@ return [null, "null"]; | ||
} | ||
else if (value[TEXT]) { | ||
return [value, "text"]; | ||
} | ||
else if (value instanceof Date) { | ||
return [value.getTime(), "timestamp"]; | ||
} | ||
else if (value instanceof RawString) { | ||
return [value.val, "str"]; | ||
} | ||
else if (value instanceof Text) { | ||
return [value, "text"]; | ||
} | ||
else if (value instanceof Uint8Array) { | ||
@@ -89,3 +107,3 @@ return [value, "bytes"]; | ||
else if (value[OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
@@ -95,6 +113,5 @@ else { | ||
} | ||
break; | ||
case 'boolean': | ||
case "boolean": | ||
return [value, "boolean"]; | ||
case 'number': | ||
case "number": | ||
if (Number.isInteger(value)) { | ||
@@ -106,6 +123,9 @@ return [value, "int"]; | ||
} | ||
break; | ||
case 'string': | ||
return [value]; | ||
break; | ||
case "string": | ||
if (textV2) { | ||
return [value, "text"]; | ||
} | ||
else { | ||
return [value, "str"]; | ||
} | ||
default: | ||
@@ -117,3 +137,3 @@ throw new RangeError(`Unsupported type of value: ${typeof value}`); | ||
get(target, key) { | ||
const { context, objectId, readonly, frozen, heads, cache } = target; | ||
const { context, objectId, cache } = target; | ||
if (key === Symbol.toStringTag) { | ||
@@ -124,12 +144,8 @@ return target[Symbol.toStringTag]; | ||
return objectId; | ||
if (key === READ_ONLY) | ||
return readonly; | ||
if (key === FROZEN) | ||
return frozen; | ||
if (key === HEADS) | ||
return heads; | ||
if (key === IS_PROXY) | ||
return true; | ||
if (key === TRACE) | ||
return target.trace; | ||
if (key === STATE) | ||
return context; | ||
return { handle: context }; | ||
if (!cache[key]) { | ||
@@ -141,15 +157,7 @@ cache[key] = valueAt(target, key); | ||
set(target, key, val) { | ||
const { context, objectId, path, readonly, frozen } = target; | ||
const { context, objectId, path, readonly, frozen, textV2 } = target; | ||
target.cache = {}; // reset cache on set | ||
if (val && val[OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
if (key === FROZEN) { | ||
target.frozen = val; | ||
return true; | ||
} | ||
if (key === HEADS) { | ||
target.heads = val; | ||
return true; | ||
} | ||
if (key === TRACE) { | ||
@@ -159,3 +167,3 @@ target.trace = val; | ||
} | ||
const [value, datatype] = import_value(val); | ||
const [value, datatype] = import_value(val, textV2); | ||
if (frozen) { | ||
@@ -170,3 +178,3 @@ throw new RangeError("Attempting to use an outdated Automerge document"); | ||
const list = context.putObject(objectId, key, []); | ||
const proxyList = listProxy(context, list, [...path, key], readonly); | ||
const proxyList = listProxy(context, list, textV2, [...path, key], readonly); | ||
for (let i = 0; i < value.length; i++) { | ||
@@ -178,7 +186,12 @@ proxyList[i] = value[i]; | ||
case "text": { | ||
const text = context.putObject(objectId, key, "", "text"); | ||
const proxyText = textProxy(context, text, [...path, key], readonly); | ||
for (let i = 0; i < value.length; i++) { | ||
proxyText[i] = value.get(i); | ||
if (textV2) { | ||
context.putObject(objectId, key, value); | ||
} | ||
else { | ||
const text = context.putObject(objectId, key, ""); | ||
const proxyText = textProxy(context, text, [...path, key], readonly); | ||
for (let i = 0; i < value.length; i++) { | ||
proxyText[i] = value.get(i); | ||
} | ||
} | ||
break; | ||
@@ -188,3 +201,3 @@ } | ||
const map = context.putObject(objectId, key, {}); | ||
const proxyMap = mapProxy(context, map, [...path, key], readonly); | ||
const proxyMap = mapProxy(context, map, textV2, [...path, key], readonly); | ||
for (const key in value) { | ||
@@ -216,5 +229,7 @@ proxyMap[key] = value[key]; | ||
const value = this.get(target, key); | ||
if (typeof value !== 'undefined') { | ||
if (typeof value !== "undefined") { | ||
return { | ||
configurable: true, enumerable: true, value | ||
configurable: true, | ||
enumerable: true, | ||
value, | ||
}; | ||
@@ -232,6 +247,8 @@ } | ||
get(target, index) { | ||
const { context, objectId, readonly, frozen, heads } = target; | ||
const { context, objectId, heads } = target; | ||
index = parseListIndex(index); | ||
if (index === Symbol.hasInstance) { | ||
return (instance) => { return Array.isArray(instance); }; | ||
return instance => { | ||
return Array.isArray(instance); | ||
}; | ||
} | ||
@@ -243,15 +260,11 @@ if (index === Symbol.toStringTag) { | ||
return objectId; | ||
if (index === READ_ONLY) | ||
return readonly; | ||
if (index === FROZEN) | ||
return frozen; | ||
if (index === HEADS) | ||
return heads; | ||
if (index === IS_PROXY) | ||
return true; | ||
if (index === TRACE) | ||
return target.trace; | ||
if (index === STATE) | ||
return context; | ||
if (index === 'length') | ||
return { handle: context }; | ||
if (index === "length") | ||
return context.length(objectId, heads); | ||
if (typeof index === 'number') { | ||
if (typeof index === "number") { | ||
return valueAt(target, index); | ||
@@ -264,15 +277,7 @@ } | ||
set(target, index, val) { | ||
const { context, objectId, path, readonly, frozen } = target; | ||
const { context, objectId, path, readonly, frozen, textV2 } = target; | ||
index = parseListIndex(index); | ||
if (val && val[OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
if (index === FROZEN) { | ||
target.frozen = val; | ||
return true; | ||
} | ||
if (index === HEADS) { | ||
target.heads = val; | ||
return true; | ||
} | ||
if (index === TRACE) { | ||
@@ -283,5 +288,5 @@ target.trace = val; | ||
if (typeof index == "string") { | ||
throw new RangeError('list index must be a number'); | ||
throw new RangeError("list index must be a number"); | ||
} | ||
const [value, datatype] = import_value(val); | ||
const [value, datatype] = import_value(val, textV2); | ||
if (frozen) { | ||
@@ -302,3 +307,3 @@ throw new RangeError("Attempting to use an outdated Automerge document"); | ||
} | ||
const proxyList = listProxy(context, list, [...path, index], readonly); | ||
const proxyList = listProxy(context, list, textV2, [...path, index], readonly); | ||
proxyList.splice(0, 0, ...value); | ||
@@ -308,11 +313,21 @@ break; | ||
case "text": { | ||
let text; | ||
if (index >= context.length(objectId)) { | ||
text = context.insertObject(objectId, index, "", "text"); | ||
if (textV2) { | ||
if (index >= context.length(objectId)) { | ||
context.insertObject(objectId, index, value); | ||
} | ||
else { | ||
context.putObject(objectId, index, value); | ||
} | ||
} | ||
else { | ||
text = context.putObject(objectId, index, "", "text"); | ||
let text; | ||
if (index >= context.length(objectId)) { | ||
text = context.insertObject(objectId, index, ""); | ||
} | ||
else { | ||
text = context.putObject(objectId, index, ""); | ||
} | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
} | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
break; | ||
@@ -328,3 +343,3 @@ } | ||
} | ||
const proxyMap = mapProxy(context, map, [...path, index], readonly); | ||
const proxyMap = mapProxy(context, map, textV2, [...path, index], readonly); | ||
for (const key in value) { | ||
@@ -348,4 +363,5 @@ proxyMap[key] = value[key]; | ||
index = parseListIndex(index); | ||
if (context.get(objectId, index)[0] == "counter") { | ||
throw new TypeError('Unsupported operation: deleting a counter from a list'); | ||
const elem = context.get(objectId, index); | ||
if (elem != null && elem[0] == "counter") { | ||
throw new TypeError("Unsupported operation: deleting a counter from a list"); | ||
} | ||
@@ -358,10 +374,10 @@ context.delete(objectId, index); | ||
index = parseListIndex(index); | ||
if (typeof index === 'number') { | ||
if (typeof index === "number") { | ||
return index < context.length(objectId, heads); | ||
} | ||
return index === 'length'; | ||
return index === "length"; | ||
}, | ||
getOwnPropertyDescriptor(target, index) { | ||
const { context, objectId, heads } = target; | ||
if (index === 'length') | ||
if (index === "length") | ||
return { writable: true, value: context.length(objectId, heads) }; | ||
@@ -374,3 +390,5 @@ if (index === OBJECT_ID) | ||
}, | ||
getPrototypeOf(target) { return Object.getPrototypeOf(target); }, | ||
getPrototypeOf(target) { | ||
return Object.getPrototypeOf(target); | ||
}, | ||
ownKeys( /*target*/) { | ||
@@ -384,30 +402,27 @@ const keys = []; | ||
return keys; | ||
} | ||
}, | ||
}; | ||
const TextHandler = Object.assign({}, ListHandler, { | ||
get(target, index) { | ||
// FIXME this is a one line change from ListHandler.get() | ||
const { context, objectId, readonly, frozen, heads } = target; | ||
const { context, objectId, heads } = target; | ||
index = parseListIndex(index); | ||
if (index === Symbol.hasInstance) { | ||
return (instance) => { | ||
return Array.isArray(instance); | ||
}; | ||
} | ||
if (index === Symbol.toStringTag) { | ||
return target[Symbol.toStringTag]; | ||
} | ||
if (index === Symbol.hasInstance) { | ||
return (instance) => { return Array.isArray(instance); }; | ||
} | ||
if (index === OBJECT_ID) | ||
return objectId; | ||
if (index === READ_ONLY) | ||
return readonly; | ||
if (index === FROZEN) | ||
return frozen; | ||
if (index === HEADS) | ||
return heads; | ||
if (index === IS_PROXY) | ||
return true; | ||
if (index === TRACE) | ||
return target.trace; | ||
if (index === STATE) | ||
return context; | ||
if (index === 'length') | ||
return { handle: context }; | ||
if (index === "length") | ||
return context.length(objectId, heads); | ||
if (typeof index === 'number') { | ||
if (typeof index === "number") { | ||
return valueAt(target, index); | ||
@@ -423,24 +438,57 @@ } | ||
}); | ||
export function mapProxy(context, objectId, path, readonly, heads) { | ||
return new Proxy({ context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {} }, MapHandler); | ||
export function mapProxy(context, objectId, textV2, path, readonly, heads) { | ||
const target = { | ||
context, | ||
objectId, | ||
path: path || [], | ||
readonly: !!readonly, | ||
frozen: false, | ||
heads, | ||
cache: {}, | ||
textV2, | ||
}; | ||
const proxied = {}; | ||
Object.assign(proxied, target); | ||
let result = new Proxy(proxied, MapHandler); | ||
// conversion through unknown is necessary because the types are so different | ||
return result; | ||
} | ||
export function listProxy(context, objectId, path, readonly, heads) { | ||
const target = []; | ||
Object.assign(target, { context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {} }); | ||
return new Proxy(target, ListHandler); | ||
export function listProxy(context, objectId, textV2, path, readonly, heads) { | ||
const target = { | ||
context, | ||
objectId, | ||
path: path || [], | ||
readonly: !!readonly, | ||
frozen: false, | ||
heads, | ||
cache: {}, | ||
textV2, | ||
}; | ||
const proxied = []; | ||
Object.assign(proxied, target); | ||
// @ts-ignore | ||
return new Proxy(proxied, ListHandler); | ||
} | ||
export function textProxy(context, objectId, path, readonly, heads) { | ||
const target = []; | ||
Object.assign(target, { context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {} }); | ||
const target = { | ||
context, | ||
objectId, | ||
path: path || [], | ||
readonly: !!readonly, | ||
frozen: false, | ||
heads, | ||
cache: {}, | ||
textV2: false, | ||
}; | ||
return new Proxy(target, TextHandler); | ||
} | ||
export function rootProxy(context, readonly) { | ||
export function rootProxy(context, textV2, readonly) { | ||
/* eslint-disable-next-line */ | ||
return mapProxy(context, "_root", [], !!readonly); | ||
return mapProxy(context, "_root", textV2, [], !!readonly); | ||
} | ||
function listMethods(target) { | ||
const { context, objectId, path, readonly, frozen, heads } = target; | ||
const { context, objectId, path, readonly, frozen, heads, textV2 } = target; | ||
const methods = { | ||
deleteAt(index, numDelete) { | ||
if (typeof numDelete === 'number') { | ||
if (typeof numDelete === "number") { | ||
context.splice(objectId, index, numDelete); | ||
@@ -454,3 +502,3 @@ } | ||
fill(val, start, end) { | ||
const [value, datatype] = import_value(val); | ||
const [value, datatype] = import_value(val, textV2); | ||
const length = context.length(objectId); | ||
@@ -460,3 +508,8 @@ start = parseListIndex(start || 0); | ||
for (let i = start; i < Math.min(end, length); i++) { | ||
context.put(objectId, i, value, datatype); | ||
if (datatype === "text" || datatype === "list" || datatype === "map") { | ||
context.putObject(objectId, i, value); | ||
} | ||
else { | ||
context.put(objectId, i, value, datatype); | ||
} | ||
} | ||
@@ -469,3 +522,3 @@ return this; | ||
const value = context.getWithType(objectId, i, heads); | ||
if (value && value[1] === o[OBJECT_ID] || value[1] === o) { | ||
if (value && (value[1] === o[OBJECT_ID] || value[1] === o)) { | ||
return i; | ||
@@ -506,3 +559,3 @@ } | ||
if (val && val[OBJECT_ID]) { | ||
throw new RangeError('Cannot create a reference to an existing document object'); | ||
throw new RangeError("Cannot create a reference to an existing document object"); | ||
} | ||
@@ -524,3 +577,3 @@ } | ||
} | ||
const values = vals.map((val) => import_value(val)); | ||
const values = vals.map(val => import_value(val, textV2)); | ||
for (const [value, datatype] of values) { | ||
@@ -530,3 +583,3 @@ switch (datatype) { | ||
const list = context.insertObject(objectId, index, []); | ||
const proxyList = listProxy(context, list, [...path, index], readonly); | ||
const proxyList = listProxy(context, list, textV2, [...path, index], readonly); | ||
proxyList.splice(0, 0, ...value); | ||
@@ -536,5 +589,10 @@ break; | ||
case "text": { | ||
const text = context.insertObject(objectId, index, "", "text"); | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
if (textV2) { | ||
context.insertObject(objectId, index, value); | ||
} | ||
else { | ||
const text = context.insertObject(objectId, index, ""); | ||
const proxyText = textProxy(context, text, [...path, index], readonly); | ||
proxyText.splice(0, 0, ...value); | ||
} | ||
break; | ||
@@ -544,3 +602,3 @@ } | ||
const map = context.insertObject(objectId, index, {}); | ||
const proxyMap = mapProxy(context, map, [...path, index], readonly); | ||
const proxyMap = mapProxy(context, map, textV2, [...path, index], readonly); | ||
for (const key in value) { | ||
@@ -573,3 +631,3 @@ proxyMap[key] = value[key]; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -589,3 +647,3 @@ return iterator; | ||
return { value, done: true }; | ||
} | ||
}, | ||
}; | ||
@@ -605,3 +663,3 @@ return iterator; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -645,3 +703,3 @@ return iterator; | ||
let index = 0; | ||
for (let v of this) { | ||
for (const v of this) { | ||
if (f(v, index)) { | ||
@@ -655,3 +713,3 @@ return v; | ||
let index = 0; | ||
for (let v of this) { | ||
for (const v of this) { | ||
if (f(v, index)) { | ||
@@ -665,3 +723,3 @@ return index; | ||
includes(elem) { | ||
return this.find((e) => e === elem) !== undefined; | ||
return this.find(e => e === elem) !== undefined; | ||
}, | ||
@@ -688,3 +746,3 @@ join(sep) { | ||
let index = 0; | ||
for (let v of this) { | ||
for (const v of this) { | ||
if (f(v, index)) { | ||
@@ -705,3 +763,3 @@ return true; | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -714,3 +772,3 @@ return methods; | ||
set(index, value) { | ||
return this[index] = value; | ||
return (this[index] = value); | ||
}, | ||
@@ -721,11 +779,11 @@ get(index) { | ||
toString() { | ||
return context.text(objectId, heads).replace(//g, ''); | ||
return context.text(objectId, heads).replace(//g, ""); | ||
}, | ||
toSpans() { | ||
const spans = []; | ||
let chars = ''; | ||
let chars = ""; | ||
const length = context.length(objectId); | ||
for (let i = 0; i < length; i++) { | ||
const value = this[i]; | ||
if (typeof value === 'string') { | ||
if (typeof value === "string") { | ||
chars += value; | ||
@@ -736,3 +794,3 @@ } | ||
spans.push(chars); | ||
chars = ''; | ||
chars = ""; | ||
} | ||
@@ -753,5 +811,5 @@ spans.push(value); | ||
return text.indexOf(o, start); | ||
} | ||
}, | ||
}; | ||
return methods; | ||
} |
import { TEXT, STATE } from "./constants"; | ||
export class Text { | ||
constructor(text) { | ||
if (typeof text === 'string') { | ||
if (typeof text === "string") { | ||
this.elems = [...text]; | ||
@@ -40,3 +40,3 @@ } | ||
} | ||
} | ||
}, | ||
}; | ||
@@ -53,8 +53,8 @@ } | ||
// https://jsperf.com/join-vs-loop-w-type-test | ||
this.str = ''; | ||
this.str = ""; | ||
for (const elem of this.elems) { | ||
if (typeof elem === 'string') | ||
if (typeof elem === "string") | ||
this.str += elem; | ||
else | ||
this.str += '\uFFFC'; | ||
this.str += "\uFFFC"; | ||
} | ||
@@ -68,4 +68,4 @@ } | ||
* | ||
* For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans: | ||
* => ['ab', {x: 3}, 'cd'] | ||
* For example, the value `['a', 'b', {x: 3}, 'c', 'd']` has spans: | ||
* `=> ['ab', {x: 3}, 'cd']` | ||
*/ | ||
@@ -75,5 +75,5 @@ toSpans() { | ||
this.spans = []; | ||
let chars = ''; | ||
let chars = ""; | ||
for (const elem of this.elems) { | ||
if (typeof elem === 'string') { | ||
if (typeof elem === "string") { | ||
chars += elem; | ||
@@ -84,3 +84,3 @@ } | ||
this.spans.push(chars); | ||
chars = ''; | ||
chars = ""; | ||
} | ||
@@ -87,0 +87,0 @@ this.spans.push(elem); |
@@ -1,4 +0,4 @@ | ||
import { v4 } from 'uuid'; | ||
import { v4 } from "uuid"; | ||
function defaultFactory() { | ||
return v4().replace(/-/g, ''); | ||
return v4().replace(/-/g, ""); | ||
} | ||
@@ -9,3 +9,7 @@ let factory = defaultFactory; | ||
}; | ||
uuid.setFactory = newFactory => { factory = newFactory; }; | ||
uuid.reset = () => { factory = defaultFactory; }; | ||
uuid.setFactory = newFactory => { | ||
factory = newFactory; | ||
}; | ||
uuid.reset = () => { | ||
factory = defaultFactory; | ||
}; |
import { Automerge, Heads, ObjID } from "@automerge/automerge-wasm"; | ||
import { Prop } from "@automerge/automerge-wasm"; | ||
import { MapValue, ListValue, TextValue } from "./types"; | ||
export declare function mapProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads): MapValue; | ||
export declare function listProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads): ListValue; | ||
export declare function mapProxy(context: Automerge, objectId: ObjID, textV2: boolean, path?: Prop[], readonly?: boolean, heads?: Heads): MapValue; | ||
export declare function listProxy(context: Automerge, objectId: ObjID, textV2: boolean, path?: Prop[], readonly?: boolean, heads?: Heads): ListValue; | ||
export declare function textProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads): TextValue; | ||
export declare function rootProxy<T>(context: Automerge, readonly?: boolean): T; | ||
export declare function rootProxy<T>(context: Automerge, textV2: boolean, readonly?: boolean): T; |
import { Value } from "@automerge/automerge-wasm"; | ||
export declare class Text { | ||
elems: Value[]; | ||
elems: Array<any>; | ||
str: string | undefined; | ||
spans: Value[] | undefined; | ||
spans: Array<any> | undefined; | ||
constructor(text?: string | string[] | Value[]); | ||
get length(): number; | ||
get(index: number): Value | undefined; | ||
get(index: number): any; | ||
/** | ||
@@ -16,3 +16,3 @@ * Iterates over the text elements character by character, including any | ||
done: boolean; | ||
value: Value; | ||
value: any; | ||
} | { | ||
@@ -32,6 +32,6 @@ done: boolean; | ||
* | ||
* For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans: | ||
* => ['ab', {x: 3}, 'cd'] | ||
* For example, the value `['a', 'b', {x: 3}, 'c', 'd']` has spans: | ||
* `=> ['ab', {x: 3}, 'cd']` | ||
*/ | ||
toSpans(): Value[]; | ||
toSpans(): Array<Value | Object>; | ||
/** | ||
@@ -49,3 +49,3 @@ * Returns the content of the Text object as a simple string, so that the | ||
*/ | ||
insertAt(index: number, ...values: Value[]): void; | ||
insertAt(index: number, ...values: Array<Value | Object>): void; | ||
/** | ||
@@ -56,10 +56,10 @@ * Deletes `numDelete` list items starting at position `index`. | ||
deleteAt(index: number, numDelete?: number): void; | ||
map<T>(callback: (e: Value) => T): void; | ||
map<T>(callback: (e: Value | Object) => T): void; | ||
lastIndexOf(searchElement: Value, fromIndex?: number): void; | ||
concat(other: Text): Text; | ||
every(test: (Value: any) => boolean): boolean; | ||
filter(test: (Value: any) => boolean): Text; | ||
find(test: (Value: any) => boolean): Value | undefined; | ||
findIndex(test: (Value: any) => boolean): number | undefined; | ||
forEach(f: (Value: any) => undefined): void; | ||
every(test: (v: Value) => boolean): boolean; | ||
filter(test: (v: Value) => boolean): Text; | ||
find(test: (v: Value) => boolean): Value | undefined; | ||
findIndex(test: (v: Value) => boolean): number | undefined; | ||
forEach(f: (v: Value) => undefined): void; | ||
includes(elem: Value): boolean; | ||
@@ -66,0 +66,0 @@ indexOf(elem: Value): number; |
@@ -1,2 +0,1 @@ | ||
import { Text } from "./text"; | ||
export { Text } from "./text"; | ||
@@ -6,5 +5,7 @@ export { Counter } from "./counter"; | ||
import { Counter } from "./counter"; | ||
import type { Patch } from "@automerge/automerge-wasm"; | ||
export type { Patch } from "@automerge/automerge-wasm"; | ||
export type AutomergeValue = ScalarValue | { | ||
[key: string]: AutomergeValue; | ||
} | Array<AutomergeValue> | Text; | ||
} | Array<AutomergeValue>; | ||
export type MapValue = { | ||
@@ -16,1 +17,19 @@ [key: string]: AutomergeValue; | ||
export type ScalarValue = string | number | null | boolean | Date | Counter | Uint8Array; | ||
/** | ||
* An automerge document. | ||
* @typeParam T - The type of the value contained in this document | ||
* | ||
* Note that this provides read only access to the fields of the value. To | ||
* modify the value use {@link change} | ||
*/ | ||
export type Doc<T> = { | ||
readonly [P in keyof T]: T[P]; | ||
}; | ||
/** | ||
* Callback which is called by various methods in this library to notify the | ||
* user of what changes have been made. | ||
* @param patch - A description of the changes made | ||
* @param before - The document before the change was made | ||
* @param after - The document after the change was made | ||
*/ | ||
export type PatchCallback<T> = (patches: Array<Patch>, before: Doc<T>, after: Doc<T>) => void; |
@@ -7,3 +7,3 @@ { | ||
], | ||
"version": "2.0.1-alpha.2", | ||
"version": "2.0.1-alpha.3", | ||
"description": "Javascript implementation of automerge, backed by @automerge/automerge-wasm", | ||
@@ -16,22 +16,6 @@ "homepage": "https://github.com/automerge/automerge-rs/tree/main/wrappers/javascript", | ||
"package.json", | ||
"index.d.ts", | ||
"dist/*.d.ts", | ||
"dist/cjs/constants.js", | ||
"dist/cjs/types.js", | ||
"dist/cjs/numbers.js", | ||
"dist/cjs/index.js", | ||
"dist/cjs/uuid.js", | ||
"dist/cjs/counter.js", | ||
"dist/cjs/low_level.js", | ||
"dist/cjs/text.js", | ||
"dist/cjs/proxies.js", | ||
"dist/mjs/constants.js", | ||
"dist/mjs/types.js", | ||
"dist/mjs/numbers.js", | ||
"dist/mjs/index.js", | ||
"dist/mjs/uuid.js", | ||
"dist/mjs/counter.js", | ||
"dist/mjs/low_level.js", | ||
"dist/mjs/text.js", | ||
"dist/mjs/proxies.js" | ||
"dist/index.d.ts", | ||
"dist/cjs/**/*.js", | ||
"dist/mjs/**/*.js", | ||
"dist/*.d.ts" | ||
], | ||
@@ -44,25 +28,26 @@ "types": "./dist/index.d.ts", | ||
"lint": "eslint src", | ||
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc --emitDeclarationOnly", | ||
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc -p config/declonly.json --emitDeclarationOnly", | ||
"test": "ts-mocha test/*.ts", | ||
"watch-docs": "typedoc src/index.ts --watch --readme typedoc-readme.md" | ||
"watch-docs": "typedoc src/index.ts --watch --readme none" | ||
}, | ||
"devDependencies": { | ||
"@types/expect": "^24.3.0", | ||
"@types/mocha": "^9.1.1", | ||
"@types/uuid": "^8.3.4", | ||
"@typescript-eslint/eslint-plugin": "^5.25.0", | ||
"@typescript-eslint/parser": "^5.25.0", | ||
"eslint": "^8.15.0", | ||
"@types/mocha": "^10.0.1", | ||
"@types/uuid": "^9.0.0", | ||
"@typescript-eslint/eslint-plugin": "^5.46.0", | ||
"@typescript-eslint/parser": "^5.46.0", | ||
"eslint": "^8.29.0", | ||
"fast-sha256": "^1.3.0", | ||
"mocha": "^10.0.0", | ||
"pako": "^2.0.4", | ||
"mocha": "^10.2.0", | ||
"pako": "^2.1.0", | ||
"prettier": "^2.8.1", | ||
"ts-mocha": "^10.0.0", | ||
"ts-node": "^10.9.1", | ||
"typedoc": "^0.23.16", | ||
"typescript": "^4.6.4" | ||
"typedoc": "^0.23.22", | ||
"typescript": "^4.9.4" | ||
}, | ||
"dependencies": { | ||
"@automerge/automerge-wasm": "0.1.19", | ||
"uuid": "^8.3" | ||
"@automerge/automerge-wasm": "0.1.21", | ||
"uuid": "^9.0.0" | ||
} | ||
} |
@@ -22,3 +22,2 @@ ## Automerge | ||
`@automerge/automerge` is a wrapper around a core library which is written in | ||
@@ -58,9 +57,9 @@ rust, compiled to WebAssembly and distributed as a separate package called | ||
let doc1 = automerge.from({ | ||
tasks: [ | ||
{description: "feed fish", done: false}, | ||
{description: "water plants", done: false}, | ||
] | ||
tasks: [ | ||
{ description: "feed fish", done: false }, | ||
{ description: "water plants", done: false }, | ||
], | ||
}) | ||
// Create a new thread of execution | ||
// Create a new thread of execution | ||
let doc2 = automerge.clone(doc1) | ||
@@ -72,3 +71,3 @@ | ||
doc2 = automerge.change(doc2, d => { | ||
d.tasks[0].done = true | ||
d.tasks[0].done = true | ||
}) | ||
@@ -78,6 +77,6 @@ | ||
doc1 = automerge.change(doc1, d => { | ||
d.tasks.push({ | ||
description: "water fish", | ||
done: false | ||
}) | ||
d.tasks.push({ | ||
description: "water fish", | ||
done: false, | ||
}) | ||
}) | ||
@@ -91,15 +90,15 @@ | ||
assert.deepEqual(doc1, { | ||
tasks: [ | ||
{description: "feed fish", done: true}, | ||
{description: "water plants", done: false}, | ||
{description: "water fish", done: false}, | ||
] | ||
tasks: [ | ||
{ description: "feed fish", done: true }, | ||
{ description: "water plants", done: false }, | ||
{ description: "water fish", done: false }, | ||
], | ||
}) | ||
assert.deepEqual(doc2, { | ||
tasks: [ | ||
{description: "feed fish", done: true}, | ||
{description: "water plants", done: false}, | ||
{description: "water fish", done: false}, | ||
] | ||
tasks: [ | ||
{ description: "feed fish", done: true }, | ||
{ description: "water plants", done: false }, | ||
{ description: "water fish", done: false }, | ||
], | ||
}) | ||
@@ -106,0 +105,0 @@ ``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
214383
42
5806
14
110
1
+ Added@automerge/automerge-wasm@0.1.21(transitive)
+ Addeduuid@9.0.1(transitive)
- Removed@automerge/automerge-wasm@0.1.19(transitive)
- Removeduuid@8.3.2(transitive)
Updateduuid@^9.0.0