automerge/javascript/src/index.ts

861 lines
29 KiB
TypeScript

/** @hidden **/
export {/** @hidden */ uuid} from './uuid'
import {rootProxy, listProxy, mapProxy} from "./proxies"
import {STATE, TRACE, IS_PROXY, OBJECT_ID } from "./constants"
import {AutomergeValue, Counter} from "./types"
export {AutomergeValue, 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 {ApiHandler, UseApi} from "./low_level"
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
*
* 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 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 function use(api: API) {
UseApi(api)
}
import * as wasm from "@automerge/automerge-wasm"
use(wasm)
/**
* Options to be passed to {@link init} or {@link load}
* @typeParam T - The type of the value the document contains
*/
export type InitOptions<T> = {
/** The actor ID to use for this document, a random one will be generated if `null` is passed */
actor?: ActorId,
freeze?: boolean,
/** A callback which will be called with the initial patch once the document has finished loading */
patchCallback?: PatchCallback<T>,
};
interface InternalState<T> {
handle: Automerge,
heads: Heads | undefined,
freeze: boolean,
patchCallback?: PatchCallback<T>
}
/** @hidden */
export function getBackend<T>(doc: Doc<T>): Automerge {
return _state(doc).handle
}
function _state<T>(doc: Doc<T>, checkroot = true): InternalState<T> {
if (typeof doc !== 'object') {
throw new RangeError("must be the document root")
}
const state = Reflect.get(doc, STATE) as InternalState<T>
if (state === undefined || state == null || (checkroot && _obj(doc) !== "_root")) {
throw new RangeError("must be the document root")
}
return state
}
function _trace<T>(doc: Doc<T>): string | undefined {
return Reflect.get(doc, TRACE) as string
}
function _obj<T>(doc: Doc<T>): ObjID | null {
if (!(typeof doc === 'object') || doc === null) {
return null
}
return Reflect.get(doc, OBJECT_ID) as ObjID
}
function _is_proxy<T>(doc: Doc<T>): boolean {
return !!Reflect.get(doc, IS_PROXY)
}
function importOpts<T>(_actor?: ActorId | InitOptions<T>): InitOptions<T> {
if (typeof _actor === 'object') {
return _actor
} else {
return {actor: _actor}
}
}
/**
* Create a new automerge document
*
* @typeParam T - The type of value contained in the document. This will be the
* type that is passed to the change closure in {@link change}
* @param _opts - Either an actorId or an {@link InitOptions} (which may
* contain an actorId). If this is null the document will be initialised with a
* random actor ID
*/
export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
const opts = importOpts(_opts)
const freeze = !!opts.freeze
const patchCallback = opts.patchCallback
const handle = ApiHandler.create(opts.actor)
handle.enablePatches(true)
handle.enableFreeze(!!opts.freeze)
handle.registerDatatype("counter", (n) => new Counter(n))
const doc = handle.materialize("/", undefined, {handle, heads: undefined, freeze, patchCallback}) as Doc<T>
return doc
}
/**
* Make an immutable view of an automerge document as at `heads`
*
* @remarks
* The document returned from this function cannot be passed to {@link change}.
* This is because it shares the same underlying memory as `doc`, but it is
* consequently a very cheap copy.
*
* Note that this function will throw an error if any of the hashes in `heads`
* are not in the document.
*
* @typeParam T - The type of the value contained in the document
* @param doc - The document to create a view of
* @param heads - The hashes of the heads to create a view at
*/
export function view<T>(doc: Doc<T>, heads: Heads): Doc<T> {
const state = _state(doc)
const handle = state.handle
return state.handle.materialize("/", heads, { ...state, handle, heads }) as Doc<T>
}
/**
* Make a full writable copy of an automerge document
*
* @remarks
* Unlike {@link view} this function makes a full copy of the memory backing
* the document and can thus be passed to {@link change}. It also generates a
* new actor ID so that changes made in the new document do not create duplicate
* sequence numbers with respect to the old document. If you need control over
* the actor ID which is generated you can pass the actor ID as the second
* argument
*
* @typeParam T - The type of the value contained in the document
* @param doc - The document to clone
* @param _opts - Either an actor ID to use for the new doc or an {@link InitOptions}
*/
export function clone<T>(doc: Doc<T>, _opts?: ActorId | InitOptions<T>): Doc<T> {
const state = _state(doc)
const heads = state.heads
const opts = importOpts(_opts)
const handle = state.handle.fork(opts.actor, heads)
// `change` uses the presence of state.heads to determine if we are in a view
// set it to undefined to indicate that this is a full fat document
const {heads: oldHeads, ...stateSansHeads} = state
return handle.applyPatches(doc, { ... stateSansHeads, handle })
}
/** Explicity free the memory backing a document. Note that this is note
* necessary in environments which support
* [`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)
*/
export function free<T>(doc: Doc<T>) {
return _state(doc).handle.free()
}
/**
* Create an automerge document from a POJO
*
* @param initialState - The initial state which will be copied into the document
* @typeParam T - The type of the value passed to `from` _and_ the type the resulting document will contain
* @typeParam actor - The actor ID of the resulting document, if this is null a random actor ID will be used
*
* @example
* ```
* const doc = automerge.from({
* tasks: [
* {description: "feed dogs", done: false}
* ]
* })
* ```
*/
export function from<T extends Record<string, unknown>>(initialState: T | Doc<T>, actor?: ActorId): Doc<T> {
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()
* doc1 = automerge.change(doc1, d => {
* d.key = "value"
* })
* assert.equal(doc1.key, "value")
* ```
*
* @example A change with a message
*
* ```
* doc1 = automerge.change(doc1, "add another value", d => {
* d.key2 = "value2"
* })
* ```
*
* @example A change with a message and a timestamp
*
* ```
* doc1 = automerge.change(doc1, {message: "add another value", timestamp: 1640995200}, d => {
* d.key2 = "value2"
* })
* ```
*
* @example responding to a patch callback
* ```
* let patchedPath
* let patchCallback = patch => {
* patchedPath = patch.path
* }
* doc1 = automerge.change(doc1, {message, "add another value", timestamp: 1640995200, patchCallback}, d => {
* d.key2 = "value2"
* })
* assert.equal(patchedPath, ["key2"])
* ```
*/
export function change<T>(doc: Doc<T>, options: string | ChangeOptions<T> | ChangeFn<T>, callback?: ChangeFn<T>): Doc<T> {
if (typeof options === 'function') {
return _change(doc, {}, 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<T>(doc: Doc<T>, heads: Heads | null, callback?: PatchCallback<T>): Doc<T> {
if (heads == null) {
return doc
}
const state = _state(doc)
const nextState = {...state, heads: undefined};
const nextDoc = state.handle.applyPatches(doc, nextState, callback)
state.heads = heads
return nextDoc
}
function _change<T>(doc: Doc<T>, options: ChangeOptions<T>, callback: ChangeFn<T>): Doc<T> {
if (typeof callback !== "function") {
throw new RangeError("invalid change function");
}
const state = _state(doc)
if (doc === undefined || state === undefined) {
throw new RangeError("must be the document root");
}
if (state.heads) {
throw new RangeError("Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.")
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
try {
state.heads = heads
const root: T = 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
*
* Why would you want to do this? One reason might be that you have merged
* changes from some other peers and you want to generate a change which
* depends on those merged changes so that you can sign the new change with all
* of the merged changes as part of the new change.
*/
export function emptyChange<T>(doc: Doc<T>, options: string | ChangeOptions<T> | void) {
if (options === undefined) {
options = {}
}
if (typeof options === "string") {
options = {message: options}
}
const state = _state(doc)
if (state.heads) {
throw new RangeError("Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.")
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.emptyChange(options.message, options.time)
return progressDocument(doc, heads)
}
/**
* Load an automerge document from a compressed document produce by {@link save}
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
* @param data - The compressed document
* @param _opts - Either an actor ID or some {@link InitOptions}, if the actor
* ID is null a random actor ID will be created
*
* Note that `load` will throw an error if passed incomplete content (for
* example if you are receiving content over the network and don't know if you
* have the complete document yet). If you need to handle incomplete content use
* {@link init} followed by {@link loadIncremental}.
*/
export function load<T>(data: Uint8Array, _opts?: ActorId | InitOptions<T>): Doc<T> {
const opts = importOpts(_opts)
const actor = opts.actor
const patchCallback = opts.patchCallback
const handle = ApiHandler.load(data, actor)
handle.enablePatches(true)
handle.enableFreeze(!!opts.freeze)
handle.registerDatatype("counter", (n) => new Counter(n))
const doc = handle.materialize("/", undefined, {handle, heads: undefined, patchCallback}) as Doc<T>
return doc
}
/**
* Load changes produced by {@link saveIncremental}, or partial changes
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
* @param data - The compressedchanges
* @param opts - an {@link ApplyOptions}
*
* This function is useful when staying up to date with a connected peer.
* Perhaps the other end sent you a full compresed document which you loaded
* with {@link load} and they're sending you the result of
* {@link getLastLocalChange} every time they make a change.
*
* Note that this function will succesfully load the results of {@link save} as
* well as {@link getLastLocalChange} or any other incremental change.
*/
export function loadIncremental<T>(doc: Doc<T>, data: Uint8Array, opts?: ApplyOptions<T>): Doc<T> {
if (!opts) {opts = {}}
const state = _state(doc)
if (state.heads) {
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc));
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.loadIncremental(data)
return progressDocument(doc, heads, opts.patchCallback || state.patchCallback)
}
/**
* Export the contents of a document to a compressed format
*
* @param doc - The doc to save
*
* The returned bytes can be passed to {@link load} or {@link loadIncremental}
*/
export function save<T>(doc: Doc<T>): Uint8Array {
return _state(doc).handle.save()
}
/**
* Merge `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
*
* @returns - The merged document
*
* Often when you are merging documents you will also need to clone them. Both
* arguments to `merge` are frozen after the call so you can no longer call
* mutating methods (such as {@link change}) on them. The symtom of this will be
* an error which says "Attempting to change an out of date document". To
* overcome this call {@link clone} on the argument before passing it to {@link
* merge}.
*/
export function merge<T>(local: Doc<T>, remote: Doc<T>): Doc<T> {
const localState = _state(local)
if (localState.heads) {
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(local));
}
const heads = localState.handle.getHeads()
const remoteState = _state(remote)
const changes = localState.handle.getChangesAdded(remoteState.handle)
localState.handle.applyChanges(changes)
return progressDocument(local, heads, localState.patchCallback)
}
/**
* Get the actor ID associated with the document
*/
export function getActorId<T>(doc: Doc<T>): ActorId {
const state = _state(doc)
return state.handle.getActorId()
}
/**
* The type of conflicts for particular key or index
*
* Maps and sequences in automerge can contain conflicting values for a
* particular key or index. In this case {@link getConflicts} can be used to
* obtain a `Conflicts` representing the multiple values present for the property
*
* A `Conflicts` is a map from a unique (per property or index) key to one of
* the possible conflicting values for the given property.
*/
type Conflicts = {[key: string]: AutomergeValue}
function conflictAt(context: Automerge, objectId: ObjID, prop: Prop): Conflicts | undefined {
const values = context.getAll(objectId, prop)
if (values.length <= 1) {
return
}
const result: Conflicts = {}
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]] = context.text(fullVal[1])
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
*
* The values of properties in a map in automerge can be conflicted if there
* are concurrent "put" operations to the same key. Automerge chooses one value
* arbitrarily (but deterministically, any two nodes who have the same set of
* changes will choose the same value) from the set of conflicting values to
* present as the value of the key.
*
* Sometimes you may want to examine these conflicts, in this case you can use
* {@link getConflicts} to get the conflicts for the key.
*
* @example
* ```
* import * as automerge from "@automerge/automerge"
*
* type Profile = {
* pets: Array<{name: string, type: string}>
* }
*
* let doc1 = automerge.init<Profile>("aaaa")
* doc1 = automerge.change(doc1, d => {
* d.pets = [{name: "Lassie", type: "dog"}]
* })
* let doc2 = automerge.init<Profile>("bbbb")
* doc2 = automerge.merge(doc2, automerge.clone(doc1))
*
* doc2 = automerge.change(doc2, d => {
* d.pets[0].name = "Beethoven"
* })
*
* doc1 = automerge.change(doc1, d => {
* d.pets[0].name = "Babe"
* })
*
* const doc3 = automerge.merge(doc1, doc2)
*
* // Note that here we pass `doc3.pets`, not `doc3`
* let conflicts = automerge.getConflicts(doc3.pets[0], "name")
*
* // The two conflicting values are the keys of the conflicts object
* assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
* ```
*/
export function getConflicts<T>(doc: Doc<T>, prop: Prop): Conflicts | undefined {
const state = _state(doc, false)
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<T>(doc: Doc<T>): Change | undefined {
const state = _state(doc)
return state.handle.getLastLocalChange() || undefined
}
/**
* Return the object ID of an arbitrary javascript value
*
* This is useful to determine if something is actually an automerge document,
* if `doc` is not an automerge document this will return null.
*/
export function getObjectId<T>(doc: Doc<T>, prop?: Prop): ObjID | null {
if (prop) {
const state = _state(doc, false)
const objectId = _obj(doc)
if (!state || !objectId) {
return null
}
return state.handle.get(objectId, prop) as ObjID
} else {
return _obj(doc)
}
}
/**
* Get the changes which are in `newState` but not in `oldState`. The returned
* changes can be loaded in `oldState` via {@link applyChanges}.
*
* Note that this will crash if there are changes in `oldState` which are not in `newState`.
*/
export function getChanges<T>(oldState: Doc<T>, newState: Doc<T>): Change[] {
const n = _state(newState)
return n.handle.getChanges(getHeads(oldState))
}
/**
* Get all the changes in a document
*
* This is different to {@link save} because the output is an array of changes
* which can be individually applied via {@link applyChanges}`
*
*/
export function getAllChanges<T>(doc: Doc<T>): Change[] {
const state = _state(doc)
return state.handle.getChanges([])
}
/**
* Apply changes received from another document
*
* `doc` will be updated to reflect the `changes`. If there are changes which
* we do not have dependencies for yet those will be stored in the document and
* applied when the depended on changes arrive.
*
* You can use the {@link ApplyOptions} to pass a patchcallback which will be
* informed of any changes which occur as a result of applying the changes
*
*/
export function applyChanges<T>(doc: Doc<T>, changes: Change[], opts?: ApplyOptions<T>): [Doc<T>] {
const state = _state(doc)
if (!opts) {opts = {}}
if (state.heads) {
throw new RangeError("Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.")
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads();
state.handle.applyChanges(changes)
state.heads = heads;
return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback)]
}
/** @hidden */
export function getHistory<T>(doc: Doc<T>): State<T>[] {
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 <T>state
}
})
)
}
/** @hidden */
// FIXME : no tests
// FIXME can we just use deep equals now?
export function equals(val1: unknown, val2: unknown): boolean {
if (!isObject(val1) || !isObject(val2)) return val1 === val2
const keys1 = Object.keys(val1).sort(), keys2 = Object.keys(val2).sort()
if (keys1.length !== keys2.length) return false
for (let i = 0; i < keys1.length; i++) {
if (keys1[i] !== keys2[i]) return false
if (!equals(val1[keys1[i]], val2[keys2[i]])) return false
}
return true
}
/**
* encode a {@link SyncState} into binary to send over the network
*
* @group sync
* */
export function encodeSyncState(state: SyncState): Uint8Array {
const sync = ApiHandler.importSyncState(state)
const result = ApiHandler.encodeSyncState(sync)
sync.free()
return result
}
/**
* Decode some binary data into a {@link SyncState}
*
* @group sync
*/
export function decodeSyncState(state: Uint8Array): SyncState {
const sync = ApiHandler.decodeSyncState(state)
const 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
*
* @group sync
*
* @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<T>(doc: Doc<T>, inState: SyncState): [SyncState, SyncMessage | null] {
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
*
* @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.
*/
export function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message: SyncMessage, opts?: ApplyOptions<T>): [Doc<T>, SyncState, null] {
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 (_is_proxy(doc)) {
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(): SyncState {
return ApiHandler.exportSyncState(ApiHandler.initSyncState())
}
/** @hidden */
export function encodeChange(change: DecodedChange): Change {
return ApiHandler.encodeChange(change)
}
/** @hidden */
export function decodeChange(data: Change): DecodedChange {
return ApiHandler.decodeChange(data)
}
/** @hidden */
export function encodeSyncMessage(message: DecodedSyncMessage): SyncMessage {
return ApiHandler.encodeSyncMessage(message)
}
/** @hidden */
export function decodeSyncMessage(message: SyncMessage): DecodedSyncMessage {
return ApiHandler.decodeSyncMessage(message)
}
/**
* Get any changes in `doc` which are not dependencies of `heads`
*/
export function getMissingDeps<T>(doc: Doc<T>, heads: Heads): Heads {
const state = _state(doc)
return state.handle.getMissingDeps(heads)
}
export function splice<T>(doc: Doc<T>, prop: Prop, index: number, del: number, newText?: string) {
if (!_is_proxy(doc)) {
throw new RangeError("object cannot be modified outside of a change block")
}
const state = _state(doc, false)
const objectId = _obj(doc)
if (!objectId) {
throw new RangeError("invalid object for splice")
}
const value = `${objectId}/${prop}`
try {
return state.handle.splice(value, index, del, newText)
} catch (e) {
throw new RangeError(`Cannot splice: ${e}`)
}
}
/**
* Get the hashes of the heads of this document
*/
export function getHeads<T>(doc: Doc<T>): Heads {
const state = _state(doc)
return state.heads || state.handle.getHeads()
}
/** @hidden */
export function dump<T>(doc: Doc<T>) {
const state = _state(doc)
state.handle.dump()
}
/** @hidden */
export function toJS<T>(doc: Doc<T>): T {
const state = _state(doc)
const enabled = state.handle.enableFreeze(false)
const result = state.handle.materialize()
state.handle.enableFreeze(enabled)
return result as T
}
export function isAutomerge(doc: unknown): boolean {
if (typeof doc == "object" && doc !== null) {
return getObjectId(doc) === "_root" && !!Reflect.get(doc, STATE)
} else {
return false
}
}
function isObject(obj: unknown): obj is Record<string, unknown> {
return typeof obj === 'object' && obj !== null
}
export type {API, SyncState, ActorId, Conflicts, Prop, Change, ObjID, DecodedChange, DecodedSyncMessage, Heads, MaterializeValue}