397 lines
12 KiB
TypeScript
397 lines
12 KiB
TypeScript
|
|
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<T> = { readonly [P in keyof T]: Doc<T[P]> }
|
|
|
|
export type ChangeFn<T> = (doc: T) => void
|
|
|
|
export interface State<T> {
|
|
change: DecodedChange
|
|
snapshot: T
|
|
}
|
|
|
|
export function use(api: API) {
|
|
UseApi(api)
|
|
}
|
|
|
|
export function getBackend<T>(doc: Doc<T>) : Automerge {
|
|
return _state(doc)
|
|
}
|
|
|
|
function _state<T>(doc: Doc<T>) : Automerge {
|
|
const state = Reflect.get(doc,STATE)
|
|
if (state == undefined) {
|
|
throw new RangeError("must be the document root")
|
|
}
|
|
return state
|
|
}
|
|
|
|
function _frozen<T>(doc: Doc<T>) : boolean {
|
|
return Reflect.get(doc,FROZEN) === true
|
|
}
|
|
|
|
function _heads<T>(doc: Doc<T>) : Heads | undefined {
|
|
return Reflect.get(doc,HEADS)
|
|
}
|
|
|
|
function _trace<T>(doc: Doc<T>) : string | undefined {
|
|
return Reflect.get(doc,TRACE)
|
|
}
|
|
|
|
function _set_heads<T>(doc: Doc<T>, heads: Heads) {
|
|
Reflect.set(doc,HEADS,heads)
|
|
Reflect.set(doc,TRACE,(new Error()).stack)
|
|
}
|
|
|
|
function _clear_heads<T>(doc: Doc<T>) {
|
|
Reflect.set(doc,HEADS,undefined)
|
|
Reflect.set(doc,TRACE,undefined)
|
|
}
|
|
|
|
function _obj<T>(doc: Doc<T>) : ObjID {
|
|
return Reflect.get(doc,OBJECT_ID)
|
|
}
|
|
|
|
function _readonly<T>(doc: Doc<T>) : boolean {
|
|
return Reflect.get(doc,READ_ONLY) === true
|
|
}
|
|
|
|
export function init<T>(actor?: ActorId) : Doc<T>{
|
|
if (typeof actor !== "string") {
|
|
actor = undefined
|
|
}
|
|
const state = ApiHandler.create(actor)
|
|
return rootProxy(state, true);
|
|
}
|
|
|
|
export function clone<T>(doc: Doc<T>) : Doc<T> {
|
|
const state = _state(doc).clone()
|
|
return rootProxy(state, true);
|
|
}
|
|
|
|
export function free<T>(doc: Doc<T>) {
|
|
return _state(doc).free()
|
|
}
|
|
|
|
export function from<T>(initialState: T | Doc<T>, actor?: ActorId): Doc<T> {
|
|
return change(init(actor), (d) => Object.assign(d, initialState))
|
|
}
|
|
|
|
export function change<T>(doc: Doc<T>, options: string | ChangeOptions | 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 _change<T>(doc: Doc<T>, options: ChangeOptions, callback: ChangeFn<T>): Doc<T> {
|
|
|
|
|
|
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<T>(doc: Doc<T>, 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<T>(data: Uint8Array, actor: ActorId) : Doc<T> {
|
|
const state = ApiHandler.load(data, actor)
|
|
return rootProxy(state, true);
|
|
}
|
|
|
|
export function save<T>(doc: Doc<T>) : Uint8Array {
|
|
const state = _state(doc)
|
|
return state.save()
|
|
}
|
|
|
|
export function merge<T>(local: Doc<T>, remote: Doc<T>) : Doc<T> {
|
|
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<T>(doc: Doc<T>) : 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<T>(doc: Doc<T>, prop: Prop) : Conflicts | undefined {
|
|
const state = _state(doc)
|
|
const objectId = _obj(doc)
|
|
return conflictAt(state, objectId, prop)
|
|
}
|
|
|
|
export function getLastLocalChange<T>(doc: Doc<T>) : Change | undefined {
|
|
const state = _state(doc)
|
|
return state.getLastLocalChange() || undefined
|
|
}
|
|
|
|
export function getObjectId<T>(doc: Doc<T>) : ObjID {
|
|
return _obj(doc)
|
|
}
|
|
|
|
export function getChanges<T>(oldState: Doc<T>, newState: Doc<T>) : Change[] {
|
|
const o = _state(oldState)
|
|
const n = _state(newState)
|
|
const heads = _heads(oldState)
|
|
return n.getChanges(heads || o.getHeads())
|
|
}
|
|
|
|
export function getAllChanges<T>(doc: Doc<T>) : Change[] {
|
|
const state = _state(doc)
|
|
return state.getChanges([])
|
|
}
|
|
|
|
export function applyChanges<T>(doc: Doc<T>, changes: Change[]) : [Doc<T>] {
|
|
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<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
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
// 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<T>(doc: Doc<T>, 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<T>(doc: Doc<T>, inState: SyncState, message: SyncMessage) : [ Doc<T>, 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<T>(doc: Doc<T>, heads: Heads) : Heads {
|
|
const state = _state(doc)
|
|
return state.getMissingDeps(heads)
|
|
}
|
|
|
|
export function getHeads<T>(doc: Doc<T>) : Heads {
|
|
const state = _state(doc)
|
|
return _heads(doc) || state.getHeads()
|
|
}
|
|
|
|
export function dump<T>(doc: Doc<T>) {
|
|
const state = _state(doc)
|
|
state.dump()
|
|
}
|
|
|
|
// FIXME - return T?
|
|
export function toJS<T>(doc: Doc<T>) : MaterializeValue {
|
|
const state = _state(doc)
|
|
const heads = _heads(doc)
|
|
return state.materialize("_root", heads)
|
|
}
|
|
|
|
|
|
function isObject(obj: unknown) : obj is Record<string,unknown> {
|
|
return typeof obj === 'object' && obj !== null
|
|
}
|