export { uuid } from './uuid' import { rootProxy, listProxy, textProxy, mapProxy } from "./proxies" import { STATE, HEADS, TRACE, OBJECT_ID, READ_ONLY, FROZEN } from "./constants" import { AutomergeValue, Counter } from "./types" export { AutomergeValue, Text, Counter, Int, Uint, Float64 } from "./types" import { API } from "automerge-types"; import { ApiHandler, UseApi } from "./low_level" import { Actor as ActorId, Prop, ObjID, Change, DecodedChange, Heads, Automerge, MaterializeValue } from "automerge-types" import { JsSyncState as SyncState, SyncMessage, DecodedSyncMessage } from "automerge-types" export type ChangeOptions = { message?: string, time?: number } export type Doc = { readonly [P in keyof T]: Doc } export type ChangeFn = (doc: T) => void export interface State { change: DecodedChange snapshot: T } export function use(api: API) { UseApi(api) } export function getBackend(doc: Doc) : Automerge { return _state(doc) } function _state(doc: Doc) : Automerge { const state = Reflect.get(doc,STATE) if (state == undefined) { throw new RangeError("must be the document root") } return state } function _frozen(doc: Doc) : boolean { return Reflect.get(doc,FROZEN) === true } function _heads(doc: Doc) : Heads | undefined { return Reflect.get(doc,HEADS) } function _trace(doc: Doc) : string | undefined { return Reflect.get(doc,TRACE) } function _set_heads(doc: Doc, heads: Heads) { Reflect.set(doc,HEADS,heads) Reflect.set(doc,TRACE,(new Error()).stack) } function _clear_heads(doc: Doc) { Reflect.set(doc,HEADS,undefined) Reflect.set(doc,TRACE,undefined) } function _obj(doc: Doc) : ObjID { return Reflect.get(doc,OBJECT_ID) } function _readonly(doc: Doc) : boolean { return Reflect.get(doc,READ_ONLY) === true } export function init(actor?: ActorId) : Doc{ if (typeof actor !== "string") { actor = undefined } const state = ApiHandler.create(actor) return rootProxy(state, true); } export function clone(doc: Doc) : Doc { const state = _state(doc).clone() return rootProxy(state, true); } export function free(doc: Doc) { return _state(doc).free() } export function from(initialState: T | Doc, actor?: ActorId): Doc { return change(init(actor), (d) => Object.assign(d, initialState)) } export function change(doc: Doc, options: string | ChangeOptions | ChangeFn, callback?: ChangeFn): Doc { 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 _change(doc: Doc, options: ChangeOptions, callback: ChangeFn): Doc { if (typeof callback !== "function") { throw new RangeError("invalid change function"); } if (doc === undefined || _state(doc) === undefined || _obj(doc) !== "_root") { throw new RangeError("must be the document root"); } if (_frozen(doc) === true) { throw new RangeError("Attempting to use an outdated Automerge document") } if (!!_heads(doc) === true) { 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 state = _state(doc) const heads = state.getHeads() try { _set_heads(doc,heads) Reflect.set(doc,FROZEN,true) const root : T = rootProxy(state); callback(root) if (state.pendingOps() === 0) { Reflect.set(doc,FROZEN,false) _clear_heads(doc) return doc } else { state.commit(options.message, options.time) return rootProxy(state, true); } } catch (e) { //console.log("ERROR: ",e) Reflect.set(doc,FROZEN,false) _clear_heads(doc) state.rollback() throw e } } export function emptyChange(doc: Doc, options: ChangeOptions) { if (options === undefined) { options = {} } if (typeof options === "string") { options = { message: options } } if (doc === undefined || _state(doc) === undefined || _obj(doc) !== "_root") { throw new RangeError("must be the document root"); } if (_frozen(doc) === true) { throw new RangeError("Attempting to use an outdated Automerge document") } if (_readonly(doc) === false) { throw new RangeError("Calls to Automerge.change cannot be nested") } const state = _state(doc) state.commit(options.message, options.time) return rootProxy(state, true); } export function load(data: Uint8Array, actor: ActorId) : Doc { const state = ApiHandler.load(data, actor) return rootProxy(state, true); } export function save(doc: Doc) : Uint8Array { const state = _state(doc) return state.save() } export function merge(local: Doc, remote: Doc) : Doc { if (!!_heads(local) === true) { throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc)); } const localState = _state(local) const heads = localState.getHeads() const remoteState = _state(remote) const changes = localState.getChangesAdded(remoteState) localState.applyChanges(changes) _set_heads(local,heads) return rootProxy(localState, true) } export function getActorId(doc: Doc) : ActorId { const state = _state(doc) return state.getActorId() } 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]] = 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 } export function getConflicts(doc: Doc, prop: Prop) : Conflicts | undefined { const state = _state(doc) const objectId = _obj(doc) return conflictAt(state, objectId, prop) } export function getLastLocalChange(doc: Doc) : Change | undefined { const state = _state(doc) return state.getLastLocalChange() || undefined } export function getObjectId(doc: Doc) : ObjID { return _obj(doc) } export function getChanges(oldState: Doc, newState: Doc) : Change[] { const o = _state(oldState) const n = _state(newState) const heads = _heads(oldState) return n.getChanges(heads || o.getHeads()) } export function getAllChanges(doc: Doc) : Change[] { const state = _state(doc) return state.getChanges([]) } export function applyChanges(doc: Doc, changes: Change[]) : [Doc] { if (doc === undefined || _obj(doc) !== "_root") { throw new RangeError("must be the document root"); } if (_frozen(doc) === true) { throw new RangeError("Attempting to use an outdated Automerge document") } if (_readonly(doc) === false) { throw new RangeError("Calls to Automerge.change cannot be nested") } const state = _state(doc) const heads = state.getHeads() state.applyChanges(changes) _set_heads(doc,heads) return [rootProxy(state, true)]; } export function getHistory(doc: Doc) : State[] { 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 } }) ) } // FIXME : no tests 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 } export function encodeSyncState(state: SyncState) : Uint8Array { return ApiHandler.encodeSyncState(ApiHandler.importSyncState(state)) } export function decodeSyncState(state: Uint8Array) : SyncState { return ApiHandler.exportSyncState(ApiHandler.decodeSyncState(state)) } export function generateSyncMessage(doc: Doc, inState: SyncState) : [ SyncState, SyncMessage | null ] { const state = _state(doc) const syncState = ApiHandler.importSyncState(inState) const message = state.generateSyncMessage(syncState) const outState = ApiHandler.exportSyncState(syncState) return [ outState, message ] } export function receiveSyncMessage(doc: Doc, inState: SyncState, message: SyncMessage) : [ Doc, SyncState, null ] { const syncState = ApiHandler.importSyncState(inState) if (doc === undefined || _obj(doc) !== "_root") { throw new RangeError("must be the document root"); } if (_frozen(doc) === true) { throw new RangeError("Attempting to use an outdated Automerge document") } if (!!_heads(doc) === true) { 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 state = _state(doc) const heads = state.getHeads() state.receiveSyncMessage(syncState, message) _set_heads(doc,heads) const outState = ApiHandler.exportSyncState(syncState) return [rootProxy(state, true), outState, null]; } export function initSyncState() : SyncState { return ApiHandler.exportSyncState(ApiHandler.initSyncState()) } export function encodeChange(change: DecodedChange) : Change { return ApiHandler.encodeChange(change) } export function decodeChange(data: Change) : DecodedChange { return ApiHandler.decodeChange(data) } export function encodeSyncMessage(message: DecodedSyncMessage) : SyncMessage { return ApiHandler.encodeSyncMessage(message) } export function decodeSyncMessage(message: SyncMessage) : DecodedSyncMessage { return ApiHandler.decodeSyncMessage(message) } export function getMissingDeps(doc: Doc, heads: Heads) : Heads { const state = _state(doc) return state.getMissingDeps(heads) } export function getHeads(doc: Doc) : Heads { const state = _state(doc) return _heads(doc) || state.getHeads() } export function dump(doc: Doc) { const state = _state(doc) state.dump() } // FIXME - return T? export function toJS(doc: Doc) : MaterializeValue { const state = _state(doc) const heads = _heads(doc) return state.materialize("_root", heads) } function isObject(obj: unknown) : obj is Record { return typeof obj === 'object' && obj !== null }