372 lines
9.6 KiB
JavaScript
372 lines
9.6 KiB
JavaScript
const AutomergeWASM = require("automerge-wasm")
|
|
const uuid = require('./uuid')
|
|
|
|
let { rootProxy, listProxy, textProxy, mapProxy } = require("./proxies")
|
|
let { Counter } = require("./counter")
|
|
let { Text } = require("./text")
|
|
let { Int, Uint, Float64 } = require("./numbers")
|
|
let { STATE, HEADS, OBJECT_ID, READ_ONLY, FROZEN } = require("./constants")
|
|
|
|
function init(actor) {
|
|
if (typeof actor != 'string') {
|
|
actor = null
|
|
}
|
|
const state = AutomergeWASM.create(actor)
|
|
return rootProxy(state, true);
|
|
}
|
|
|
|
function clone(doc) {
|
|
const state = doc[STATE].clone()
|
|
return rootProxy(state, true);
|
|
}
|
|
|
|
function free(doc) {
|
|
return doc[STATE].free()
|
|
}
|
|
|
|
function from(data, actor) {
|
|
let doc1 = init(actor)
|
|
let doc2 = change(doc1, (d) => Object.assign(d, data))
|
|
return doc2
|
|
}
|
|
|
|
function change(doc, options, callback) {
|
|
if (callback === undefined) {
|
|
// FIXME implement options
|
|
callback = options
|
|
options = {}
|
|
}
|
|
if (typeof options === "string") {
|
|
options = { message: options }
|
|
}
|
|
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
|
throw new RangeError("must be the document root");
|
|
}
|
|
if (doc[FROZEN] === true) {
|
|
throw new RangeError("Attempting to use an outdated Automerge document")
|
|
}
|
|
if (!!doc[HEADS] === true) {
|
|
throw new RangeError("Attempting to change an out of date document");
|
|
}
|
|
if (doc[READ_ONLY] === false) {
|
|
throw new RangeError("Calls to Automerge.change cannot be nested")
|
|
}
|
|
const state = doc[STATE]
|
|
const heads = state.getHeads()
|
|
try {
|
|
doc[HEADS] = heads
|
|
doc[FROZEN] = true
|
|
let root = rootProxy(state);
|
|
callback(root)
|
|
if (state.pendingOps() === 0) {
|
|
doc[FROZEN] = false
|
|
doc[HEADS] = undefined
|
|
return doc
|
|
} else {
|
|
state.commit(options.message, options.time)
|
|
return rootProxy(state, true);
|
|
}
|
|
} catch (e) {
|
|
//console.log("ERROR: ",e)
|
|
doc[FROZEN] = false
|
|
doc[HEADS] = undefined
|
|
state.rollback()
|
|
throw e
|
|
}
|
|
}
|
|
|
|
function emptyChange(doc, options) {
|
|
if (options === undefined) {
|
|
options = {}
|
|
}
|
|
if (typeof options === "string") {
|
|
options = { message: options }
|
|
}
|
|
|
|
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
|
throw new RangeError("must be the document root");
|
|
}
|
|
if (doc[FROZEN] === true) {
|
|
throw new RangeError("Attempting to use an outdated Automerge document")
|
|
}
|
|
if (doc[READ_ONLY] === false) {
|
|
throw new RangeError("Calls to Automerge.change cannot be nested")
|
|
}
|
|
|
|
const state = doc[STATE]
|
|
state.commit(options.message, options.time)
|
|
return rootProxy(state, true);
|
|
}
|
|
|
|
function load(data, actor) {
|
|
const state = AutomergeWASM.loadDoc(data, actor)
|
|
return rootProxy(state, true);
|
|
}
|
|
|
|
function save(doc) {
|
|
const state = doc[STATE]
|
|
return state.save()
|
|
}
|
|
|
|
function merge(local, remote) {
|
|
if (local[HEADS] === true) {
|
|
throw new RangeError("Attempting to change an out of date document");
|
|
}
|
|
const localState = local[STATE]
|
|
const heads = localState.getHeads()
|
|
const remoteState = remote[STATE]
|
|
const changes = localState.getChangesAdded(remoteState)
|
|
localState.applyChanges(changes)
|
|
local[HEADS] = heads
|
|
return rootProxy(localState, true)
|
|
}
|
|
|
|
function getActorId(doc) {
|
|
const state = doc[STATE]
|
|
return state.getActorId()
|
|
}
|
|
|
|
function conflictAt(context, objectId, prop) {
|
|
let values = context.values(objectId, prop)
|
|
if (values.length <= 1) {
|
|
return
|
|
}
|
|
let result = {}
|
|
for (const conflict of values) {
|
|
const datatype = conflict[0]
|
|
const value = conflict[1]
|
|
switch (datatype) {
|
|
case "map":
|
|
result[value] = mapProxy(context, value, [ prop ], true)
|
|
break;
|
|
case "list":
|
|
result[value] = listProxy(context, value, [ prop ], true)
|
|
break;
|
|
case "text":
|
|
result[value] = textProxy(context, value, [ prop ], true)
|
|
break;
|
|
//case "table":
|
|
//case "cursor":
|
|
case "str":
|
|
case "uint":
|
|
case "int":
|
|
case "f64":
|
|
case "boolean":
|
|
case "bytes":
|
|
case "null":
|
|
result[conflict[2]] = value
|
|
break;
|
|
case "counter":
|
|
result[conflict[2]] = new Counter(value)
|
|
break;
|
|
case "timestamp":
|
|
result[conflict[2]] = new Date(value)
|
|
break;
|
|
default:
|
|
throw RangeError(`datatype ${datatype} unimplemented`)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
function getConflicts(doc, prop) {
|
|
const state = doc[STATE]
|
|
const objectId = doc[OBJECT_ID]
|
|
return conflictAt(state, objectId, prop)
|
|
}
|
|
|
|
function getLastLocalChange(doc) {
|
|
const state = doc[STATE]
|
|
try {
|
|
return state.getLastLocalChange()
|
|
} catch (e) {
|
|
return
|
|
}
|
|
}
|
|
|
|
function getObjectId(doc) {
|
|
return doc[OBJECT_ID]
|
|
}
|
|
|
|
function getChanges(oldState, newState) {
|
|
const o = oldState[STATE]
|
|
const n = newState[STATE]
|
|
const heads = oldState[HEADS]
|
|
return n.getChanges(heads || o.getHeads())
|
|
}
|
|
|
|
function getAllChanges(doc) {
|
|
const state = doc[STATE]
|
|
return state.getChanges([])
|
|
}
|
|
|
|
function applyChanges(doc, changes) {
|
|
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
|
throw new RangeError("must be the document root");
|
|
}
|
|
if (doc[FROZEN] === true) {
|
|
throw new RangeError("Attempting to use an outdated Automerge document")
|
|
}
|
|
if (doc[READ_ONLY] === false) {
|
|
throw new RangeError("Calls to Automerge.change cannot be nested")
|
|
}
|
|
const state = doc[STATE]
|
|
const heads = state.getHeads()
|
|
state.applyChanges(changes)
|
|
doc[HEADS] = heads
|
|
return [rootProxy(state, true)];
|
|
}
|
|
|
|
function getHistory(doc) {
|
|
const actor = getActorId(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
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
function equals() {
|
|
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
|
|
}
|
|
|
|
function encodeSyncMessage(msg) {
|
|
return AutomergeWASM.encodeSyncMessage(msg)
|
|
}
|
|
|
|
function decodeSyncMessage(msg) {
|
|
return AutomergeWASM.decodeSyncMessage(msg)
|
|
}
|
|
|
|
function encodeSyncState(state) {
|
|
return AutomergeWASM.encodeSyncState(AutomergeWASM.importSyncState(state))
|
|
}
|
|
|
|
function decodeSyncState(state) {
|
|
return AutomergeWASM.exportSyncState(AutomergeWASM.decodeSyncState(state))
|
|
}
|
|
|
|
function generateSyncMessage(doc, inState) {
|
|
const state = doc[STATE]
|
|
const syncState = AutomergeWASM.importSyncState(inState)
|
|
const message = state.generateSyncMessage(syncState)
|
|
const outState = AutomergeWASM.exportSyncState(syncState)
|
|
return [ outState, message ]
|
|
}
|
|
|
|
function receiveSyncMessage(doc, inState, message) {
|
|
const syncState = AutomergeWASM.importSyncState(inState)
|
|
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
|
throw new RangeError("must be the document root");
|
|
}
|
|
if (doc[FROZEN] === true) {
|
|
throw new RangeError("Attempting to use an outdated Automerge document")
|
|
}
|
|
if (!!doc[HEADS] === true) {
|
|
throw new RangeError("Attempting to change an out of date document");
|
|
}
|
|
if (doc[READ_ONLY] === false) {
|
|
throw new RangeError("Calls to Automerge.change cannot be nested")
|
|
}
|
|
const state = doc[STATE]
|
|
const heads = state.getHeads()
|
|
state.receiveSyncMessage(syncState, message)
|
|
const outState = AutomergeWASM.exportSyncState(syncState)
|
|
doc[HEADS] = heads
|
|
return [rootProxy(state, true), outState, null];
|
|
}
|
|
|
|
function initSyncState() {
|
|
return AutomergeWASM.exportSyncState(AutomergeWASM.initSyncState(change))
|
|
}
|
|
|
|
function encodeChange(change) {
|
|
return AutomergeWASM.encodeChange(change)
|
|
}
|
|
|
|
function decodeChange(data) {
|
|
return AutomergeWASM.decodeChange(data)
|
|
}
|
|
|
|
function encodeSyncMessage(change) {
|
|
return AutomergeWASM.encodeSyncMessage(change)
|
|
}
|
|
|
|
function decodeSyncMessage(data) {
|
|
return AutomergeWASM.decodeSyncMessage(data)
|
|
}
|
|
|
|
function getMissingDeps(doc, heads) {
|
|
const state = doc[STATE]
|
|
return state.getMissingDeps(heads)
|
|
}
|
|
|
|
function getHeads(doc) {
|
|
const state = doc[STATE]
|
|
return doc[HEADS] || state.getHeads()
|
|
}
|
|
|
|
function dump(doc) {
|
|
const state = doc[STATE]
|
|
state.dump()
|
|
}
|
|
|
|
function toJS(doc) {
|
|
if (typeof doc === "object") {
|
|
if (doc instanceof Uint8Array) {
|
|
return doc
|
|
}
|
|
if (doc === null) {
|
|
return doc
|
|
}
|
|
if (doc instanceof Array) {
|
|
return doc.map((a) => toJS(a))
|
|
}
|
|
if (doc instanceof Text) {
|
|
return doc.map((a) => toJS(a))
|
|
}
|
|
let tmp = {}
|
|
for (index in doc) {
|
|
tmp[index] = toJS(doc[index])
|
|
}
|
|
return tmp
|
|
} else {
|
|
return doc
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
init, from, change, emptyChange, clone, free,
|
|
load, save, merge, getChanges, getAllChanges, applyChanges,
|
|
getLastLocalChange, getObjectId, getActorId, getConflicts,
|
|
encodeChange, decodeChange, equals, getHistory, getHeads, uuid,
|
|
generateSyncMessage, receiveSyncMessage, initSyncState,
|
|
decodeSyncMessage, encodeSyncMessage, decodeSyncState, encodeSyncState,
|
|
getMissingDeps,
|
|
dump, Text, Counter, Int, Uint, Float64, toJS,
|
|
}
|
|
|
|
// depricated
|
|
// Frontend, setDefaultBackend, Backend
|
|
|
|
// more...
|
|
/*
|
|
for (let name of ['getObjectId', 'getObjectById',
|
|
'setActorId',
|
|
'Text', 'Table', 'Counter', 'Observable' ]) {
|
|
module.exports[name] = Frontend[name]
|
|
}
|
|
*/
|