diff --git a/automerge-js/src/constants.ts b/automerge-js/src/constants.ts index aa414c8b..e37835d1 100644 --- a/automerge-js/src/constants.ts +++ b/automerge-js/src/constants.ts @@ -3,6 +3,7 @@ //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 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) diff --git a/automerge-js/src/index.ts b/automerge-js/src/index.ts index ef231727..a8e3e2be 100644 --- a/automerge-js/src/index.ts +++ b/automerge-js/src/index.ts @@ -2,7 +2,7 @@ export { uuid } from './uuid' import { rootProxy, listProxy, textProxy, mapProxy } from "./proxies" -import { STATE, HEADS, OBJECT_ID, READ_ONLY, FROZEN } from "./constants" +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" @@ -48,6 +48,20 @@ 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) } @@ -104,7 +118,7 @@ function _change(doc: Doc, options: ChangeOptions, callback: ChangeFn): 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"); + 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") @@ -112,13 +126,13 @@ function _change(doc: Doc, options: ChangeOptions, callback: ChangeFn): const state = _state(doc) const heads = state.getHeads() try { - Reflect.set(doc,HEADS,heads) + _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) - Reflect.set(doc,HEADS,undefined) + _clear_heads(doc) return doc } else { state.commit(options.message, options.time) @@ -127,7 +141,7 @@ function _change(doc: Doc, options: ChangeOptions, callback: ChangeFn): } catch (e) { //console.log("ERROR: ",e) Reflect.set(doc,FROZEN,false) - Reflect.set(doc,HEADS,undefined) + _clear_heads(doc) state.rollback() throw e } @@ -168,14 +182,14 @@ export function save(doc: Doc) : Uint8Array { export function merge(local: Doc, remote: Doc) : Doc { if (!!_heads(local) === true) { - throw new RangeError("Attempting to change an out of date document"); + 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) - Reflect.set(local,HEADS,heads) + _set_heads(local,heads) return rootProxy(localState, true) } @@ -267,7 +281,7 @@ export function applyChanges(doc: Doc, changes: Change[]) : [Doc] { const state = _state(doc) const heads = state.getHeads() state.applyChanges(changes) - Reflect.set(doc,HEADS,heads) + _set_heads(doc,heads) return [rootProxy(state, true)]; } @@ -322,7 +336,7 @@ export function receiveSyncMessage(doc: Doc, inState: SyncState, message: 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"); + 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") @@ -330,7 +344,7 @@ export function receiveSyncMessage(doc: Doc, inState: SyncState, message: const state = _state(doc) const heads = state.getHeads() state.receiveSyncMessage(syncState, message) - Reflect.set(doc,HEADS,heads) + _set_heads(doc,heads) const outState = ApiHandler.exportSyncState(syncState) return [rootProxy(state, true), outState, null]; } diff --git a/automerge-js/src/proxies.ts b/automerge-js/src/proxies.ts index a19a1b9f..8de6c954 100644 --- a/automerge-js/src/proxies.ts +++ b/automerge-js/src/proxies.ts @@ -5,7 +5,7 @@ import { AutomergeValue, ScalarValue, MapValue, ListValue, TextValue } from "./t import { Int, Uint, Float64 } from "./numbers" import { Counter, getWriteableCounter } from "./counter" import { Text } from "./text" -import { STATE, HEADS, FROZEN, OBJECT_ID, READ_ONLY, COUNTER, INT, UINT, F64, TEXT } from "./constants" +import { STATE, HEADS, TRACE, FROZEN, OBJECT_ID, READ_ONLY, COUNTER, INT, UINT, F64, TEXT } from "./constants" function parseListIndex(key) { if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10) @@ -108,6 +108,7 @@ const MapHandler = { if (key === READ_ONLY) return readonly if (key === FROZEN) return frozen if (key === HEADS) return heads + if (key === TRACE) return target.trace if (key === STATE) return context; if (!cache[key]) { cache[key] = valueAt(target, key) @@ -129,6 +130,10 @@ const MapHandler = { target.heads = val return true } + if (key === TRACE) { + target.trace = val + return true + } const [ value, datatype ] = import_value(val) if (frozen) { throw new RangeError("Attempting to use an outdated Automerge document") @@ -211,6 +216,7 @@ const ListHandler = { if (index === READ_ONLY) return readonly if (index === FROZEN) return frozen if (index === HEADS) return heads + if (index === TRACE) return target.trace if (index === STATE) return context; if (index === 'length') return context.length(objectId, heads); if (index === Symbol.iterator) { @@ -246,6 +252,10 @@ const ListHandler = { target.heads = val return true } + if (index === TRACE) { + target.trace = val + return true + } if (typeof index == "string") { throw new RangeError('list index must be a number') } @@ -356,6 +366,7 @@ const TextHandler = Object.assign({}, ListHandler, { if (index === READ_ONLY) return readonly if (index === FROZEN) return frozen if (index === HEADS) return heads + if (index === TRACE) return target.trace if (index === STATE) return context; if (index === 'length') return context.length(objectId, heads); if (index === Symbol.iterator) { diff --git a/automerge-wasm/.gitignore b/automerge-wasm/.gitignore index a5ef445c..90f5b649 100644 --- a/automerge-wasm/.gitignore +++ b/automerge-wasm/.gitignore @@ -1,5 +1,7 @@ /node_modules /dev +/node +/web /target Cargo.lock yarn.lock diff --git a/automerge-wasm/Cargo.toml b/automerge-wasm/Cargo.toml index f7668bfa..e80e8798 100644 --- a/automerge-wasm/Cargo.toml +++ b/automerge-wasm/Cargo.toml @@ -40,10 +40,10 @@ version = "^0.2" features = ["serde-serialize", "std"] [package.metadata.wasm-pack.profile.release] -# wasm-opt = false +wasm-opt = true [package.metadata.wasm-pack.profile.profiling] -wasm-opt = false +wasm-opt = true # The `web-sys` crate allows you to interact with the various browser APIs, # like the DOM. diff --git a/automerge-wasm/attr_bug.js b/automerge-wasm/attr_bug.js new file mode 100644 index 00000000..324fba33 --- /dev/null +++ b/automerge-wasm/attr_bug.js @@ -0,0 +1,15 @@ +let Automerge = require(".") +let util = require('util') + +let heads = ['d138235e8123c407852968a976bb3d05bb30b9f7639854e64cb4adee98a407a6'] +let newHeads = ['d2a0500dad1b4ef1ca0f66015ae24f5cd7bec8316aa8e1115640a665e188147e'] +let text = '10@e1761c3ec92a87d3620d1bc007bdf83a000015ca0b60684edfd007672a0f00113ba1' +let data = '133,111,74,131,126,182,225,217,0,130,22,22,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,45,11,84,68,75,148,168,76,245,27,147,189,91,99,157,102,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,60,72,31,34,255,16,190,226,176,124,232,19,117,181,152,202,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,173,17,57,82,13,196,120,217,253,4,117,222,120,203,127,31,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,195,238,208,1,215,183,150,181,230,202,10,131,10,53,212,98,16,118,64,44,216,205,38,70,50,172,104,141,96,213,70,225,153,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,7,90,18,166,242,242,169,181,172,173,95,218,197,230,53,171,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,20,123,52,22,113,155,106,167,61,96,211,220,13,176,202,18,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,21,202,11,96,104,78,223,208,7,103,42,15,0,17,59,161,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,49,157,99,144,176,89,107,142,238,50,16,33,198,172,12,98,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,49,160,189,244,223,205,155,34,245,110,74,38,170,63,47,165,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,101,43,36,88,127,139,248,176,98,81,75,151,178,155,65,235,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,104,72,125,26,22,39,88,236,174,2,180,0,186,44,23,100,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,106,192,146,37,220,38,124,176,133,96,99,183,52,146,51,32,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,137,185,129,79,171,192,93,254,162,191,198,11,166,169,184,231,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,183,221,99,120,31,214,103,85,152,145,225,205,226,10,71,148,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,204,247,249,8,135,23,98,57,29,144,111,93,62,1,176,68,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,243,90,241,176,57,235,58,247,98,38,71,96,245,193,178,119,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,23,61,123,236,184,3,106,194,171,46,241,84,223,211,110,241,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,32,181,113,40,11,161,118,67,217,36,93,201,189,221,55,174,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,97,188,15,173,96,163,123,87,228,32,227,245,56,237,53,228,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,97,221,248,228,210,133,45,170,105,131,177,2,9,124,254,61,16,255,46,217,125,15,181,79,74,181,101,95,13,121,190,236,160,1,210,160,80,13,173,27,78,241,202,15,102,1,90,226,79,92,215,190,200,49,106,168,225,17,86,64,166,101,225,136,20,126,8,1,48,3,78,19,38,35,3,53,77,64,17,67,20,86,3,14,1,11,2,17,17,165,1,27,242,1,21,202,1,33,238,1,43,187,2,52,4,66,51,86,78,95,177,2,128,1,45,129,1,15,131,1,23,127,7,21,4,24,8,12,15,3,14,15,6,193,0,12,8,13,3,10,127,9,6,11,127,16,3,2,20,3,10,5,33,3,126,5,17,17,1,11,17,24,21,12,0,125,18,20,19,126,1,0,20,1,127,108,23,1,127,105,11,1,127,117,2,1,127,126,14,1,127,114,192,0,1,127,64,7,1,127,121,2,1,126,126,0,5,1,126,123,0,2,1,127,126,19,1,127,109,9,1,127,11,32,1,125,86,118,0,16,1,127,113,10,1,127,117,23,1,127,105,11,1,127,117,2,0,127,14,2,3,16,1,127,3,2,1,127,2,9,1,2,2,14,1,121,2,1,2,1,2,1,2,171,1,1,127,2,28,1,126,0,4,38,1,167,2,0,255,1,0,127,70,123,34,97,117,116,104,111,114,73,100,34,58,34,101,49,55,54,49,99,51,101,99,57,50,97,56,55,100,51,54,50,48,100,49,98,99,48,48,55,98,100,102,56,51,97,34,44,34,109,101,115,115,97,103,101,34,58,34,74,97,102,102,97,32,67,97,107,101,34,125,39,0,127,0,192,1,1,127,2,32,1,127,2,18,1,127,2,49,1,127,0,191,1,1,126,119,10,33,1,126,95,34,17,1,126,112,17,49,1,167,2,7,0,17,161,2,7,127,4,3,8,4,15,0,17,3,9,157,2,10,119,12,19,42,53,55,71,74,77,80,0,21,5,7,124,6,7,15,7,3,4,127,3,4,4,127,6,7,4,124,6,12,3,4,2,15,127,3,2,15,127,6,15,3,127,6,14,3,11,21,2,3,127,6,11,4,126,6,12,15,1,5,4,126,12,3,13,4,127,14,3,4,2,14,124,6,12,3,4,3,8,127,10,9,17,126,8,12,10,21,126,8,6,5,8,127,3,3,8,127,12,8,13,3,8,127,6,6,8,2,14,124,6,12,3,8,4,15,127,6,2,8,123,15,12,3,15,6,2,8,127,12,2,2,123,8,6,12,3,8,4,15,127,6,6,8,125,12,10,3,5,8,127,6,2,8,11,0,16,8,127,6,2,12,127,9,6,11,126,16,3,2,4,0,10,69,142,189,75,66,97,28,133,207,121,223,50,149,140,140,43,213,32,45,125,64,208,93,226,6,81,75,83,67,4,145,91,91,67,91,229,216,7,250,187,16,33,53,93,250,35,154,171,185,177,32,130,162,156,36,18,84,156,28,85,80,55,125,95,21,228,89,14,156,7,206,129,35,152,28,195,113,30,79,254,230,110,8,250,133,63,2,226,23,207,62,97,121,67,162,128,188,184,184,103,91,48,27,112,138,82,149,46,98,244,189,40,211,59,108,202,235,80,181,124,192,185,179,74,32,84,56,225,63,62,81,148,65,165,161,76,114,15,21,214,48,190,250,142,178,85,36,199,10,194,204,62,72,17,143,140,48,211,202,122,123,74,243,58,96,221,28,249,195,66,136,87,184,49,11,235,251,70,191,32,22,161,189,173,154,36,53,206,166,83,42,254,5,55,231,39,15,88,198,10,59,178,180,189,81,147,121,83,57,41,104,5,150,48,23,239,244,247,151,143,194,13,70,121,122,43,151,163,183,150,196,55,24,155,96,102,166,32,233,115,68,122,127,8,97,114,99,104,105,118,101,100,2,6,97,117,116,104,111,114,126,8,99,111,109,109,101,110,116,115,12,99,111,110,116,114,105,98,117,116,111,114,115,3,7,109,101,115,115,97,103,101,2,9,112,97,114,101,110,116,95,105,100,125,6,112,105,110,110,101,100,6,115,104,97,114,101,100,4,116,101,120,116,3,4,116,105,109,101,124,5,116,105,116,108,101,32,48,48,97,101,48,56,49,52,48,99,50,54,55,54,56,99,53,102,52,99,55,98,56,98,48,54,100,52,98,98,49,54,32,101,49,55,54,49,99,51,101,99,57,50,97,56,55,100,51,54,50,48,100,49,98,99,48,48,55,98,100,102,56,51,97,32,101,53,57,54,102,53,56,56,52,99,57,55,51,98,55,49,53,100,55,48,57,53,101,97,48,55,52,52,49,52,100,53,0,157,2,9,4,116,121,112,101,2,7,127,21,3,7,124,4,21,4,21,4,7,119,4,21,7,1,7,17,7,5,3,2,12,121,6,12,15,12,4,12,4,2,3,111,12,4,12,6,12,4,12,4,12,4,12,6,12,3,5,15,5,2,3,126,12,6,30,3,12,21,2,3,115,12,4,12,4,12,4,12,4,12,4,12,6,12,16,1,123,4,10,12,4,12,2,3,102,4,12,4,12,4,12,4,12,4,12,4,12,14,12,4,12,14,12,6,12,3,5,8,5,3,10,10,17,127,12,11,21,126,6,12,4,8,2,3,125,12,8,12,7,13,102,5,13,8,12,6,12,8,5,12,8,12,14,12,6,12,3,5,15,5,3,12,6,12,8,15,12,2,3,124,6,12,8,12,3,2,117,6,12,3,5,15,5,3,12,6,12,15,2,8,124,12,8,12,10,2,3,121,8,12,8,12,6,12,8,12,0,112,12,8,12,8,12,8,12,8,12,8,12,8,12,8,12,6,2,12,127,9,6,11,125,16,3,5,2,4,2,7,127,4,3,8,4,15,117,143,207,43,131,113,28,199,63,239,103,101,241,236,49,19,113,115,81,147,178,56,108,10,7,92,108,56,40,162,20,218,193,109,139,210,52,179,231,251,41,23,148,103,59,40,7,57,176,54,155,205,197,143,63,65,106,53,57,49,108,53,187,56,104,23,66,118,179,135,92,148,195,235,211,235,240,254,244,249,188,157,85,193,93,169,164,194,95,187,37,125,168,154,244,164,178,113,85,214,164,103,181,140,144,120,129,38,46,193,147,253,203,242,76,235,17,223,81,138,79,128,40,167,248,150,2,198,36,103,233,23,79,99,150,228,55,161,33,104,218,97,79,24,102,176,91,1,219,101,44,14,96,157,227,252,64,127,241,54,108,84,98,179,97,177,13,231,33,231,40,77,200,139,28,233,170,147,224,123,210,79,234,209,137,14,125,142,219,166,218,47,200,116,35,142,177,226,46,82,53,68,73,196,80,3,53,41,218,98,108,64,32,12,184,244,181,150,42,204,93,211,41,175,85,124,218,26,231,12,66,223,31,12,119,143,218,123,149,178,216,132,111,112,172,43,202,150,12,217,16,225,206,3,126,36,171,132,249,61,238,115,40,239,149,18,75,174,17,199,25,123,165,2,69,184,64,205,22,124,138,31,29,234,73,240,43,100,120,243,98,159,139,244,31,231,124,69,80,140,240,213,155,211,98,193,47,25,155,100,169,206,96,248,2,20,157,2,9,3,1,2,0,7,1,127,4,7,1,127,0,28,1,127,0,235,0,1,127,0,43,1,127,7,6,1,127,9,9,1,127,0,5,1,127,0,15,1,127,0,5,1,2,0,55,1,127,0,10,1,127,1,2,134,4,2,0,125,230,1,166,1,182,1,2,214,2,2,1,126,0,105,2,100,127,6,3,2,127,0,28,22,127,0,30,22,127,38,204,0,22,127,0,43,22,125,102,2,6,6,22,127,2,9,22,127,0,5,22,127,0,15,22,127,0,5,22,2,0,54,22,125,38,0,22,9,150,1,173,80,75,78,195,48,16,189,74,110,208,184,78,234,100,87,17,85,21,16,169,5,129,248,108,208,56,246,164,85,211,16,98,47,210,172,145,216,245,2,108,122,150,46,144,224,74,116,193,36,161,233,5,106,217,158,55,243,230,243,108,205,196,136,37,92,39,225,16,2,161,248,104,232,42,38,19,215,21,82,97,192,65,251,225,8,253,32,240,146,80,112,41,152,175,132,27,250,26,92,225,121,204,83,254,253,252,97,18,199,47,183,179,217,221,120,60,190,2,68,112,34,88,233,233,50,79,117,41,75,13,10,163,236,102,184,201,46,39,69,52,227,79,38,136,179,231,183,245,133,143,108,90,101,139,98,190,102,122,163,217,99,140,215,209,251,215,246,251,119,240,209,222,135,159,207,237,97,32,107,107,43,203,44,88,131,96,149,229,22,27,76,177,90,202,26,201,46,150,198,161,13,14,161,60,117,122,127,191,107,2,70,33,28,15,145,170,45,111,110,110,13,41,51,10,254,121,84,176,34,138,16,145,202,58,132,129,186,55,147,186,89,85,223,135,90,244,80,85,173,131,77,54,118,203,216,242,53,79,201,240,118,208,177,158,140,75,237,121,151,216,199,154,199,164,8,157,164,147,86,210,211,82,39,193,21,125,4,45,206,121,85,203,253,206,41,160,132,180,132,98,113,46,240,7,126,0,1,3,0,2,1,126,0,1,4,0,2,1,51,0,127,1,10,0,127,1,219,0,0,127,1,15,0,3,1,36,0,127,1,21,0,2,1,54,0,127,1,11,0,126,21,4,2,21,123,4,21,18,19,20,6,15,127,4,118,156,2,243,125,140,2,3,242,125,141,2,37,2,127,145,126,2,127,3,125,127,92' + +let doc = Automerge.loadDoc(new Uint8Array(data.toString().split(",").map((n) => parseInt(n)))) + +console.log(doc.text(text,heads)) +console.log(doc.text(text,newHeads)) +console.log(doc.text(text)) +console.log(util.inspect(doc.attribute(text,heads,[newHeads]), false, null, false)) + diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index d515b3c7..eb48c402 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -1,2 +1,38 @@ +import { Automerge as VanillaAutomerge } from "automerge-types" + export * from "automerge-types" export { default } from "automerge-types" + +export class Automerge extends VanillaAutomerge { + // experimental spans api - unstable! + mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; + unmark(obj: ObjID, mark: ObjID): void; + spans(obj: ObjID): any; + raw_spans(obj: ObjID): any; + blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + attribute(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + attribute2(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + + // override old methods that return automerge + clone(actor?: string): Automerge; + fork(actor?: string): Automerge; + forkAt(heads: Heads, actor?: string): Automerge; +} + +export type ChangeSetDeletion = { + pos: number; + val: string; +} + +export type ChangeSetAddition = { + start: number; + end: number; +}; + +export type ChangeSet = { + add: ChangeSetAddition[]; + del: ChangeSetDeletion[]; +}; + +export function create(actor?: Actor): Automerge; +export function load(data: Uint8Array, actor?: Actor): Automerge; diff --git a/automerge-wasm/nodejs-index.js b/automerge-wasm/nodejs-index.js index 07087e59..4a42f201 100644 --- a/automerge-wasm/nodejs-index.js +++ b/automerge-wasm/nodejs-index.js @@ -2,6 +2,4 @@ let wasm = require("./bindgen") module.exports = wasm module.exports.load = module.exports.loadDoc delete module.exports.loadDoc -Object.defineProperty(module.exports, "__esModule", { value: true }) module.exports.init = () => (new Promise((resolve,reject) => { resolve(module.exports) })) -module.exports.default = module.exports.init diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 50744364..9f7a2a30 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -4,11 +4,11 @@ "Alex Good ", "Martin Kleppmann" ], - "name": "automerge-wasm", + "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", "homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-wasm", "repository": "github:automerge/automerge-rs", - "version": "0.1.6", + "version": "0.1.8", "license": "MIT", "files": [ "README.md", diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index bc17c018..a8ced00b 100644 --- a/automerge-wasm/src/interop.rs +++ b/automerge-wasm/src/interop.rs @@ -352,6 +352,15 @@ pub(crate) fn get_heads(heads: Option) -> Option> { heads.ok() } +pub(crate) fn get_js_heads(heads: JsValue) -> Result, JsValue> { + let heads = heads.dyn_into::()?; + heads + .iter() + .map(|j| j.into_serde()) + .collect::, _>>() + .map_err(to_js_err) +} + pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue { let keys = doc.keys(obj); let map = Object::new(); diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 9111a4de..49c321a9 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -34,6 +34,7 @@ use automerge::Patch; use automerge::VecOpObserver; use automerge::{Change, ObjId, Prop, Value, ROOT}; use js_sys::{Array, Object, Uint8Array}; +use regex::Regex; use std::convert::TryInto; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -43,8 +44,8 @@ mod sync; mod value; use interop::{ - get_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at, to_js_err, - to_objtype, to_prop, AR, JS, + get_heads, get_js_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at, + to_js_err, to_objtype, to_prop, AR, JS, }; use sync::SyncState; use value::{datatype, ScalarValue}; @@ -161,12 +162,9 @@ impl Automerge { } else { ApplyOptions::default() }; - let heads = self.doc.merge_with(&mut other.doc, options)?; - let heads: Array = heads - .iter() - .map(|h| JsValue::from_str(&hex::encode(&h.0))) - .collect(); - Ok(heads) + let objs = self.doc.merge_with(&mut other.doc, options)?; + let objs: Array = objs.iter().map(|o| JsValue::from(o.to_string())).collect(); + Ok(objs) } pub fn rollback(&mut self) -> f64 { @@ -292,6 +290,18 @@ impl Automerge { Ok(()) } + pub fn make( + &mut self, + obj: JsValue, + prop: JsValue, + value: JsValue, + _datatype: JsValue, + ) -> Result { + // remove this + am::log!("doc.make() is depricated - please use doc.set_object() or doc.insert_object()"); + self.put_object(obj, prop, value) + } + #[wasm_bindgen(js_name = putObject)] pub fn put_object( &mut self, @@ -311,7 +321,7 @@ impl Automerge { fn subset(&mut self, obj: &am::ObjId, vals: Vec<(am::Prop, JsValue)>) -> Result<(), JsValue> { for (p, v) in vals { let (value, subvals) = self.import_value(&v, None)?; - //let opid = self.0.set(id, p, value)?; + //let opid = self.doc.set(id, p, value)?; let opid = match (p, value) { (Prop::Map(s), Value::Object(objtype)) => { Some(self.doc.put_object(obj, s, objtype)?) @@ -551,6 +561,209 @@ impl Automerge { Ok(()) } + pub fn mark( + &mut self, + obj: JsValue, + range: JsValue, + name: JsValue, + value: JsValue, + datatype: JsValue, + ) -> Result<(), JsValue> { + let obj = self.import(obj)?; + let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); + let range = range.as_string().ok_or("range must be a string")?; + let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; + let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; + let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; + let start_sticky = &cap[1] == "("; + let end_sticky = &cap[4] == ")"; + let name = name + .as_string() + .ok_or("invalid mark name") + .map_err(to_js_err)?; + let value = self + .import_scalar(&value, &datatype.as_string()) + .ok_or_else(|| to_js_err("invalid value"))?; + self.doc + .mark(&obj, start, start_sticky, end, end_sticky, &name, value) + .map_err(to_js_err)?; + Ok(()) + } + + pub fn unmark(&mut self, obj: JsValue, mark: JsValue) -> Result<(), JsValue> { + let obj = self.import(obj)?; + let mark = self.import(mark)?; + self.doc.unmark(&obj, &mark).map_err(to_js_err)?; + Ok(()) + } + + pub fn spans(&mut self, obj: JsValue) -> Result { + let obj = self.import(obj)?; + let text: Vec<_> = self.doc.list_range(&obj, ..).collect(); + let spans = self.doc.spans(&obj).map_err(to_js_err)?; + let mut last_pos = 0; + let result = Array::new(); + for s in spans { + let marks = Array::new(); + for m in s.marks { + let mark = Array::new(); + mark.push(&m.0.into()); + mark.push(&datatype(&m.1).into()); + mark.push(&ScalarValue(m.1).into()); + marks.push(&mark.into()); + } + let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos); + if !text_span.is_empty() { + let t: String = text_span + .iter() + .filter_map(|(_, v, _)| v.as_string()) + .collect(); + result.push(&t.into()); + } + result.push(&marks); + last_pos = s.pos; + //let obj = Object::new().into(); + //js_set(&obj, "pos", s.pos as i32)?; + //js_set(&obj, "marks", marks)?; + //result.push(&obj.into()); + } + let text_span = &text[last_pos..]; + if !text_span.is_empty() { + let t: String = text_span + .iter() + .filter_map(|(_, v, _)| v.as_string()) + .collect(); + result.push(&t.into()); + } + Ok(result.into()) + } + + pub fn raw_spans(&mut self, obj: JsValue) -> Result { + let obj = self.import(obj)?; + let spans = self.doc.raw_spans(&obj).map_err(to_js_err)?; + let result = Array::new(); + for s in spans { + result.push(&JsValue::from_serde(&s).map_err(to_js_err)?); + } + Ok(result) + } + + pub fn blame( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + am::log!("doc.blame() is depricated - please use doc.attribute()"); + self.attribute(obj, baseline, change_sets) + } + + pub fn attribute( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + let obj = self.import(obj)?; + let baseline = get_js_heads(baseline)?; + let change_sets = change_sets.dyn_into::()?; + let change_sets = change_sets + .iter() + .map(get_js_heads) + .collect::, _>>()?; + let result = self.doc.attribute(&obj, &baseline, &change_sets)?; + let result = result + .into_iter() + .map(|cs| { + let add = cs + .add + .iter() + .map::, _>(|range| { + let r = Object::new(); + js_set(&r, "start", range.start as f64)?; + js_set(&r, "end", range.end as f64)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let del = cs + .del + .iter() + .map::, _>(|d| { + let r = Object::new(); + js_set(&r, "pos", d.0 as f64)?; + js_set(&r, "val", &d.1)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let obj = Object::new(); + js_set(&obj, "add", add)?; + js_set(&obj, "del", del)?; + Ok(obj.into()) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + Ok(result) + } + + pub fn attribute2( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + let obj = self.import(obj)?; + let baseline = get_js_heads(baseline)?; + let change_sets = change_sets.dyn_into::()?; + let change_sets = change_sets + .iter() + .map(get_js_heads) + .collect::, _>>()?; + let result = self.doc.attribute2(&obj, &baseline, &change_sets)?; + let result = result + .into_iter() + .map(|cs| { + let add = cs + .add + .iter() + .map::, _>(|a| { + let r = Object::new(); + js_set(&r, "actor", &self.doc.actor_to_str(a.actor))?; + js_set(&r, "start", a.range.start as f64)?; + js_set(&r, "end", a.range.end as f64)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let del = cs + .del + .iter() + .map::, _>(|d| { + let r = Object::new(); + js_set(&r, "actor", &self.doc.actor_to_str(d.actor))?; + js_set(&r, "pos", d.pos as f64)?; + js_set(&r, "val", &d.span)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let obj = Object::new(); + js_set(&obj, "add", add)?; + js_set(&obj, "del", del)?; + Ok(obj.into()) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + Ok(result) + } + pub fn save(&mut self) -> Uint8Array { self.ensure_transaction_closed(); Uint8Array::from(self.doc.save().as_slice()) diff --git a/automerge-wasm/test/attribute.ts b/automerge-wasm/test/attribute.ts new file mode 100644 index 00000000..3369acae --- /dev/null +++ b/automerge-wasm/test/attribute.ts @@ -0,0 +1,188 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { BloomFilter } from './helpers/sync' +import { create, load, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { DecodedSyncMessage, Hash } from '..' + +describe('Automerge', () => { + describe('attribute', () => { + it('should be able to attribute text segments on change sets', () => { + let doc1 = create() + let text = doc1.putObject("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork(); + doc2.splice(text, 5, 7, " big"); + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork(); + doc3.splice(text, 0, 0, "Well, "); + let h3 = doc3.getHeads(); + assert.deepEqual(doc3.text(text), "Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc3) + assert.deepEqual(doc1.text(text), "Well, hello big world") + let attribute = doc1.attribute(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { start: 11, end: 15 } ], del: [ { pos: 15, val: ' little' } ] }, + { add: [ { start: 0, end: 6 } ], del: [] } + ]) + }) + + it('should be able to hand complex attribute change sets', () => { + let doc1 = create("aaaa") + let text = doc1.putObject("_root", "notes","AAAAAA") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork("bbbb"); + doc2.splice(text, 0, 2, "BB"); + doc2.commit() + doc2.splice(text, 2, 2, "BB"); + doc2.commit() + doc2.splice(text, 6, 0, "BB"); + doc2.commit() + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "BBBBAABB") + + let doc3 = doc1.fork("cccc"); + doc3.splice(text, 1, 1, "C"); + doc3.commit() + doc3.splice(text, 3, 1, "C"); + doc3.commit() + doc3.splice(text, 5, 1, "C"); + doc3.commit() + let h3 = doc3.getHeads(); + // with tombstones its + // AC.AC.AC. + assert.deepEqual(doc3.text(text), "ACACAC") + + doc1.merge(doc2) + + assert.deepEqual(doc1.attribute(text, h1, [h2]), [ + { add: [ {start:0, end: 4}, { start: 6, end: 8 } ], del: [ { pos: 4, val: 'AAAA' } ] }, + ]) + + doc1.merge(doc3) + + assert.deepEqual(doc1.text(text), "BBBBCCACBB") + + // with tombstones its + // BBBB.C..C.AC.BB + assert.deepEqual(doc1.attribute(text, h1, [h2,h3]), [ + { add: [ {start:0, end: 4}, { start: 8, end: 10 } ], del: [ { pos: 4, val: 'A' }, { pos: 5, val: 'AA' }, { pos: 6, val: 'A' } ] }, + { add: [ {start:4, end: 6}, { start: 7, end: 8 } ], del: [ { pos: 5, val: 'A' }, { pos: 6, val: 'A' }, { pos: 8, val: 'A' } ] } + ]) + }) + + it('should not include attribution of text that is inserted and deleted only within change sets', () => { + let doc1 = create() + let text = doc1.putObject("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork(); + doc2.splice(text, 5, 7, " big"); + doc2.splice(text, 9, 0, " bad"); + doc2.splice(text, 9, 4) + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork(); + doc3.splice(text, 0, 0, "Well, HI THERE"); + doc3.splice(text, 6, 8, "") + let h3 = doc3.getHeads(); + assert.deepEqual(doc3.text(text), "Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc3) + assert.deepEqual(doc1.text(text), "Well, hello big world") + let attribute = doc1.attribute(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { start: 11, end: 15 } ], del: [ { pos: 15, val: ' little' } ] }, + { add: [ { start: 0, end: 6 } ], del: [] } + ]) + }) + + }) + describe('attribute2', () => { + it('should be able to attribute text segments on change sets', () => { + let doc1 = create("aaaa") + let text = doc1.putObject("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork("bbbb"); + doc2.splice(text, 5, 7, " big"); + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork("cccc"); + doc3.splice(text, 0, 0, "Well, "); + let doc4 = doc3.fork("dddd") + doc4.splice(text, 0, 0, "Gee, "); + let h3 = doc4.getHeads(); + assert.deepEqual(doc4.text(text), "Gee, Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc4) + assert.deepEqual(doc1.text(text), "Gee, Well, hello big world") + let attribute = doc1.attribute2(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { actor: "bbbb", start: 16, end: 20 } ], del: [ { actor: "bbbb", pos: 20, val: ' little' } ] }, + { add: [ { actor: "dddd", start:0, end: 5 }, { actor: "cccc", start: 5, end: 11 } ], del: [] } + ]) + }) + + it('should not include attribution of text that is inserted and deleted only within change sets', () => { + let doc1 = create("aaaa") + let text = doc1.putObject("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork("bbbb"); + doc2.splice(text, 5, 7, " big"); + doc2.splice(text, 9, 0, " bad"); + doc2.splice(text, 9, 4) + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork("cccc"); + doc3.splice(text, 0, 0, "Well, HI THERE"); + doc3.splice(text, 6, 8, "") + let h3 = doc3.getHeads(); + assert.deepEqual(doc3.text(text), "Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc3) + assert.deepEqual(doc1.text(text), "Well, hello big world") + let attribute = doc1.attribute2(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { start: 11, end: 15, actor: "bbbb" } ], del: [ { pos: 15, val: ' little', actor: "bbbb" } ] }, + { add: [ { start: 0, end: 6, actor: "cccc" } ], del: [] } + ]) + + let h4 = doc1.getHeads() + + doc3.splice(text, 24, 0, "!!!") + doc1.merge(doc3) + + let h5 = doc1.getHeads() + + assert.deepEqual(doc1.text(text), "Well, hello big world!!!") + attribute = doc1.attribute2(text, h4, [h5]) + + assert.deepEqual(attribute, [ + { add: [ { start: 21, end: 24, actor: "cccc" } ], del: [] }, + ]) + }) + }) +}) diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts new file mode 100644 index 00000000..a525a2cf --- /dev/null +++ b/automerge-wasm/test/marks.ts @@ -0,0 +1,201 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { create, load, Automerge, encodeChange, decodeChange } from '..' + +describe('Automerge', () => { + describe('marks', () => { + it('should handle marks [..]', () => { + let doc = create() + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[3..6]", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.insert(list, 6, "A") + doc.insert(list, 3, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); + }) + + it('should handle marks [..] at the beginning of a string', () => { + let doc = create() + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.insert(list, 0, "A") + doc2.insert(list, 4, "B") + doc.merge(doc2) + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'A', [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'Bbbbccc' ]); + }) + + it('should handle marks [..] with splice', () => { + let doc = create() + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.splice(list, 0, 2, "AAA") + doc2.splice(list, 4, 0, "BBB") + doc.merge(doc2) + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'a', [], 'BBBbbbccc' ]); + }) + + it('should handle marks across multiple forks', () => { + let doc = create() + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark. + + let doc3 = doc.fork() + doc3.insert(list, 0, "AAA") // should not be included in mark. + + doc.merge(doc2) + doc.merge(doc3) + + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'aZa', [], 'bbbccc' ]); + }) + + + it('should handle marks with deleted ends [..]', () => { + let doc = create() + let list = doc.putObject("_root", "list", "") + + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[3..6]", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.delete(list,5); + doc.delete(list,5); + doc.delete(list,2); + doc.delete(list,2); + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + doc.insert(list, 3, "A") + doc.insert(list, 2, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaA', [ [ 'bold', 'boolean', true ] ], 'b', [], 'Acc' ]) + }) + + it('should handle sticky marks (..)', () => { + let doc = create() + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "(3..6)", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.insert(list, 6, "A") + doc.insert(list, 3, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); + }) + + it('should handle sticky marks with deleted ends (..)', () => { + let doc = create() + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "(3..6)", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.delete(list,5); + doc.delete(list,5); + doc.delete(list,2); + doc.delete(list,2); + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + doc.insert(list, 3, "A") + doc.insert(list, 2, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + + // make sure save/load can handle marks + + let doc2 = load(doc.save()) + spans = doc2.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + + assert.deepStrictEqual(doc.getHeads(), doc2.getHeads()) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + + it('should handle overlapping marks', () => { + let doc : Automerge = create("aabbcc") + let list = doc.putObject("_root", "list", "") + doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + doc.mark(list, "[0..37]", "bold" , true) + doc.mark(list, "[4..19]", "itallic" , true) + doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") + doc.commit("marks"); + let spans = doc.spans(list); + assert.deepStrictEqual(spans, + [ + [ [ 'bold', 'boolean', true ] ], + 'the ', + [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], + 'quick ', + [ + [ 'bold', 'boolean', true ], + [ 'comment', 'str', 'foxes are my favorite animal!' ], + [ 'itallic', 'boolean', true ] + ], + 'fox', + [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], + ' jumps', + [ [ 'bold', 'boolean', true ] ], + ' over the lazy dog', + [], + ] + ) + let text = doc.text(list); + assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); + let raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, + { id: "41@aabbcc", start: 4, end: 19, type: 'itallic', value: true }, + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + doc.unmark(list, "41@aabbcc") + raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + // mark sure encode decode can handle marks + + doc.unmark(list, "39@aabbcc") + raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + let all = doc.getChanges([]) + let decoded = all.map((c) => decodeChange(c)) + let encoded = decoded.map((c) => encodeChange(c)) + let doc2 = create(); + doc2.applyChanges(encoded) + + assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + }) +}) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 7c573061..bb906e70 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -396,6 +396,8 @@ describe('Automerge', () => { assert.deepEqual(change2, null) if (change1 === null) { throw new RangeError("change1 should not be null") } assert.deepEqual(decodeChange(change1).hash, head1[0]) + assert.deepEqual(head1.some((hash) => doc1.getChangeByHash(hash) === null), false) + assert.deepEqual(head2.some((hash) => doc1.getChangeByHash(hash) === null), true) }) it('recursive sets are possible', () => { @@ -1654,7 +1656,7 @@ describe('Automerge', () => { if (m2 === null) { throw new RangeError("message should not be null") } n1.receiveSyncMessage(s1, m2) n2.receiveSyncMessage(s2, m1) - + // Then n1 and n2 send each other their changes, except for the false positive m1 = n1.generateSyncMessage(s1) m2 = n2.generateSyncMessage(s2) diff --git a/automerge-wasm/types/index.d.ts b/automerge-wasm/types/index.d.ts index 68277203..ea57f9c2 100644 --- a/automerge-wasm/types/index.d.ts +++ b/automerge-wasm/types/index.d.ts @@ -205,5 +205,5 @@ export class SyncState { readonly sharedHeads: Heads; } -export default function init (): Promise; export function init (): Promise; + diff --git a/automerge-wasm/web-index.js b/automerge-wasm/web-index.js index 6510fe05..9bbe47df 100644 --- a/automerge-wasm/web-index.js +++ b/automerge-wasm/web-index.js @@ -47,7 +47,3 @@ export function init() { })) } -// depricating default export -export default function() { - return init() -} diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index 1233c1e0..ac72c60c 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -3,13 +3,14 @@ use std::ops::RangeBounds; use crate::exid::ExId; use crate::op_observer::OpObserver; use crate::transaction::{CommitOptions, Transactable}; +use crate::Parents; use crate::{ - sync, ApplyOptions, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, - Parents, ScalarValue, + query, transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, + Prop, Value, Values, }; use crate::{ - transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, - Value, Values, + sync, ApplyOptions, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, + ScalarValue, }; /// An automerge document that automatically manages transactions. @@ -33,6 +34,11 @@ impl AutoCommit { } } + // FIXME : temp + pub fn actor_to_str(&self, actor: usize) -> String { + self.doc.ops.m.actors.cache[actor].to_hex_string() + } + /// Get the inner document. #[doc(hidden)] pub fn document(&mut self) -> &Automerge { @@ -404,6 +410,37 @@ impl Transactable for AutoCommit { tx.insert(&mut self.doc, obj.as_ref(), index, value) } + #[allow(clippy::too_many_arguments)] + fn mark>( + &mut self, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + self.ensure_transaction_open(); + let tx = self.transaction.as_mut().unwrap(); + tx.mark( + &mut self.doc, + obj, + start, + expand_start, + end, + expand_end, + mark, + value, + ) + } + + fn unmark>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError> { + self.ensure_transaction_open(); + let tx = self.transaction.as_mut().unwrap(); + tx.unmark(&mut self.doc, obj, mark) + } + fn insert_object>( &mut self, obj: O, @@ -462,6 +499,32 @@ impl Transactable for AutoCommit { self.doc.text_at(obj, heads) } + fn spans>(&self, obj: O) -> Result>, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute(obj, baseline, change_sets) + } + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute2(obj, baseline, change_sets) + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index c167178b..0c08f6d2 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -452,6 +452,28 @@ impl Automerge { } } + pub(crate) fn exid_to_obj_tmp_unchecked(&self, id: &ExId) -> Result { + match id { + ExId::Root => Ok(ObjId::root()), + ExId::Id(ctr, actor, idx) => { + // do a direct get here b/c this could be foriegn and not be within the array + // bounds + if self.ops.m.actors.cache.get(*idx) == Some(actor) { + Ok(ObjId(OpId(*ctr, *idx))) + } else { + // FIXME - make a real error + let idx = self + .ops + .m + .actors + .lookup(actor) + .ok_or(AutomergeError::Fail)?; + Ok(ObjId(OpId(*ctr, idx))) + } + } + } + } + pub(crate) fn id_to_exid(&self, id: OpId) -> ExId { self.ops.id_to_exid(id) } @@ -491,6 +513,71 @@ impl Automerge { Ok(buffer) } + pub fn spans>(&self, obj: O) -> Result>, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let mut query = self.ops.search(&obj, query::Spans::new()); + query.check_marks(); + Ok(query.spans) + } + + pub fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let baseline = self.clock_at(baseline); + let change_sets: Vec = change_sets + .iter() + .map(|p| self.clock_at(p).unwrap()) + .collect(); + let mut query = self + .ops + .search(&obj, query::Attribute::new(baseline.unwrap(), change_sets)); + query.finish(); + Ok(query.change_sets) + } + + pub fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let baseline = self.clock_at(baseline); + let change_sets: Vec = change_sets + .iter() + .map(|p| self.clock_at(p).unwrap()) + .collect(); + let mut query = self + .ops + .search(&obj, query::Attribute2::new(baseline.unwrap(), change_sets)); + query.finish(); + Ok(query.change_sets) + } + + pub fn raw_spans>( + &self, + obj: O, + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let query = self.ops.search(&obj, query::RawSpans::new()); + let result = query + .spans + .into_iter() + .map(|s| query::SpanInfo { + id: self.id_to_exid(s.id), + start: s.start, + end: s.end, + span_type: s.name, + value: s.value, + }) + .collect(); + Ok(result) + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? @@ -1041,6 +1128,8 @@ impl Automerge { OpType::Put(value) => format!("{}", value), OpType::Make(obj) => format!("make({})", obj), OpType::Increment(obj) => format!("inc({})", obj), + OpType::MarkBegin(m) => format!("mark({}={})", m.name, m.value), + OpType::MarkEnd(_) => "/mark".into(), OpType::Delete => format!("del{}", 0), }; let pred: Vec<_> = op.pred.iter().map(|id| self.to_string(*id)).collect(); diff --git a/automerge/src/columnar.rs b/automerge/src/columnar.rs index 25748a25..6cd3b2fb 100644 --- a/automerge/src/columnar.rs +++ b/automerge/src/columnar.rs @@ -137,6 +137,15 @@ impl<'a> Iterator for OperationIterator<'a> { Action::MakeTable => OpType::Make(ObjType::Table), Action::Del => OpType::Delete, Action::Inc => OpType::Increment(value.to_i64()?), + Action::MarkBegin => { + // mark has 3 things in the val column + let name = value.as_string()?; + let expand = self.value.next()?.to_bool()?; + let value = self.value.next()?; + OpType::mark(name, expand, value) + } + Action::MarkEnd => OpType::MarkEnd(value.to_bool()?), + Action::Unused => panic!("invalid action"), }; Some(amp::Op { action, @@ -178,6 +187,15 @@ impl<'a> Iterator for DocOpIterator<'a> { Action::MakeTable => OpType::Make(ObjType::Table), Action::Del => OpType::Delete, Action::Inc => OpType::Increment(value.to_i64()?), + Action::MarkBegin => { + // mark has 3 things in the val column + let name = value.as_string()?; + let expand = self.value.next()?.to_bool()?; + let value = self.value.next()?; + OpType::mark(name, expand, value) + } + Action::MarkEnd => OpType::MarkEnd(value.to_bool()?), + Action::Unused => panic!("invalid action"), }; Some(DocOp { actor, @@ -1082,6 +1100,16 @@ impl DocOpEncoder { self.val.append_null(); Action::Del } + amp::OpType::MarkBegin(m) => { + self.val.append_value(&m.name.clone().into(), actors); + self.val.append_value(&m.expand.into(), actors); + self.val.append_value(&m.value.clone(), actors); + Action::MarkBegin + } + amp::OpType::MarkEnd(s) => { + self.val.append_value(&(*s).into(), actors); + Action::MarkEnd + } amp::OpType::Make(kind) => { self.val.append_null(); match kind { @@ -1191,6 +1219,16 @@ impl ColumnEncoder { self.val.append_null(); Action::Del } + OpType::MarkBegin(m) => { + self.val.append_value2(&m.name.clone().into(), actors); + self.val.append_value2(&m.expand.into(), actors); + self.val.append_value2(&m.value.clone(), actors); + Action::MarkBegin + } + OpType::MarkEnd(s) => { + self.val.append_value2(&(*s).into(), actors); + Action::MarkEnd + } OpType::Make(kind) => { self.val.append_null(); match kind { @@ -1296,8 +1334,11 @@ pub(crate) enum Action { MakeText, Inc, MakeTable, + MarkBegin, + Unused, // final bit is used to mask `Make` actions + MarkEnd, } -const ACTIONS: [Action; 7] = [ +const ACTIONS: [Action; 10] = [ Action::MakeMap, Action::Set, Action::MakeList, @@ -1305,6 +1346,9 @@ const ACTIONS: [Action; 7] = [ Action::MakeText, Action::Inc, Action::MakeTable, + Action::MarkBegin, + Action::Unused, + Action::MarkEnd, ]; impl Decodable for Action { diff --git a/automerge/src/legacy/serde_impls/op.rs b/automerge/src/legacy/serde_impls/op.rs index 0f7ef8c2..c540097f 100644 --- a/automerge/src/legacy/serde_impls/op.rs +++ b/automerge/src/legacy/serde_impls/op.rs @@ -50,6 +50,12 @@ impl Serialize for Op { OpType::Increment(n) => op.serialize_field("value", &n)?, OpType::Put(ScalarValue::Counter(c)) => op.serialize_field("value", &c.start)?, OpType::Put(value) => op.serialize_field("value", &value)?, + OpType::MarkBegin(m) => { + op.serialize_field("name", &m.name)?; + op.serialize_field("expand", &m.expand)?; + op.serialize_field("value", &m.value)?; + } + OpType::MarkEnd(s) => op.serialize_field("expand", &s)?, _ => {} } op.serialize_field("pred", &self.pred)?; @@ -71,6 +77,8 @@ pub(crate) enum RawOpType { Del, Inc, Set, + MarkBegin, + MarkEnd, } impl Serialize for RawOpType { @@ -86,6 +94,8 @@ impl Serialize for RawOpType { RawOpType::Del => "del", RawOpType::Inc => "inc", RawOpType::Set => "set", + RawOpType::MarkBegin => "mark_begin", + RawOpType::MarkEnd => "mark_end", }; serializer.serialize_str(s) } @@ -117,6 +127,8 @@ impl<'de> Deserialize<'de> for RawOpType { "del" => Ok(RawOpType::Del), "inc" => Ok(RawOpType::Inc), "set" => Ok(RawOpType::Set), + "mark_begin" => Ok(RawOpType::MarkBegin), + "mark_end" => Ok(RawOpType::MarkEnd), other => Err(Error::unknown_variant(other, VARIANTS)), } } @@ -189,6 +201,30 @@ impl<'de> Deserialize<'de> for Op { RawOpType::MakeList => OpType::Make(ObjType::List), RawOpType::MakeText => OpType::Make(ObjType::Text), RawOpType::Del => OpType::Delete, + RawOpType::MarkBegin => { + let name = name.ok_or_else(|| Error::missing_field("mark(name)"))?; + let expand = expand.unwrap_or(false); + let value = if let Some(datatype) = datatype { + let raw_value = value + .ok_or_else(|| Error::missing_field("value"))? + .unwrap_or(ScalarValue::Null); + raw_value.as_datatype(datatype).map_err(|e| { + Error::invalid_value( + Unexpected::Other(e.unexpected.as_str()), + &e.expected.as_str(), + ) + })? + } else { + value + .ok_or_else(|| Error::missing_field("value"))? + .unwrap_or(ScalarValue::Null) + }; + OpType::mark(name, expand, value) + } + RawOpType::MarkEnd => { + let expand = expand.unwrap_or(true); + OpType::MarkEnd(expand) + } RawOpType::Set => { let value = if let Some(datatype) = datatype { let raw_value = value diff --git a/automerge/src/legacy/serde_impls/op_type.rs b/automerge/src/legacy/serde_impls/op_type.rs index b054bad7..c2711d0c 100644 --- a/automerge/src/legacy/serde_impls/op_type.rs +++ b/automerge/src/legacy/serde_impls/op_type.rs @@ -15,6 +15,8 @@ impl Serialize for OpType { OpType::Make(ObjType::Table) => RawOpType::MakeTable, OpType::Make(ObjType::List) => RawOpType::MakeList, OpType::Make(ObjType::Text) => RawOpType::MakeText, + OpType::MarkBegin(_) => RawOpType::MarkBegin, + OpType::MarkEnd(_) => RawOpType::MarkEnd, OpType::Delete => RawOpType::Del, OpType::Increment(_) => RawOpType::Inc, OpType::Put(_) => RawOpType::Set, diff --git a/automerge/src/query.rs b/automerge/src/query.rs index f09ed0c1..f92c4f3c 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -1,10 +1,14 @@ +use crate::exid::ExId; use crate::op_tree::{OpSetMetadata, OpTreeNode}; use crate::types::{Clock, Counter, Key, Op, OpId, OpType, ScalarValue}; use fxhash::FxBuildHasher; +use serde::Serialize; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +mod attribute; +mod attribute2; mod elem_id_pos; mod insert; mod keys; @@ -22,9 +26,13 @@ mod nth_at; mod opid; mod prop; mod prop_at; +mod raw_spans; mod seek_op; mod seek_op_with_patch; +mod spans; +pub(crate) use attribute::{Attribute, ChangeSet}; +pub(crate) use attribute2::{Attribute2, ChangeSet2}; pub(crate) use elem_id_pos::ElemIdPos; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; @@ -42,8 +50,20 @@ pub(crate) use nth_at::NthAt; pub(crate) use opid::OpIdSearch; pub(crate) use prop::Prop; pub(crate) use prop_at::PropAt; +pub(crate) use raw_spans::RawSpans; pub(crate) use seek_op::SeekOp; pub(crate) use seek_op_with_patch::SeekOpWithPatch; +pub(crate) use spans::{Span, Spans}; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct SpanInfo { + pub id: ExId, + pub start: usize, + pub end: usize, + #[serde(rename = "type")] + pub span_type: String, + pub value: ScalarValue, +} // use a struct for the args for clarity as they are passed up the update chain in the optree #[derive(Debug, Clone)] diff --git a/automerge/src/query/attribute.rs b/automerge/src/query/attribute.rs new file mode 100644 index 00000000..ca0b28f7 --- /dev/null +++ b/automerge/src/query/attribute.rs @@ -0,0 +1,129 @@ +use crate::clock::Clock; +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op}; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Attribute { + pos: usize, + seen: usize, + last_seen: Option, + baseline: Clock, + pub(crate) change_sets: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ChangeSet { + clock: Clock, + next_add: Option>, + next_del: Option<(usize, String)>, + pub add: Vec>, + pub del: Vec<(usize, String)>, +} + +impl From for ChangeSet { + fn from(clock: Clock) -> Self { + ChangeSet { + clock, + next_add: None, + next_del: None, + add: Vec::new(), + del: Vec::new(), + } + } +} + +impl ChangeSet { + fn cut_add(&mut self) { + if let Some(add) = self.next_add.take() { + self.add.push(add) + } + } + + fn cut_del(&mut self) { + if let Some(del) = self.next_del.take() { + self.del.push(del) + } + } +} + +impl Attribute { + pub(crate) fn new(baseline: Clock, change_sets: Vec) -> Self { + Attribute { + pos: 0, + seen: 0, + last_seen: None, + baseline, + change_sets: change_sets.into_iter().map(|c| c.into()).collect(), + } + } + + fn update_add(&mut self, element: &Op) { + let baseline = self.baseline.covers(&element.id); + for cs in &mut self.change_sets { + if !baseline && cs.clock.covers(&element.id) { + // is part of the change_set + if let Some(range) = &mut cs.next_add { + range.end += 1; + } else { + cs.next_add = Some(Range { + start: self.seen, + end: self.seen + 1, + }); + } + } else { + cs.cut_add(); + } + cs.cut_del(); + } + } + + // id is in baseline + // succ is not in baseline but is in cs + + fn update_del(&mut self, element: &Op) { + if !self.baseline.covers(&element.id) + || element.succ.iter().any(|id| self.baseline.covers(id)) + { + return; + } + for cs in &mut self.change_sets { + if element.succ.iter().any(|id| cs.clock.covers(id)) { + // was deleted by change set + if let Some(s) = element.as_string() { + if let Some((_, span)) = &mut cs.next_del { + span.push_str(&s); + } else { + cs.next_del = Some((self.seen, s)) + } + } + } + } + } + + pub(crate) fn finish(&mut self) { + for cs in &mut self.change_sets { + cs.cut_add(); + cs.cut_del(); + } + } +} + +impl<'a> TreeQuery<'a> for Attribute { + fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { + if element.insert { + self.last_seen = None; + } + if self.last_seen.is_none() && element.visible() { + self.update_add(element); + self.seen += 1; + self.last_seen = element.elemid(); + } + if !element.succ.is_empty() { + self.update_del(element); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/query/attribute2.rs b/automerge/src/query/attribute2.rs new file mode 100644 index 00000000..48ab9040 --- /dev/null +++ b/automerge/src/query/attribute2.rs @@ -0,0 +1,174 @@ +use crate::clock::Clock; +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op}; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Attribute2 { + pos: usize, + seen: usize, + last_seen: Option, + baseline: Clock, + pub(crate) change_sets: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ChangeSet2 { + clock: Clock, + next_add: Option, + next_del: Option, + pub add: Vec, + pub del: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CS2Add { + pub actor: usize, + pub range: Range, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CS2Del { + pub pos: usize, + pub actor: usize, + pub span: String, +} + +impl From for ChangeSet2 { + fn from(clock: Clock) -> Self { + ChangeSet2 { + clock, + next_add: None, + next_del: None, + add: Vec::new(), + del: Vec::new(), + } + } +} + +impl ChangeSet2 { + fn cut_add(&mut self) { + if let Some(add) = self.next_add.take() { + self.add.push(add) + } + } + + fn cut_del(&mut self) { + if let Some(del) = self.next_del.take() { + self.del.push(del) + } + } +} + +impl Attribute2 { + pub(crate) fn new(baseline: Clock, change_sets: Vec) -> Self { + Attribute2 { + pos: 0, + seen: 0, + last_seen: None, + baseline, + change_sets: change_sets.into_iter().map(|c| c.into()).collect(), + } + } + + fn update_add(&mut self, element: &Op) { + let baseline = self.baseline.covers(&element.id); + for cs in &mut self.change_sets { + if !baseline && cs.clock.covers(&element.id) { + // is part of the change_set + if let Some(CS2Add { range, actor }) = &mut cs.next_add { + if *actor == element.id.actor() { + range.end += 1; + } else { + cs.cut_add(); + cs.next_add = Some(CS2Add { + actor: element.id.actor(), + range: Range { + start: self.seen, + end: self.seen + 1, + }, + }); + } + } else { + cs.next_add = Some(CS2Add { + actor: element.id.actor(), + range: Range { + start: self.seen, + end: self.seen + 1, + }, + }); + } + } else { + cs.cut_add(); + } + cs.cut_del(); + } + } + + // id is in baseline + // succ is not in baseline but is in cs + + fn update_del(&mut self, element: &Op) { + if !self.baseline.covers(&element.id) + || element.succ.iter().any(|id| self.baseline.covers(id)) + { + return; + } + for cs in &mut self.change_sets { + let succ: Vec<_> = element + .succ + .iter() + .filter(|id| cs.clock.covers(id)) + .collect(); + // was deleted by change set + if let Some(suc) = succ.get(0) { + if let Some(s) = element.as_string() { + if let Some(CS2Del { actor, span, .. }) = &mut cs.next_del { + if suc.actor() == *actor { + span.push_str(&s); + } else { + cs.cut_del(); + cs.next_del = Some(CS2Del { + pos: self.seen, + actor: suc.actor(), + span: s, + }) + } + } else { + cs.next_del = Some(CS2Del { + pos: self.seen, + actor: suc.actor(), + span: s, + }) + } + } + } + } + } + + pub(crate) fn finish(&mut self) { + for cs in &mut self.change_sets { + cs.cut_add(); + cs.cut_del(); + } + } +} + +impl<'a> TreeQuery<'a> for Attribute2 { + fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { + if element.insert { + self.last_seen = None; + } + if self.last_seen.is_none() && element.visible() { + self.update_add(element); + self.seen += 1; + self.last_seen = element.elemid(); + } + if !element.succ.is_empty() { + self.update_del(element); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/query/insert.rs b/automerge/src/query/insert.rs index 9e495c49..ed59cf76 100644 --- a/automerge/src/query/insert.rs +++ b/automerge/src/query/insert.rs @@ -99,6 +99,10 @@ impl<'a> TreeQuery<'a> for InsertNth { self.last_seen = None; self.last_insert = element.elemid(); } + if self.valid.is_some() && element.valid_mark_anchor() { + self.last_valid_insert = Some(element.elemid_or_key()); + self.valid = None; + } if self.last_seen.is_none() && element.visible() { if self.seen >= self.target { return QueryResult::Finish; diff --git a/automerge/src/query/list_vals.rs b/automerge/src/query/list_vals.rs index 4ad2f47b..aee172e2 100644 --- a/automerge/src/query/list_vals.rs +++ b/automerge/src/query/list_vals.rs @@ -19,7 +19,7 @@ impl ListVals { } impl<'a> TreeQuery<'a> for ListVals { - fn query_node(&mut self, child: &OpTreeNode) -> QueryResult { + fn query_node(&mut self, child: &'a OpTreeNode) -> QueryResult { let start = 0; for pos in start..child.len() { let op = child.get(pos).unwrap(); diff --git a/automerge/src/query/raw_spans.rs b/automerge/src/query/raw_spans.rs new file mode 100644 index 00000000..e3b54f46 --- /dev/null +++ b/automerge/src/query/raw_spans.rs @@ -0,0 +1,78 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpId, OpType, ScalarValue}; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct RawSpans { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + changed: bool, + pub(crate) spans: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct RawSpan { + pub(crate) id: OpId, + pub(crate) start: usize, + pub(crate) end: usize, + pub(crate) name: String, + pub(crate) value: ScalarValue, +} + +impl RawSpans { + pub(crate) fn new() -> Self { + RawSpans { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + changed: false, + spans: Vec::new(), + } + } +} + +impl<'a> TreeQuery<'a> for RawSpans { + fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { + // find location to insert + // mark or set + if element.succ.is_empty() { + if let OpType::MarkBegin(md) = &element.action { + let pos = self + .spans + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.spans.insert( + pos, + RawSpan { + id: element.id, + start: self.seen, + end: 0, + name: md.name.clone(), + value: md.value.clone(), + }, + ); + } + if let OpType::MarkEnd(_) = &element.action { + for s in self.spans.iter_mut() { + if s.id == element.id.prev() { + s.end = self.seen; + break; + } + } + } + } + if element.insert { + self.last_seen = None; + self.last_insert = element.elemid(); + } + if self.last_seen.is_none() && element.visible() { + self.seen += 1; + self.last_seen = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/query/spans.rs b/automerge/src/query/spans.rs new file mode 100644 index 00000000..8459c0bc --- /dev/null +++ b/automerge/src/query/spans.rs @@ -0,0 +1,109 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpType, ScalarValue}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Spans<'a> { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + seen_at_this_mark: Option, + seen_at_last_mark: Option, + ops: Vec<&'a Op>, + marks: HashMap, + changed: bool, + pub(crate) spans: Vec>, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Span<'a> { + pub pos: usize, + pub marks: Vec<(String, Cow<'a, ScalarValue>)>, +} + +impl<'a> Spans<'a> { + pub(crate) fn new() -> Self { + Spans { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + seen_at_last_mark: None, + seen_at_this_mark: None, + changed: false, + ops: Vec::new(), + marks: HashMap::new(), + spans: Vec::new(), + } + } + + pub(crate) fn check_marks(&mut self) { + let mut new_marks = HashMap::new(); + for op in &self.ops { + if let OpType::MarkBegin(m) = &op.action { + new_marks.insert(m.name.clone(), &m.value); + } + } + if new_marks != self.marks { + self.changed = true; + self.marks = new_marks; + } + if self.changed + && (self.seen_at_last_mark != self.seen_at_this_mark + || self.seen_at_last_mark.is_none() && self.seen_at_this_mark.is_none()) + { + self.changed = false; + self.seen_at_last_mark = self.seen_at_this_mark; + let mut marks: Vec<_> = self + .marks + .iter() + .map(|(key, val)| (key.clone(), Cow::Borrowed(*val))) + .collect(); + marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); + self.spans.push(Span { + pos: self.seen, + marks, + }); + } + } +} + +impl<'a> TreeQuery<'a> for Spans<'a> { + /* + fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { + unimplemented!() + } + */ + + fn query_element_with_metadata(&mut self, element: &'a Op, m: &OpSetMetadata) -> QueryResult { + // find location to insert + // mark or set + if element.succ.is_empty() { + if let OpType::MarkBegin(_) = &element.action { + let pos = self + .ops + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.ops.insert(pos, element); + } + if let OpType::MarkEnd(_) = &element.action { + self.ops.retain(|op| op.id != element.id.prev()); + } + } + if element.insert { + self.last_seen = None; + self.last_insert = element.elemid(); + } + if self.last_seen.is_none() && element.visible() { + self.check_marks(); + self.seen += 1; + self.last_seen = element.elemid(); + self.seen_at_this_mark = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/sync.rs b/automerge/src/sync.rs index 2b4b454b..99e6fedd 100644 --- a/automerge/src/sync.rs +++ b/automerge/src/sync.rs @@ -1,3 +1,6 @@ +use crate::{ + decoding, decoding::Decoder, encoding::Encodable, Automerge, AutomergeError, Change, ChangeHash, +}; use itertools::Itertools; use std::{ borrow::Cow, @@ -6,10 +9,7 @@ use std::{ io::Write, }; -use crate::{ - decoding, decoding::Decoder, encoding::Encodable, types::HASH_SIZE, ApplyOptions, Automerge, - AutomergeError, Change, ChangeHash, OpObserver, -}; +use crate::{types::HASH_SIZE, ApplyOptions, OpObserver}; mod bloom; mod state; diff --git a/automerge/src/transaction/inner.rs b/automerge/src/transaction/inner.rs index 86936492..dbd67342 100644 --- a/automerge/src/transaction/inner.rs +++ b/automerge/src/transaction/inner.rs @@ -171,6 +171,73 @@ impl TransactionInner { self.operations.push((obj, prop, op)); } + #[allow(clippy::too_many_arguments)] + pub(crate) fn mark>( + &mut self, + doc: &mut Automerge, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = doc.exid_to_obj(obj.as_ref())?; + + self.do_insert( + doc, + obj, + start, + OpType::mark(mark.into(), expand_start, value), + )?; + self.do_insert(doc, obj, end, OpType::MarkEnd(expand_end))?; + + Ok(()) + } + + pub(crate) fn unmark>( + &mut self, + doc: &mut Automerge, + obj: O, + mark: O, + ) -> Result<(), AutomergeError> { + let obj = doc.exid_to_obj(obj.as_ref())?; + let markid = doc.exid_to_obj_tmp_unchecked(mark.as_ref())?.0; + let op1 = Op { + id: self.next_id(), + action: OpType::Delete, + key: markid.into(), + succ: Default::default(), + pred: doc.ops.m.sorted_opids(vec![markid].into_iter()), + insert: false, + }; + let q1 = doc.ops.search(&obj, query::SeekOp::new(&op1)); + doc.ops.add_succ(&obj, q1.succ.into_iter(), &op1); + //for i in q1.succ { + // doc.ops.replace(&obj, i, |old_op| old_op.add_succ(&op1)); + //} + self.operations.push((obj, Prop::Map("".into()), op1)); + + let markid = markid.next(); + let op2 = Op { + id: self.next_id(), + action: OpType::Delete, + key: markid.into(), + succ: Default::default(), + pred: doc.ops.m.sorted_opids(vec![markid].into_iter()), + insert: false, + }; + let q2 = doc.ops.search(&obj, query::SeekOp::new(&op2)); + + doc.ops.add_succ(&obj, q2.succ.into_iter(), &op2); + //for i in q2.succ { + // doc.ops.replace(&obj, i, |old_op| old_op.add_succ(&op2)); + //} + self.operations.push((obj, Prop::Map("".into()), op2)); + Ok(()) + } + pub(crate) fn insert>( &mut self, doc: &mut Automerge, diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index 58c5ca88..b7c3fc9b 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -1,12 +1,12 @@ use std::ops::RangeBounds; +use super::{CommitOptions, Transactable, TransactionInner}; use crate::exid::ExId; +use crate::query; use crate::{Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ScalarValue, Value, Values}; use crate::{AutomergeError, Keys}; use crate::{ListRange, ListRangeAt, MapRange, MapRangeAt}; -use super::{CommitOptions, Transactable, TransactionInner}; - /// A transaction on a document. /// Transactions group operations into a single change so that no other operations can happen /// in-between. @@ -129,6 +129,33 @@ impl<'a> Transactable for Transaction<'a> { .insert(self.doc, obj.as_ref(), index, value) } + #[allow(clippy::too_many_arguments)] + fn mark>( + &mut self, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + self.inner.as_mut().unwrap().mark( + self.doc, + obj, + start, + expand_start, + end, + expand_end, + mark, + value, + ) + } + + fn unmark>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError> { + self.inner.as_mut().unwrap().unmark(self.doc, obj, mark) + } + fn insert_object>( &mut self, obj: O, @@ -253,6 +280,32 @@ impl<'a> Transactable for Transaction<'a> { self.doc.text_at(obj, heads) } + fn spans>(&self, obj: O) -> Result>, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute(obj, baseline, change_sets) + } + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute2(obj, baseline, change_sets) + } + fn get, P: Into>( &self, obj: O, diff --git a/automerge/src/transaction/transactable.rs b/automerge/src/transaction/transactable.rs index 0c7f6c45..c6546c38 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -1,6 +1,7 @@ use std::ops::RangeBounds; use crate::exid::ExId; +use crate::query; use crate::{ AutomergeError, ChangeHash, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Parents, Prop, ScalarValue, Value, Values, @@ -61,6 +62,21 @@ pub trait Transactable { object: ObjType, ) -> Result; + /// Set a mark within a range on a list + #[allow(clippy::too_many_arguments)] + fn mark>( + &mut self, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError>; + + fn unmark>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError>; + /// Increment the counter at the prop in the object by `value`. fn increment, P: Into>( &mut self, @@ -151,6 +167,28 @@ pub trait Transactable { heads: &[ChangeHash], ) -> Result; + /// test spans api for mark/span experiment + fn spans>(&self, obj: O) -> Result>, AutomergeError>; + + /// test raw_spans api for mark/span experiment + fn raw_spans>(&self, obj: O) -> Result, AutomergeError>; + + /// test attribute api for mark/span experiment + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError>; + + /// test attribute api for mark/span experiment + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError>; + /// Get the value at this prop in the object. fn get, P: Into>( &self, diff --git a/automerge/src/types.rs b/automerge/src/types.rs index 288c2846..397aa4c1 100644 --- a/automerge/src/types.rs +++ b/automerge/src/types.rs @@ -182,9 +182,29 @@ impl fmt::Display for ObjType { #[derive(PartialEq, Debug, Clone)] pub enum OpType { Make(ObjType), + /// Perform a deletion, expanding the operation to cover `n` deletions (multiOp). Delete, Increment(i64), Put(ScalarValue), + MarkBegin(MarkData), + MarkEnd(bool), +} + +impl OpType { + pub(crate) fn mark(name: String, expand: bool, value: ScalarValue) -> Self { + OpType::MarkBegin(MarkData { + name, + expand, + value, + }) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct MarkData { + pub name: String, + pub value: ScalarValue, + pub expand: bool, } impl From for OpType { @@ -219,6 +239,14 @@ impl OpId { pub(crate) fn actor(&self) -> usize { self.1 } + #[inline] + pub(crate) fn prev(&self) -> OpId { + OpId(self.0 - 1, self.1) + } + #[inline] + pub(crate) fn next(&self) -> OpId { + OpId(self.0 + 1, self.1) + } } impl Exportable for ObjId { @@ -419,7 +447,7 @@ impl Op { } pub(crate) fn visible(&self) -> bool { - if self.is_inc() { + if self.is_inc() || self.is_mark() { false } else if self.is_counter() { self.succ.len() <= self.incs() @@ -444,6 +472,18 @@ impl Op { matches!(&self.action, OpType::Increment(_)) } + pub(crate) fn valid_mark_anchor(&self) -> bool { + self.succ.is_empty() + && matches!( + &self.action, + OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(false) + ) + } + + pub(crate) fn is_mark(&self) -> bool { + matches!(&self.action, OpType::MarkBegin(_) | OpType::MarkEnd(_)) + } + pub(crate) fn is_counter(&self) -> bool { matches!(&self.action, OpType::Put(ScalarValue::Counter(_))) } @@ -472,6 +512,13 @@ impl Op { } } + pub(crate) fn as_string(&self) -> Option { + match &self.action { + OpType::Put(scalar) => scalar.as_string(), + _ => None, + } + } + pub(crate) fn get_increment_value(&self) -> Option { if let OpType::Increment(i) = self.action { Some(i) @@ -484,6 +531,8 @@ impl Op { match &self.action { OpType::Make(obj_type) => Value::Object(*obj_type), OpType::Put(scalar) => Value::Scalar(Cow::Borrowed(scalar)), + OpType::MarkBegin(mark) => Value::Scalar(Cow::Owned(format!("markBegin[{}]={}",mark.name, mark.value).into())), + OpType::MarkEnd(_) => Value::Scalar(Cow::Owned("markEnd".into())), _ => panic!("cant convert op into a value - {:?}", self), } } @@ -492,6 +541,8 @@ impl Op { match &self.action { OpType::Make(obj_type) => Value::Object(*obj_type), OpType::Put(scalar) => Value::Scalar(Cow::Owned(scalar.clone())), + OpType::MarkBegin(mark) => Value::Scalar(Cow::Owned(format!("markBegin[{}]={}",mark.name, mark.value).into())), + OpType::MarkEnd(_) => Value::Scalar(Cow::Owned("markEnd".into())), _ => panic!("cant convert op into a value - {:?}", self), } } @@ -502,6 +553,8 @@ impl Op { OpType::Put(value) if self.insert => format!("i:{}", value), OpType::Put(value) => format!("s:{}", value), OpType::Make(obj) => format!("make{}", obj), + OpType::MarkBegin(m) => format!("mark{}={}", m.name, m.value), + OpType::MarkEnd(_) => "unmark".into(), OpType::Increment(val) => format!("inc:{}", val), OpType::Delete => "del".to_string(), } diff --git a/automerge/src/value.rs b/automerge/src/value.rs index 1df87ace..7464a92d 100644 --- a/automerge/src/value.rs +++ b/automerge/src/value.rs @@ -14,6 +14,13 @@ pub enum Value<'a> { } impl<'a> Value<'a> { + pub fn as_string(&self) -> Option { + match self { + Value::Scalar(val) => val.as_string(), + _ => None, + } + } + pub fn map() -> Value<'a> { Value::Object(ObjType::Map) } @@ -629,6 +636,13 @@ impl ScalarValue { } } + pub fn as_string(&self) -> Option { + match self { + ScalarValue::Str(s) => Some(s.to_string()), + _ => None, + } + } + pub fn counter(n: i64) -> ScalarValue { ScalarValue::Counter(n.into()) } diff --git a/automerge/src/visualisation.rs b/automerge/src/visualisation.rs index 5e6dae6f..b4410e4b 100644 --- a/automerge/src/visualisation.rs +++ b/automerge/src/visualisation.rs @@ -238,6 +238,8 @@ impl OpTableRow { crate::OpType::Put(v) => format!("set {}", v), crate::OpType::Make(obj) => format!("make {}", obj), crate::OpType::Increment(v) => format!("inc {}", v), + crate::OpType::MarkBegin(v) => format!("mark {}={}", v.name, v.value), + crate::OpType::MarkEnd(v) => format!("/mark {}", v), }; let prop = match op.key { crate::types::Key::Map(k) => metadata.props[k].clone(), diff --git a/automerge/tests/attribute.rs b/automerge/tests/attribute.rs new file mode 100644 index 00000000..9968b248 --- /dev/null +++ b/automerge/tests/attribute.rs @@ -0,0 +1,39 @@ +use automerge::transaction::Transactable; +use automerge::{AutoCommit, AutomergeError, ROOT}; + +/* +mod helpers; +use helpers::{ + pretty_print, realize, realize_obj, + RealizedObject, +}; +*/ + +#[test] +fn simple_attribute_text() -> Result<(), AutomergeError> { + let mut doc = AutoCommit::new(); + let note = doc.put_object(&ROOT, "note", automerge::ObjType::Text)?; + doc.splice_text(¬e, 0, 0, "hello little world")?; + let baseline = doc.get_heads(); + assert!(doc.text(¬e).unwrap() == "hello little world"); + let mut doc2 = doc.fork(); + doc2.splice_text(¬e, 5, 7, " big")?; + let h2 = doc2.get_heads(); + assert!(doc2.text(¬e)? == "hello big world"); + let mut doc3 = doc.fork(); + doc3.splice_text(¬e, 0, 0, "Well, ")?; + let h3 = doc3.get_heads(); + assert!(doc3.text(¬e)? == "Well, hello little world"); + doc.merge(&mut doc2)?; + doc.merge(&mut doc3)?; + let text = doc.text(¬e)?; + assert!(text == "Well, hello big world"); + let cs = vec![h2, h3]; + let attribute = doc.attribute(¬e, &baseline, &cs)?; + assert!(&text[attribute[0].add[0].clone()] == " big"); + assert!(attribute[0].del[0] == (15, " little".to_owned())); + //println!("{:?} == {:?}", attribute[0].del[0] , (15, " little".to_owned())); + assert!(&text[attribute[1].add[0].clone()] == "Well, "); + //println!("- ------- attribute = {:?}", attribute); + Ok(()) +} diff --git a/scripts/ci/js_tests b/scripts/ci/js_tests index 9b1d0e77..6c4a16d4 100755 --- a/scripts/ci/js_tests +++ b/scripts/ci/js_tests @@ -7,9 +7,9 @@ yarn --cwd $WASM_PROJECT install; yarn --cwd $WASM_PROJECT build; # If the dependencies are already installed we delete automerge-wasm. This makes # this script usable for iterative development. -if [ -d $JS_PROJECT/node_modules/automerge-wasm ]; then - rm -rf $JS_PROJECT/node_modules/automerge-wasm -fi +#if [ -d $JS_PROJECT/node_modules/automerge-wasm ]; then +# rm -rf $JS_PROJECT/node_modules/automerge-wasm +#fi # --check-files forces yarn to check if the local dep has changed yarn --cwd $JS_PROJECT install --check-files; yarn --cwd $JS_PROJECT test;