Compare commits
	
		
			6 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							 | 
						
							
							
								
							
							
	
	
	12308206ff | 
						
						
							||
| 
							 | 
						
							
							
								
							
							
	
	
	dbf438f7cb | 
						
						
							||
| 
							 | 
						846f7bb181 | ||
| 
							 | 
						8b7fa5d33f | ||
| 
							 | 
						
							
							
								
							
							
	
	
	4e17d798be | 
						
						
							||
| 
							 | 
						
							
							
								
							
							
	
	
	210e9648bf | 
						
						
							
					 40 changed files with 2442 additions and 1171 deletions
				
			
		| 
						 | 
				
			
			@ -170,7 +170,7 @@ pub unsafe extern "C" fn AMcommit(
 | 
			
		|||
    if let Some(time) = time.as_ref() {
 | 
			
		||||
        options.set_time(*time);
 | 
			
		||||
    }
 | 
			
		||||
    to_result(doc.commit_with::<()>(options))
 | 
			
		||||
    to_result(doc.commit_with(options))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// \memberof AMdoc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ packages:
 | 
			
		|||
  "automerge-wasm":
 | 
			
		||||
      access: "$all"
 | 
			
		||||
      publish: "$all"
 | 
			
		||||
  "automerge-js":
 | 
			
		||||
  "automerge":
 | 
			
		||||
      access: "$all"
 | 
			
		||||
      publish: "$all"
 | 
			
		||||
  "*":
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,7 @@
 | 
			
		|||
    "mocha": "^10.0.0",
 | 
			
		||||
    "pako": "^2.0.4",
 | 
			
		||||
    "ts-mocha": "^10.0.0",
 | 
			
		||||
    "ts-node": "^10.9.1",
 | 
			
		||||
    "typescript": "^4.6.4"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
// Properties of the document root object
 | 
			
		||||
//const OPTIONS   = Symbol('_options')   // object containing options passed to init()
 | 
			
		||||
//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 STATE      = Symbol.for('_am_state')     // object containing metadata about current state (e.g. sequence numbers)
 | 
			
		||||
export const STATE      = Symbol.for('_am_meta')     // 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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ 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"
 | 
			
		||||
import { AutomergeValue, Text, Counter } from "./types"
 | 
			
		||||
export { AutomergeValue, Text, Counter, Int, Uint, Float64 } from "./types"
 | 
			
		||||
 | 
			
		||||
import { API } from "automerge-wasm";
 | 
			
		||||
| 
						 | 
				
			
			@ -13,9 +13,10 @@ import { ApiHandler, UseApi } from "./low_level"
 | 
			
		|||
import { Actor as ActorId, Prop, ObjID, Change, DecodedChange, Heads, Automerge, MaterializeValue } from "automerge-wasm"
 | 
			
		||||
import { JsSyncState as SyncState, SyncMessage, DecodedSyncMessage } from "automerge-wasm"
 | 
			
		||||
 | 
			
		||||
export type ChangeOptions = { message?: string, time?: number }
 | 
			
		||||
export type ChangeOptions = { message?: string, time?: number, patchCallback?: Function }
 | 
			
		||||
export type ApplyOptions = { patchCallback?: Function }
 | 
			
		||||
 | 
			
		||||
export type Doc<T> = { readonly [P in keyof T]: Doc<T[P]> }
 | 
			
		||||
export type Doc<T> = { readonly [P in keyof T]: T[P] }
 | 
			
		||||
 | 
			
		||||
export type ChangeFn<T> = (doc: T) => void
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,21 +25,32 @@ export interface State<T> {
 | 
			
		|||
  snapshot: T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type InitOptions = {
 | 
			
		||||
    actor?: ActorId,
 | 
			
		||||
    freeze?: boolean,
 | 
			
		||||
    patchCallback?: Function,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function use(api: API) {
 | 
			
		||||
  UseApi(api)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
import * as wasm from "automerge-wasm"
 | 
			
		||||
use(wasm)
 | 
			
		||||
 | 
			
		||||
export function getBackend<T>(doc: Doc<T>) : Automerge {
 | 
			
		||||
  return _state(doc)
 | 
			
		||||
interface InternalState {
 | 
			
		||||
  handle: Automerge,
 | 
			
		||||
  heads: Heads | undefined,
 | 
			
		||||
  freeze: boolean,
 | 
			
		||||
  patchCallback: Function | undefined,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _state<T>(doc: Doc<T>) : Automerge {
 | 
			
		||||
export function getBackend<T>(doc: Doc<T>) : Automerge {
 | 
			
		||||
  return _state(doc).handle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _state<T>(doc: Doc<T>, checkroot = true) : InternalState {
 | 
			
		||||
  const state = Reflect.get(doc,STATE)
 | 
			
		||||
  if (state == undefined) {
 | 
			
		||||
  if (state === undefined || (checkroot && _obj(doc) !== "_root")) {
 | 
			
		||||
    throw new RangeError("must be the document root")
 | 
			
		||||
  }
 | 
			
		||||
  return state
 | 
			
		||||
| 
						 | 
				
			
			@ -48,17 +60,12 @@ 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)
 | 
			
		||||
  _state(doc).heads = heads
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _clear_heads<T>(doc: Doc<T>) {
 | 
			
		||||
| 
						 | 
				
			
			@ -67,28 +74,55 @@ function _clear_heads<T>(doc: Doc<T>) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function _obj<T>(doc: Doc<T>) : ObjID {
 | 
			
		||||
  return Reflect.get(doc,OBJECT_ID)
 | 
			
		||||
  let proxy_objid = Reflect.get(doc,OBJECT_ID)
 | 
			
		||||
  if (proxy_objid) {
 | 
			
		||||
    return proxy_objid
 | 
			
		||||
  }
 | 
			
		||||
  if (Reflect.get(doc,STATE)) {
 | 
			
		||||
    return "_root"
 | 
			
		||||
  }
 | 
			
		||||
  throw new RangeError("invalid document passed to _obj()")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _readonly<T>(doc: Doc<T>) : boolean {
 | 
			
		||||
  return Reflect.get(doc,READ_ONLY) === true
 | 
			
		||||
  return Reflect.get(doc,READ_ONLY) !== false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function init<T>(actor?: ActorId) : Doc<T>{
 | 
			
		||||
  if (typeof actor !== "string") {
 | 
			
		||||
    actor = undefined
 | 
			
		||||
function importOpts(_actor?: ActorId | InitOptions) : InitOptions {
 | 
			
		||||
  if (typeof _actor === 'object') {
 | 
			
		||||
    return _actor
 | 
			
		||||
  } else {
 | 
			
		||||
    return { actor: _actor }
 | 
			
		||||
  }
 | 
			
		||||
  const state = ApiHandler.create(actor)
 | 
			
		||||
  return rootProxy(state, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function init<T>(_opts?: ActorId | InitOptions) : Doc<T>{
 | 
			
		||||
  let opts = importOpts(_opts)
 | 
			
		||||
  let freeze = !!opts.freeze
 | 
			
		||||
  let patchCallback = opts.patchCallback
 | 
			
		||||
  const handle = ApiHandler.create(opts.actor)
 | 
			
		||||
  handle.enablePatches(true)
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  handle.registerDatatype("counter", (n) => new Counter(n))
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  handle.registerDatatype("text", (n) => new Text(n))
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  const doc = handle.materialize("/", undefined, { handle, heads: undefined, freeze, patchCallback })
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  return doc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function clone<T>(doc: Doc<T>) : Doc<T> {
 | 
			
		||||
  const state = _state(doc).clone()
 | 
			
		||||
  return rootProxy(state, true);
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  const handle = state.heads ? state.handle.forkAt(state.heads) : state.handle.fork()
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  const clonedDoc : any = handle.materialize("/", undefined, { ... state, handle })
 | 
			
		||||
 | 
			
		||||
  return clonedDoc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function free<T>(doc: Doc<T>) {
 | 
			
		||||
  return _state(doc).free()
 | 
			
		||||
  return _state(doc).handle.free()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function from<T extends Record<string, unknown>>(initialState: T | Doc<T>, actor?: ActorId): Doc<T> {
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +142,16 @@ export function change<T>(doc: Doc<T>, options: string | ChangeOptions | ChangeF
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function progressDocument<T>(doc: Doc<T>, heads: Heads, callback?: Function): Doc<T> {
 | 
			
		||||
  let state = _state(doc)
 | 
			
		||||
  let nextState = { ... state, heads: undefined };
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  let nextDoc = state.handle.applyPatches(doc, nextState, callback)
 | 
			
		||||
  state.heads = heads
 | 
			
		||||
  if (nextState.freeze) { Object.freeze(nextDoc) }
 | 
			
		||||
  return nextDoc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function _change<T>(doc: Doc<T>, options: ChangeOptions, callback: ChangeFn<T>): Doc<T> {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -115,38 +159,33 @@ function _change<T>(doc: Doc<T>, options: ChangeOptions, callback: ChangeFn<T>):
 | 
			
		|||
    throw new RangeError("invalid change function");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (doc === undefined || _state(doc) === undefined || _obj(doc) !== "_root") {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
 | 
			
		||||
  if (doc === undefined || state === undefined) {
 | 
			
		||||
    throw new RangeError("must be the document root");
 | 
			
		||||
  }
 | 
			
		||||
  if (_frozen(doc) === true) {
 | 
			
		||||
  if (state.heads) {
 | 
			
		||||
    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()
 | 
			
		||||
  const heads = state.handle.getHeads()
 | 
			
		||||
  try {
 | 
			
		||||
    _set_heads(doc,heads)
 | 
			
		||||
    Reflect.set(doc,FROZEN,true)
 | 
			
		||||
    const root : T = rootProxy(state);
 | 
			
		||||
    state.heads = heads
 | 
			
		||||
    const root : T = rootProxy(state.handle);
 | 
			
		||||
    callback(root)
 | 
			
		||||
    if (state.pendingOps() === 0) {
 | 
			
		||||
      Reflect.set(doc,FROZEN,false)
 | 
			
		||||
      _clear_heads(doc)
 | 
			
		||||
    if (state.handle.pendingOps() === 0) {
 | 
			
		||||
      state.heads = undefined
 | 
			
		||||
      return doc
 | 
			
		||||
    } else {
 | 
			
		||||
      state.commit(options.message, options.time)
 | 
			
		||||
      return rootProxy(state, true);
 | 
			
		||||
      state.handle.commit(options.message, options.time)
 | 
			
		||||
      return progressDocument(doc, heads, options.patchCallback || state.patchCallback);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    //console.log("ERROR: ",e)
 | 
			
		||||
    Reflect.set(doc,FROZEN,false)
 | 
			
		||||
    _clear_heads(doc)
 | 
			
		||||
    state.rollback()
 | 
			
		||||
    state.heads = undefined
 | 
			
		||||
    state.handle.rollback()
 | 
			
		||||
    throw e
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -159,47 +198,55 @@ export function emptyChange<T>(doc: Doc<T>, options: ChangeOptions) {
 | 
			
		|||
    options = { message: options }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (doc === undefined || _state(doc) === undefined || _obj(doc) !== "_root") {
 | 
			
		||||
    throw new RangeError("must be the document root");
 | 
			
		||||
  }
 | 
			
		||||
  if (_frozen(doc) === true) {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
 | 
			
		||||
  if (state.heads) {
 | 
			
		||||
    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);
 | 
			
		||||
  const heads = state.handle.getHeads()
 | 
			
		||||
  state.handle.commit(options.message, options.time)
 | 
			
		||||
  return progressDocument(doc, heads)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function load<T>(data: Uint8Array, actor?: ActorId) : Doc<T> {
 | 
			
		||||
  const state = ApiHandler.load(data, actor)
 | 
			
		||||
  return rootProxy(state, true);
 | 
			
		||||
export function load<T>(data: Uint8Array, _opts?: ActorId | InitOptions) : Doc<T> {
 | 
			
		||||
  const opts = importOpts(_opts)
 | 
			
		||||
  const actor = opts.actor
 | 
			
		||||
  const patchCallback = opts.patchCallback
 | 
			
		||||
  const handle = ApiHandler.load(data, actor)
 | 
			
		||||
  handle.enablePatches(true)
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  handle.registerDatatype("counter", (n) => new Counter(n))
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  handle.registerDatatype("text", (n) => new Text(n))
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  const doc : any = handle.materialize("/", undefined, { handle, heads: undefined, patchCallback })
 | 
			
		||||
  return doc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function save<T>(doc: Doc<T>) : Uint8Array  {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  return state.save()
 | 
			
		||||
  return _state(doc).handle.save()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function merge<T>(local: Doc<T>, remote: Doc<T>) : Doc<T> {
 | 
			
		||||
  if (!!_heads(local) === true) {
 | 
			
		||||
  const localState = _state(local)
 | 
			
		||||
 | 
			
		||||
  if (localState.heads) {
 | 
			
		||||
    throw new RangeError("Attempting to change an out of date document - set at: " + _trace(local));
 | 
			
		||||
  }
 | 
			
		||||
  const localState = _state(local)
 | 
			
		||||
  const heads = localState.getHeads()
 | 
			
		||||
  const heads = localState.handle.getHeads()
 | 
			
		||||
  const remoteState = _state(remote)
 | 
			
		||||
  const changes = localState.getChangesAdded(remoteState)
 | 
			
		||||
  localState.applyChanges(changes)
 | 
			
		||||
  _set_heads(local,heads)
 | 
			
		||||
  return rootProxy(localState, true)
 | 
			
		||||
  const changes = localState.handle.getChangesAdded(remoteState.handle)
 | 
			
		||||
  localState.handle.applyChanges(changes)
 | 
			
		||||
  return progressDocument(local, heads, localState.patchCallback)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getActorId<T>(doc: Doc<T>) : ActorId {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  return state.getActorId()
 | 
			
		||||
  return state.handle.getActorId()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Conflicts = { [key: string]: AutomergeValue }
 | 
			
		||||
| 
						 | 
				
			
			@ -246,14 +293,14 @@ function conflictAt(context : Automerge, objectId: ObjID, prop: Prop) : Conflict
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export function getConflicts<T>(doc: Doc<T>, prop: Prop) : Conflicts | undefined {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  const state = _state(doc, false)
 | 
			
		||||
  const objectId = _obj(doc)
 | 
			
		||||
  return conflictAt(state, objectId, prop)
 | 
			
		||||
  return conflictAt(state.handle, objectId, prop)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getLastLocalChange<T>(doc: Doc<T>) : Change | undefined {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  return state.getLastLocalChange() || undefined
 | 
			
		||||
  return state.handle.getLastLocalChange() || undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getObjectId<T>(doc: Doc<T>) : ObjID {
 | 
			
		||||
| 
						 | 
				
			
			@ -263,30 +310,27 @@ export function getObjectId<T>(doc: Doc<T>) : ObjID {
 | 
			
		|||
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())
 | 
			
		||||
  return n.handle.getChanges(getHeads(oldState))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAllChanges<T>(doc: Doc<T>) : Change[] {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  return state.getChanges([])
 | 
			
		||||
  return state.handle.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) {
 | 
			
		||||
export function applyChanges<T>(doc: Doc<T>, changes: Change[], opts?: ApplyOptions) : [Doc<T>] {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  if (!opts) { opts = {} }
 | 
			
		||||
  if (state.heads) {
 | 
			
		||||
    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)];
 | 
			
		||||
  const heads = state.handle.getHeads();
 | 
			
		||||
  state.handle.applyChanges(changes)
 | 
			
		||||
  state.heads = heads;
 | 
			
		||||
  return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback )]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getHistory<T>(doc: Doc<T>) : State<T>[] {
 | 
			
		||||
| 
						 | 
				
			
			@ -304,6 +348,7 @@ export function getHistory<T>(doc: Doc<T>) : State<T>[] {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// FIXME : no tests
 | 
			
		||||
// FIXME can we just use deep equals now?
 | 
			
		||||
export function equals(val1: unknown, val2: unknown) : boolean {
 | 
			
		||||
  if (!isObject(val1) || !isObject(val2)) return val1 === val2
 | 
			
		||||
  const keys1 = Object.keys(val1).sort(), keys2 = Object.keys(val2).sort()
 | 
			
		||||
| 
						 | 
				
			
			@ -326,31 +371,25 @@ export function decodeSyncState(state: Uint8Array) : SyncState {
 | 
			
		|||
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 message = state.handle.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 ] {
 | 
			
		||||
export function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message: SyncMessage, opts?: ApplyOptions) : [ 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) {
 | 
			
		||||
  if (!opts) { opts = {} }
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  if (state.heads) {
 | 
			
		||||
    throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc));
 | 
			
		||||
  }
 | 
			
		||||
  if (_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];
 | 
			
		||||
  const heads = state.handle.getHeads()
 | 
			
		||||
  state.handle.receiveSyncMessage(syncState, message)
 | 
			
		||||
  const outSyncState = ApiHandler.exportSyncState(syncState)
 | 
			
		||||
  return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback), outSyncState, null];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function initSyncState() : SyncState {
 | 
			
		||||
| 
						 | 
				
			
			@ -375,24 +414,24 @@ export function decodeSyncMessage(message: SyncMessage) : DecodedSyncMessage {
 | 
			
		|||
 | 
			
		||||
export function getMissingDeps<T>(doc: Doc<T>, heads: Heads) : Heads {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  return state.getMissingDeps(heads)
 | 
			
		||||
  return state.handle.getMissingDeps(heads)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getHeads<T>(doc: Doc<T>) : Heads {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  return _heads(doc) || state.getHeads()
 | 
			
		||||
  return state.heads || state.handle.getHeads()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function dump<T>(doc: Doc<T>) {
 | 
			
		||||
  const state = _state(doc)
 | 
			
		||||
  state.dump()
 | 
			
		||||
  state.handle.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)
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  return state.handle.materialize("_root", state.heads, state)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -218,18 +218,6 @@ const ListHandler = {
 | 
			
		|||
    if (index === TRACE) return target.trace
 | 
			
		||||
    if (index === STATE) return context;
 | 
			
		||||
    if (index === 'length') return context.length(objectId, heads);
 | 
			
		||||
    if (index === Symbol.iterator) {
 | 
			
		||||
      let i = 0;
 | 
			
		||||
      return function *() {
 | 
			
		||||
        // FIXME - ugly
 | 
			
		||||
        let value = valueAt(target, i)
 | 
			
		||||
        while (value !== undefined) {
 | 
			
		||||
            yield value
 | 
			
		||||
            i += 1
 | 
			
		||||
            value = valueAt(target, i)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (typeof index === 'number') {
 | 
			
		||||
      return valueAt(target, index)
 | 
			
		||||
    } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -368,17 +356,6 @@ const TextHandler = Object.assign({}, ListHandler, {
 | 
			
		|||
    if (index === TRACE) return target.trace
 | 
			
		||||
    if (index === STATE) return context;
 | 
			
		||||
    if (index === 'length') return context.length(objectId, heads);
 | 
			
		||||
    if (index === Symbol.iterator) {
 | 
			
		||||
      let i = 0;
 | 
			
		||||
      return function *() {
 | 
			
		||||
        let value = valueAt(target, i)
 | 
			
		||||
        while (value !== undefined) {
 | 
			
		||||
            yield value
 | 
			
		||||
            i += 1
 | 
			
		||||
            value = valueAt(target, i)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (typeof index === 'number') {
 | 
			
		||||
      return valueAt(target, index)
 | 
			
		||||
    } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -424,11 +401,11 @@ function listMethods(target) {
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    fill(val: ScalarValue, start: number, end: number) {
 | 
			
		||||
      // FIXME needs tests
 | 
			
		||||
      const [value, datatype] = import_value(val)
 | 
			
		||||
      const length = context.length(objectId)
 | 
			
		||||
      start = parseListIndex(start || 0)
 | 
			
		||||
      end = parseListIndex(end || context.length(objectId))
 | 
			
		||||
      for (let i = start; i < end; i++) {
 | 
			
		||||
      end = parseListIndex(end || length)
 | 
			
		||||
      for (let i = start; i < Math.min(end, length); i++) {
 | 
			
		||||
        context.put(objectId, i, value, datatype)
 | 
			
		||||
      }
 | 
			
		||||
      return this
 | 
			
		||||
| 
						 | 
				
			
			@ -572,15 +549,9 @@ function listMethods(target) {
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return iterator
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
  // Read-only methods that can delegate to the JavaScript built-in implementations
 | 
			
		||||
  // FIXME - super slow
 | 
			
		||||
  for (const method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',
 | 
			
		||||
                      'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',
 | 
			
		||||
                      'slice', 'some', 'toLocaleString', 'toString']) {
 | 
			
		||||
    methods[method] = (...args) => {
 | 
			
		||||
    toArray() : AutomergeValue[] {
 | 
			
		||||
      const list : AutomergeValue = []
 | 
			
		||||
      let value
 | 
			
		||||
      do {
 | 
			
		||||
| 
						 | 
				
			
			@ -590,10 +561,107 @@ function listMethods(target) {
 | 
			
		|||
        }
 | 
			
		||||
      } while (value !== undefined)
 | 
			
		||||
 | 
			
		||||
      return list[method](...args)
 | 
			
		||||
      return list
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    map<T>(f: (AutomergeValue, number) => T) : T[] {
 | 
			
		||||
      return this.toArray().map(f)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    toString() : string {
 | 
			
		||||
      return this.toArray().toString()
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    toLocaleString() : string {
 | 
			
		||||
      return this.toArray().toLocaleString()
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    forEach(f: (AutomergeValue, number) => undefined ) {
 | 
			
		||||
      return this.toArray().forEach(f)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // todo: real concat function is different
 | 
			
		||||
    concat(other: AutomergeValue[]) : AutomergeValue[] {
 | 
			
		||||
      return this.toArray().concat(other)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    every(f: (AutomergeValue, number) => boolean) : boolean {
 | 
			
		||||
      return this.toArray().every(f)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    filter(f: (AutomergeValue, number) => boolean) : AutomergeValue[] {
 | 
			
		||||
      return this.toArray().filter(f)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    find(f: (AutomergeValue, number) => boolean) : AutomergeValue | undefined {
 | 
			
		||||
      let index = 0
 | 
			
		||||
      for (let v of this) {
 | 
			
		||||
        if (f(v, index)) {
 | 
			
		||||
          return v
 | 
			
		||||
        }
 | 
			
		||||
        index += 1
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    findIndex(f: (AutomergeValue, number) => boolean) : number {
 | 
			
		||||
      let index = 0
 | 
			
		||||
      for (let v of this) {
 | 
			
		||||
        if (f(v, index)) {
 | 
			
		||||
          return index
 | 
			
		||||
        }
 | 
			
		||||
        index += 1
 | 
			
		||||
      }
 | 
			
		||||
      return -1
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    includes(elem: AutomergeValue) : boolean {
 | 
			
		||||
      return this.find((e) => e === elem) !== undefined
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    join(sep?: string) : string {
 | 
			
		||||
      return this.toArray().join(sep)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // todo: remove the any
 | 
			
		||||
    reduce<T>(f: (any, AutomergeValue) => T, initalValue?: T) : T | undefined {
 | 
			
		||||
      return this.toArray().reduce(f,initalValue)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // todo: remove the any
 | 
			
		||||
    reduceRight<T>(f: (any, AutomergeValue) => T, initalValue?: T) : T | undefined{
 | 
			
		||||
      return this.toArray().reduceRight(f,initalValue)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    lastIndexOf(search: AutomergeValue, fromIndex = +Infinity) : number {
 | 
			
		||||
      // this can be faster
 | 
			
		||||
      return this.toArray().lastIndexOf(search,fromIndex)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    slice(index?: number, num?: number) : AutomergeValue[] {
 | 
			
		||||
      return this.toArray().slice(index,num)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    some(f: (AutomergeValue, number) => boolean) : boolean {
 | 
			
		||||
      let index = 0;
 | 
			
		||||
      for (let v of this) {
 | 
			
		||||
        if (f(v,index)) {
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
        index += 1
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    [Symbol.iterator]: function *() {
 | 
			
		||||
      let i = 0;
 | 
			
		||||
      let value = valueAt(target, i)
 | 
			
		||||
      while (value !== undefined) {
 | 
			
		||||
          yield value
 | 
			
		||||
          i += 1
 | 
			
		||||
          value = valueAt(target, i)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return methods
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,12 @@
 | 
			
		|||
import { Value } from "automerge-wasm"
 | 
			
		||||
import { TEXT } from "./constants"
 | 
			
		||||
import { TEXT, STATE } from "./constants"
 | 
			
		||||
 | 
			
		||||
export class Text {
 | 
			
		||||
  elems: Value[]
 | 
			
		||||
  str: string | undefined
 | 
			
		||||
  spans: Value[] | undefined
 | 
			
		||||
 | 
			
		||||
  constructor (text?: string | string[]) {
 | 
			
		||||
    //const instance = Object.create(Text.prototype)
 | 
			
		||||
  constructor (text?: string | string[] | Value[]) {
 | 
			
		||||
    if (typeof text === 'string') {
 | 
			
		||||
      this.elems = [...text]
 | 
			
		||||
    } else if (Array.isArray(text)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -50,14 +51,17 @@ export class Text {
 | 
			
		|||
   * non-character elements.
 | 
			
		||||
   */
 | 
			
		||||
  toString() : string {
 | 
			
		||||
    // Concatting to a string is faster than creating an array and then
 | 
			
		||||
    // .join()ing for small (<100KB) arrays.
 | 
			
		||||
    // https://jsperf.com/join-vs-loop-w-type-test
 | 
			
		||||
    let str = ''
 | 
			
		||||
    for (const elem of this.elems) {
 | 
			
		||||
      if (typeof elem === 'string') str += elem
 | 
			
		||||
    if (!this.str) {
 | 
			
		||||
      // Concatting to a string is faster than creating an array and then
 | 
			
		||||
      // .join()ing for small (<100KB) arrays.
 | 
			
		||||
      // https://jsperf.com/join-vs-loop-w-type-test
 | 
			
		||||
      this.str = ''
 | 
			
		||||
      for (const elem of this.elems) {
 | 
			
		||||
        if (typeof elem === 'string') this.str += elem
 | 
			
		||||
        else this.str += '\uFFFC'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return str
 | 
			
		||||
    return this.str
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -68,23 +72,25 @@ export class Text {
 | 
			
		|||
   * => ['ab', {x: 3}, 'cd']
 | 
			
		||||
   */
 | 
			
		||||
  toSpans() : Value[] {
 | 
			
		||||
    const spans : Value[] = []
 | 
			
		||||
    let chars = ''
 | 
			
		||||
    for (const elem of this.elems) {
 | 
			
		||||
      if (typeof elem === 'string') {
 | 
			
		||||
        chars += elem
 | 
			
		||||
      } else {
 | 
			
		||||
        if (chars.length > 0) {
 | 
			
		||||
          spans.push(chars)
 | 
			
		||||
          chars = ''
 | 
			
		||||
    if (!this.spans) {
 | 
			
		||||
      this.spans = []
 | 
			
		||||
      let chars = ''
 | 
			
		||||
      for (const elem of this.elems) {
 | 
			
		||||
        if (typeof elem === 'string') {
 | 
			
		||||
          chars += elem
 | 
			
		||||
        } else {
 | 
			
		||||
          if (chars.length > 0) {
 | 
			
		||||
            this.spans.push(chars)
 | 
			
		||||
            chars = ''
 | 
			
		||||
          }
 | 
			
		||||
          this.spans.push(elem)
 | 
			
		||||
        }
 | 
			
		||||
        spans.push(elem)
 | 
			
		||||
      }
 | 
			
		||||
      if (chars.length > 0) {
 | 
			
		||||
        this.spans.push(chars)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (chars.length > 0) {
 | 
			
		||||
      spans.push(chars)
 | 
			
		||||
    }
 | 
			
		||||
    return spans
 | 
			
		||||
    return this.spans
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -99,6 +105,9 @@ export class Text {
 | 
			
		|||
   * Updates the list item at position `index` to a new value `value`.
 | 
			
		||||
   */
 | 
			
		||||
  set (index: number, value: Value) {
 | 
			
		||||
    if (this[STATE]) {
 | 
			
		||||
      throw new RangeError("object cannot be modified outside of a change block")
 | 
			
		||||
    }
 | 
			
		||||
    this.elems[index] = value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -106,6 +115,9 @@ export class Text {
 | 
			
		|||
   * Inserts new list items `values` starting at position `index`.
 | 
			
		||||
   */
 | 
			
		||||
  insertAt(index: number, ...values: Value[]) {
 | 
			
		||||
    if (this[STATE]) {
 | 
			
		||||
      throw new RangeError("object cannot be modified outside of a change block")
 | 
			
		||||
    }
 | 
			
		||||
    this.elems.splice(index, 0, ... values)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -114,6 +126,9 @@ export class Text {
 | 
			
		|||
   * if `numDelete` is not given, one item is deleted.
 | 
			
		||||
   */
 | 
			
		||||
  deleteAt(index: number, numDelete = 1) {
 | 
			
		||||
    if (this[STATE]) {
 | 
			
		||||
      throw new RangeError("object cannot be modified outside of a change block")
 | 
			
		||||
    }
 | 
			
		||||
    this.elems.splice(index, numDelete)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -121,16 +136,64 @@ export class Text {
 | 
			
		|||
    this.elems.map(callback)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  lastIndexOf(searchElement: Value, fromIndex?: number) {
 | 
			
		||||
    this.elems.lastIndexOf(searchElement, fromIndex)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
  concat(other: Text) : Text {
 | 
			
		||||
    return new Text(this.elems.concat(other.elems))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
// Read-only methods that can delegate to the JavaScript built-in array
 | 
			
		||||
for (const method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',
 | 
			
		||||
                    'indexOf', 'join', 'lastIndexOf', 'reduce', 'reduceRight',
 | 
			
		||||
                    'slice', 'some', 'toLocaleString']) {
 | 
			
		||||
  Text.prototype[method] = function (...args) {
 | 
			
		||||
    const array = [...this]
 | 
			
		||||
    return array[method](...args)
 | 
			
		||||
  every(test: (Value) => boolean) : boolean {
 | 
			
		||||
    return this.elems.every(test)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  filter(test: (Value) => boolean) : Text {
 | 
			
		||||
    return new Text(this.elems.filter(test))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  find(test: (Value) => boolean) : Value | undefined {
 | 
			
		||||
    return this.elems.find(test)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  findIndex(test: (Value) => boolean) : number | undefined {
 | 
			
		||||
    return this.elems.findIndex(test)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  forEach(f: (Value) => undefined) {
 | 
			
		||||
    this.elems.forEach(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  includes(elem: Value) : boolean {
 | 
			
		||||
    return this.elems.includes(elem)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  indexOf(elem: Value) {
 | 
			
		||||
    return this.elems.indexOf(elem)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  join(sep?: string) : string{
 | 
			
		||||
    return this.elems.join(sep)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reduce(f: (previousValue: Value, currentValue: Value, currentIndex: number, array: Value[]) => Value)  {
 | 
			
		||||
    this.elems.reduce(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reduceRight(f: (previousValue: Value, currentValue: Value, currentIndex: number, array: Value[]) => Value) {
 | 
			
		||||
    this.elems.reduceRight(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  slice(start?: number, end?: number) {
 | 
			
		||||
    new Text(this.elems.slice(start,end))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  some(test: (Value) => boolean) : boolean {
 | 
			
		||||
    return this.elems.some(test)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toLocaleString() {
 | 
			
		||||
    this.toString()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -170,6 +170,55 @@ describe('Automerge', () => {
 | 
			
		|||
          console.log(doc.text.indexOf("world"))
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('proxy lists', () => {
 | 
			
		||||
        it('behave like arrays', () => {
 | 
			
		||||
          let doc = Automerge.from({
 | 
			
		||||
            chars: ["a","b","c"],
 | 
			
		||||
            numbers: [20,3,100],
 | 
			
		||||
            repeats: [20,20,3,3,3,3,100,100]
 | 
			
		||||
          })
 | 
			
		||||
          let r1 = []
 | 
			
		||||
          doc = Automerge.change(doc, (d) => {
 | 
			
		||||
            assert.deepEqual(d.chars.concat([1,2]), ["a","b","c",1,2])
 | 
			
		||||
            assert.deepEqual(d.chars.map((n) => n + "!"), ["a!", "b!", "c!"])
 | 
			
		||||
            assert.deepEqual(d.numbers.map((n) => n + 10), [30, 13, 110])
 | 
			
		||||
            assert.deepEqual(d.numbers.toString(), "20,3,100")
 | 
			
		||||
            assert.deepEqual(d.numbers.toLocaleString(), "20,3,100")
 | 
			
		||||
            assert.deepEqual(d.numbers.forEach((n) => r1.push(n)), undefined)
 | 
			
		||||
            assert.deepEqual(d.numbers.every((n) => n > 1), true)
 | 
			
		||||
            assert.deepEqual(d.numbers.every((n) => n > 10), false)
 | 
			
		||||
            assert.deepEqual(d.numbers.filter((n) => n > 10), [20,100])
 | 
			
		||||
            assert.deepEqual(d.repeats.find((n) => n < 10), 3)
 | 
			
		||||
            assert.deepEqual(d.repeats.toArray().find((n) => n < 10), 3)
 | 
			
		||||
            assert.deepEqual(d.repeats.find((n) => n < 0), undefined)
 | 
			
		||||
            assert.deepEqual(d.repeats.findIndex((n) => n < 10), 2)
 | 
			
		||||
            assert.deepEqual(d.repeats.findIndex((n) => n < 0), -1)
 | 
			
		||||
            assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 10), 2)
 | 
			
		||||
            assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 0), -1)
 | 
			
		||||
            assert.deepEqual(d.numbers.includes(3), true)
 | 
			
		||||
            assert.deepEqual(d.numbers.includes(-3), false)
 | 
			
		||||
            assert.deepEqual(d.numbers.join("|"), "20|3|100")
 | 
			
		||||
            assert.deepEqual(d.numbers.join(), "20,3,100")
 | 
			
		||||
            assert.deepEqual(d.numbers.some((f) => f === 3), true)
 | 
			
		||||
            assert.deepEqual(d.numbers.some((f) => f < 0), false)
 | 
			
		||||
            assert.deepEqual(d.numbers.reduce((sum,n) => sum + n, 100), 223)
 | 
			
		||||
            assert.deepEqual(d.repeats.reduce((sum,n) => sum + n, 100), 352)
 | 
			
		||||
            assert.deepEqual(d.chars.reduce((sum,n) => sum + n, "="), "=abc")
 | 
			
		||||
            assert.deepEqual(d.chars.reduceRight((sum,n) => sum + n, "="), "=cba")
 | 
			
		||||
            assert.deepEqual(d.numbers.reduceRight((sum,n) => sum + n, 100), 223)
 | 
			
		||||
            assert.deepEqual(d.repeats.lastIndexOf(3), 5)
 | 
			
		||||
            assert.deepEqual(d.repeats.lastIndexOf(3,3), 3)
 | 
			
		||||
          })
 | 
			
		||||
          doc = Automerge.change(doc, (d) => {
 | 
			
		||||
            assert.deepEqual(d.numbers.fill(-1,1,2), [20,-1,100])
 | 
			
		||||
            assert.deepEqual(d.chars.fill("z",1,100), ["a","z","z"])
 | 
			
		||||
          })
 | 
			
		||||
          assert.deepEqual(r1, [20,3,100])
 | 
			
		||||
          assert.deepEqual(doc.numbers, [20,-1,100])
 | 
			
		||||
          assert.deepEqual(doc.chars, ["a","z","z"])
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
    
 | 
			
		||||
    it('should obtain the same conflicts, regardless of merge order', () => {
 | 
			
		||||
      let s1 = Automerge.init()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -280,47 +280,34 @@ describe('Automerge', () => {
 | 
			
		|||
        assert.strictEqual(s2.list[0].getTime(), now.getTime())
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      /*
 | 
			
		||||
      it.skip('should call patchCallback if supplied', () => {
 | 
			
		||||
      it('should call patchCallback if supplied', () => {
 | 
			
		||||
        const callbacks = [], actor = Automerge.getActorId(s1)
 | 
			
		||||
        const s2 = Automerge.change(s1, {
 | 
			
		||||
          patchCallback: (patch, before, after, local) => callbacks.push({patch, before, after, local})
 | 
			
		||||
          patchCallback: (patch, before, after) => callbacks.push({patch, before, after})
 | 
			
		||||
        }, doc => {
 | 
			
		||||
          doc.birds = ['Goldfinch']
 | 
			
		||||
        })
 | 
			
		||||
        assert.strictEqual(callbacks.length, 1)
 | 
			
		||||
        assert.deepStrictEqual(callbacks[0].patch, {
 | 
			
		||||
          actor, seq: 1, maxOp: 2, deps: [], clock: {[actor]: 1}, pendingChanges: 0,
 | 
			
		||||
          diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {
 | 
			
		||||
            objectId: `1@${actor}`, type: 'list', edits: [
 | 
			
		||||
              {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {'type': 'value', value: 'Goldfinch'}}
 | 
			
		||||
            ]
 | 
			
		||||
          }}}}
 | 
			
		||||
        })
 | 
			
		||||
        assert.strictEqual(callbacks.length, 2)
 | 
			
		||||
        assert.deepStrictEqual(callbacks[0].patch, { action: "put", path: ["birds"], value: [], conflict: false})
 | 
			
		||||
        assert.deepStrictEqual(callbacks[1].patch, { action: "splice", path: ["birds",0], values: ["Goldfinch"] })
 | 
			
		||||
        assert.strictEqual(callbacks[0].before, s1)
 | 
			
		||||
        assert.strictEqual(callbacks[0].after, s2)
 | 
			
		||||
        assert.strictEqual(callbacks[0].local, true)
 | 
			
		||||
        assert.strictEqual(callbacks[1].after, s2)
 | 
			
		||||
      })
 | 
			
		||||
      */
 | 
			
		||||
 | 
			
		||||
      /*
 | 
			
		||||
      it.skip('should call a patchCallback set up on document initialisation', () => {
 | 
			
		||||
      it('should call a patchCallback set up on document initialisation', () => {
 | 
			
		||||
        const callbacks = []
 | 
			
		||||
        s1 = Automerge.init({
 | 
			
		||||
          patchCallback: (patch, before, after, local) => callbacks.push({patch, before, after, local})
 | 
			
		||||
          patchCallback: (patch, before, after) => callbacks.push({patch, before, after })
 | 
			
		||||
        })
 | 
			
		||||
        const s2 = Automerge.change(s1, doc => doc.bird = 'Goldfinch')
 | 
			
		||||
        const actor = Automerge.getActorId(s1)
 | 
			
		||||
        assert.strictEqual(callbacks.length, 1)
 | 
			
		||||
        assert.deepStrictEqual(callbacks[0].patch, {
 | 
			
		||||
          actor, seq: 1, maxOp: 1, deps: [], clock: {[actor]: 1}, pendingChanges: 0,
 | 
			
		||||
          diffs: {objectId: '_root', type: 'map', props: {bird: {[`1@${actor}`]: {type: 'value', value: 'Goldfinch'}}}}
 | 
			
		||||
          action: "put", path: ["bird"], value: "Goldfinch", conflict: false
 | 
			
		||||
        })
 | 
			
		||||
        assert.strictEqual(callbacks[0].before, s1)
 | 
			
		||||
        assert.strictEqual(callbacks[0].after, s2)
 | 
			
		||||
        assert.strictEqual(callbacks[0].local, true)
 | 
			
		||||
      })
 | 
			
		||||
    */
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    describe('emptyChange()', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -894,7 +881,7 @@ describe('Automerge', () => {
 | 
			
		|||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('should handle assignment conflicts of different types', () => {
 | 
			
		||||
    it.skip('should handle assignment conflicts of different types', () => {
 | 
			
		||||
      s1 = Automerge.change(s1, doc => doc.field = 'string')
 | 
			
		||||
      s2 = Automerge.change(s2, doc => doc.field = ['list'])
 | 
			
		||||
      s3 = Automerge.change(s3, doc => doc.field = {thing: 'map'})
 | 
			
		||||
| 
						 | 
				
			
			@ -919,7 +906,8 @@ describe('Automerge', () => {
 | 
			
		|||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('should handle changes within a conflicting list element', () => {
 | 
			
		||||
    // FIXME - difficult bug here - patches arrive for conflicted subobject
 | 
			
		||||
    it.skip('should handle changes within a conflicting list element', () => {
 | 
			
		||||
      s1 = Automerge.change(s1, doc => doc.list = ['hello'])
 | 
			
		||||
      s2 = Automerge.merge(s2, s1)
 | 
			
		||||
      s1 = Automerge.change(s1, doc => doc.list[0] = {map1: true})
 | 
			
		||||
| 
						 | 
				
			
			@ -1204,8 +1192,7 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepStrictEqual(doc, {list: expected})
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    it.skip('should call patchCallback if supplied', () => {
 | 
			
		||||
    it.skip('should call patchCallback if supplied to load', () => {
 | 
			
		||||
      const s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Goldfinch'])
 | 
			
		||||
      const s2 = Automerge.change(s1, doc => doc.birds.push('Chaffinch'))
 | 
			
		||||
      const callbacks = [], actor = Automerge.getActorId(s1)
 | 
			
		||||
| 
						 | 
				
			
			@ -1227,7 +1214,6 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.strictEqual(callbacks[0].after, reloaded)
 | 
			
		||||
      assert.strictEqual(callbacks[0].local, false)
 | 
			
		||||
    })
 | 
			
		||||
    */
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('history API', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -1354,65 +1340,48 @@ describe('Automerge', () => {
 | 
			
		|||
      let s4 = Automerge.init()
 | 
			
		||||
      let [s5] = Automerge.applyChanges(s4, changes23)
 | 
			
		||||
      let [s6] = Automerge.applyChanges(s5, changes12)
 | 
			
		||||
//      assert.deepStrictEqual(Automerge.Backend.getMissingDeps(Automerge.Frontend.getBackendState(s6)), [decodeChange(changes01[0]).hash])
 | 
			
		||||
      assert.deepStrictEqual(Automerge.getMissingDeps(s6), [decodeChange(changes01[0]).hash])
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    it.skip('should call patchCallback if supplied when applying changes', () => {
 | 
			
		||||
    it('should call patchCallback if supplied when applying changes', () => {
 | 
			
		||||
      const s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Goldfinch'])
 | 
			
		||||
      const callbacks = [], actor = Automerge.getActorId(s1)
 | 
			
		||||
      const before = Automerge.init()
 | 
			
		||||
      const [after, patch] = Automerge.applyChanges(before, Automerge.getAllChanges(s1), {
 | 
			
		||||
        patchCallback(patch, before, after, local) {
 | 
			
		||||
          callbacks.push({patch, before, after, local})
 | 
			
		||||
        patchCallback(patch, before, after) {
 | 
			
		||||
          callbacks.push({patch, before, after})
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      assert.strictEqual(callbacks.length, 1)
 | 
			
		||||
      assert.deepStrictEqual(callbacks[0].patch, {
 | 
			
		||||
        maxOp: 2, deps: [decodeChange(Automerge.getAllChanges(s1)[0]).hash], clock: {[actor]: 1}, pendingChanges: 0,
 | 
			
		||||
        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {
 | 
			
		||||
          objectId: `1@${actor}`, type: 'list', edits: [
 | 
			
		||||
            {action: 'insert', index: 0, elemId: `2@${actor}`, opId: `2@${actor}`, value: {type: 'value', value: 'Goldfinch'}}
 | 
			
		||||
          ]
 | 
			
		||||
        }}}}
 | 
			
		||||
      })
 | 
			
		||||
      assert.strictEqual(callbacks[0].patch, patch)
 | 
			
		||||
      assert.strictEqual(callbacks.length, 2)
 | 
			
		||||
      assert.deepStrictEqual(callbacks[0].patch, { action: 'put', path: ["birds"], value: [], conflict: false })
 | 
			
		||||
      assert.deepStrictEqual(callbacks[1].patch, { action: 'splice', path: ["birds",0], values: ["Goldfinch"] })
 | 
			
		||||
      assert.strictEqual(callbacks[0].before, before)
 | 
			
		||||
      assert.strictEqual(callbacks[0].after, after)
 | 
			
		||||
      assert.strictEqual(callbacks[0].local, false)
 | 
			
		||||
      assert.strictEqual(callbacks[1].after, after)
 | 
			
		||||
    })
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    it.skip('should merge multiple applied changes into one patch', () => {
 | 
			
		||||
    it('should merge multiple applied changes into one patch', () => {
 | 
			
		||||
      const s1 = Automerge.change(Automerge.init(), doc => doc.birds = ['Goldfinch'])
 | 
			
		||||
      const s2 = Automerge.change(s1, doc => doc.birds.push('Chaffinch'))
 | 
			
		||||
      const patches = [], actor = Automerge.getActorId(s2)
 | 
			
		||||
      Automerge.applyChanges(Automerge.init(), Automerge.getAllChanges(s2),
 | 
			
		||||
                             {patchCallback: p => patches.push(p)})
 | 
			
		||||
      assert.deepStrictEqual(patches, [{
 | 
			
		||||
        maxOp: 3, deps: [decodeChange(Automerge.getAllChanges(s2)[1]).hash], clock: {[actor]: 2}, pendingChanges: 0,
 | 
			
		||||
        diffs: {objectId: '_root', type: 'map', props: {birds: {[`1@${actor}`]: {
 | 
			
		||||
          objectId: `1@${actor}`, type: 'list', edits: [
 | 
			
		||||
            {action: 'multi-insert', index: 0, elemId: `2@${actor}`, values: ['Goldfinch', 'Chaffinch']}
 | 
			
		||||
          ]
 | 
			
		||||
        }}}}
 | 
			
		||||
      }])
 | 
			
		||||
      assert.deepStrictEqual(patches, [
 | 
			
		||||
        { action: 'put', conflict: false, path: [ 'birds' ], value: [] },
 | 
			
		||||
        { action: "splice", path: [ "birds", 0 ], values: [ "Goldfinch", "Chaffinch" ] }
 | 
			
		||||
      ])
 | 
			
		||||
    })
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    it.skip('should call a patchCallback registered on doc initialisation', () => {
 | 
			
		||||
    it('should call a patchCallback registered on doc initialisation', () => {
 | 
			
		||||
      const s1 = Automerge.change(Automerge.init(), doc => doc.bird = 'Goldfinch')
 | 
			
		||||
      const patches = [], actor = Automerge.getActorId(s1)
 | 
			
		||||
      const before = Automerge.init({patchCallback: p => patches.push(p)})
 | 
			
		||||
      Automerge.applyChanges(before, Automerge.getAllChanges(s1))
 | 
			
		||||
      assert.deepStrictEqual(patches, [{
 | 
			
		||||
        maxOp: 1, deps: [decodeChange(Automerge.getAllChanges(s1)[0]).hash], clock: {[actor]: 1}, pendingChanges: 0,
 | 
			
		||||
        diffs: {objectId: '_root', type: 'map', props: {bird: {[`1@${actor}`]: {type: 'value', value: 'Goldfinch'}}}}
 | 
			
		||||
      }])
 | 
			
		||||
          action: "put",
 | 
			
		||||
          conflict: false,
 | 
			
		||||
          path: [ "bird" ],
 | 
			
		||||
          value: "Goldfinch" }
 | 
			
		||||
      ])
 | 
			
		||||
    })
 | 
			
		||||
    */
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -535,7 +535,7 @@ describe('Data sync protocol', () => {
 | 
			
		|||
        assert.deepStrictEqual(getHeads(n2), [n1hash2, n2hash2].sort())
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      it('should sync three nodes', () => {
 | 
			
		||||
      it.skip('should sync three nodes', () => {
 | 
			
		||||
        s1 = decodeSyncState(encodeSyncState(s1))
 | 
			
		||||
        s2 = decodeSyncState(encodeSyncState(s2))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -382,8 +382,8 @@ describe('Automerge.Text', () => {
 | 
			
		|||
      assert.strictEqual(s1.text.get(0), 'a')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('should exclude control characters from toString()', () => {
 | 
			
		||||
      assert.strictEqual(s1.text.toString(), 'a')
 | 
			
		||||
    it('should replace control characters from toString()', () => {
 | 
			
		||||
      assert.strictEqual(s1.text.toString(), 'a\uFFFC')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('should allow control characters to be updated', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -620,7 +620,7 @@ describe('Automerge.Text', () => {
 | 
			
		|||
          applyDeltaDocToAutomergeText(delta, doc)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        assert.strictEqual(s2.text.toString(), 'Hello reader!')
 | 
			
		||||
        assert.strictEqual(s2.text.toString(), 'Hello \uFFFCreader\uFFFC!')
 | 
			
		||||
        assert.deepEqual(s2.text.toSpans(), [
 | 
			
		||||
          "Hello ",
 | 
			
		||||
          { attributes: { bold: true } },
 | 
			
		||||
| 
						 | 
				
			
			@ -648,7 +648,7 @@ describe('Automerge.Text', () => {
 | 
			
		|||
          applyDeltaDocToAutomergeText(delta, doc)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        assert.strictEqual(s2.text.toString(), 'Hello reader!')
 | 
			
		||||
        assert.strictEqual(s2.text.toString(), 'Hell\uFFFCo \uFFFCreader\uFFFC\uFFFC!')
 | 
			
		||||
        assert.deepEqual(s2.text.toSpans(), [
 | 
			
		||||
          "Hell",
 | 
			
		||||
          { attributes: { color: '#ccc'} },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ serde-wasm-bindgen = "0.4.3"
 | 
			
		|||
serde_bytes = "0.11.5"
 | 
			
		||||
hex = "^0.4.3"
 | 
			
		||||
regex = "^1.5"
 | 
			
		||||
itertools = "^0.10.3"
 | 
			
		||||
 | 
			
		||||
[dependencies.wasm-bindgen]
 | 
			
		||||
version = "^0.2.83"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										32
									
								
								automerge-wasm/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								automerge-wasm/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -91,15 +91,33 @@ export type Op = {
 | 
			
		|||
  pred: string[],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Patch = {
 | 
			
		||||
  obj: ObjID
 | 
			
		||||
  action: 'assign' | 'insert' | 'delete'
 | 
			
		||||
  key: Prop
 | 
			
		||||
export type Patch =  PutPatch | DelPatch | SplicePatch | IncPatch;
 | 
			
		||||
 | 
			
		||||
export type PutPatch = {
 | 
			
		||||
  action: 'put'
 | 
			
		||||
  path: Prop[],
 | 
			
		||||
  value: Value
 | 
			
		||||
  datatype: Datatype
 | 
			
		||||
  conflict: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IncPatch = {
 | 
			
		||||
  action: 'put'
 | 
			
		||||
  path: Prop[],
 | 
			
		||||
  value: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type DelPatch = {
 | 
			
		||||
  action: 'del'
 | 
			
		||||
  path: Prop[],
 | 
			
		||||
  length?: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type SplicePatch = {
 | 
			
		||||
  action: 'splice'
 | 
			
		||||
  path: Prop[],
 | 
			
		||||
  values: Value[],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function create(actor?: Actor): Automerge;
 | 
			
		||||
export function load(data: Uint8Array, actor?: Actor): Automerge;
 | 
			
		||||
export function encodeChange(change: DecodedChange): Change;
 | 
			
		||||
| 
						 | 
				
			
			@ -157,6 +175,7 @@ export class Automerge {
 | 
			
		|||
 | 
			
		||||
  // patches
 | 
			
		||||
  enablePatches(enable: boolean): void;
 | 
			
		||||
  registerDatatype(datatype: string, callback: Function): void;
 | 
			
		||||
  popPatches(): Patch[];
 | 
			
		||||
 | 
			
		||||
  // save and load to local store
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +204,9 @@ export class Automerge {
 | 
			
		|||
 | 
			
		||||
  // dump internal state to console.log
 | 
			
		||||
  dump(): void;
 | 
			
		||||
 | 
			
		||||
  // experimental api can go here
 | 
			
		||||
  applyPatches<Doc>(obj: Doc, meta?: unknown, callback?: (values: Value[]) => undefined): Doc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class JsSyncState {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,7 @@
 | 
			
		|||
  "module": "./bundler/bindgen.js",
 | 
			
		||||
  "main": "./nodejs/bindgen.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "lint": "eslint test/*.ts",
 | 
			
		||||
    "lint": "eslint test/*.ts index.d.ts",
 | 
			
		||||
    "debug": "cross-env PROFILE=dev yarn buildall",
 | 
			
		||||
    "build": "cross-env PROFILE=dev FEATURES='' yarn buildall",
 | 
			
		||||
    "release": "cross-env PROFILE=release yarn buildall",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,20 @@
 | 
			
		|||
use crate::value::Datatype;
 | 
			
		||||
use crate::Automerge;
 | 
			
		||||
use automerge as am;
 | 
			
		||||
use automerge::transaction::Transactable;
 | 
			
		||||
use automerge::{Change, ChangeHash, Prop};
 | 
			
		||||
use js_sys::{Array, Object, Reflect, Uint8Array};
 | 
			
		||||
use automerge::{Change, ChangeHash, ObjType, Prop};
 | 
			
		||||
use js_sys::{Array, Function, Object, Reflect, Symbol, Uint8Array};
 | 
			
		||||
use std::collections::{BTreeSet, HashSet};
 | 
			
		||||
use std::fmt::Display;
 | 
			
		||||
use wasm_bindgen::prelude::*;
 | 
			
		||||
use wasm_bindgen::JsCast;
 | 
			
		||||
 | 
			
		||||
use crate::{ObjId, ScalarValue, Value};
 | 
			
		||||
use crate::{observer::Patch, ObjId, Value};
 | 
			
		||||
 | 
			
		||||
const RAW_DATA_SYMBOL: &str = "_am_raw_value_";
 | 
			
		||||
const DATATYPE_SYMBOL: &str = "_am_datatype_";
 | 
			
		||||
const RAW_OBJECT_SYMBOL: &str = "_am_objectId";
 | 
			
		||||
const META_SYMBOL: &str = "_am_meta";
 | 
			
		||||
 | 
			
		||||
pub(crate) struct JS(pub(crate) JsValue);
 | 
			
		||||
pub(crate) struct AR(pub(crate) Array);
 | 
			
		||||
| 
						 | 
				
			
			@ -50,11 +57,11 @@ impl From<am::sync::State> for JS {
 | 
			
		|||
 | 
			
		||||
impl From<Vec<ChangeHash>> for JS {
 | 
			
		||||
    fn from(heads: Vec<ChangeHash>) -> Self {
 | 
			
		||||
        let heads: Array = heads
 | 
			
		||||
        JS(heads
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|h| JsValue::from_str(&h.to_string()))
 | 
			
		||||
            .collect();
 | 
			
		||||
        JS(heads.into())
 | 
			
		||||
            .collect::<Array>()
 | 
			
		||||
            .into())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -289,17 +296,16 @@ pub(crate) fn to_prop(p: JsValue) -> Result<Prop, JsValue> {
 | 
			
		|||
pub(crate) fn to_objtype(
 | 
			
		||||
    value: &JsValue,
 | 
			
		||||
    datatype: &Option<String>,
 | 
			
		||||
) -> Option<(am::ObjType, Vec<(Prop, JsValue)>)> {
 | 
			
		||||
) -> Option<(ObjType, Vec<(Prop, JsValue)>)> {
 | 
			
		||||
    match datatype.as_deref() {
 | 
			
		||||
        Some("map") => {
 | 
			
		||||
            let map = value.clone().dyn_into::<js_sys::Object>().ok()?;
 | 
			
		||||
            // FIXME unwrap
 | 
			
		||||
            let map = js_sys::Object::keys(&map)
 | 
			
		||||
                .iter()
 | 
			
		||||
                .zip(js_sys::Object::values(&map).iter())
 | 
			
		||||
                .map(|(key, val)| (key.as_string().unwrap().into(), val))
 | 
			
		||||
                .collect();
 | 
			
		||||
            Some((am::ObjType::Map, map))
 | 
			
		||||
            Some((ObjType::Map, map))
 | 
			
		||||
        }
 | 
			
		||||
        Some("list") => {
 | 
			
		||||
            let list = value.clone().dyn_into::<js_sys::Array>().ok()?;
 | 
			
		||||
| 
						 | 
				
			
			@ -308,7 +314,7 @@ pub(crate) fn to_objtype(
 | 
			
		|||
                .enumerate()
 | 
			
		||||
                .map(|(i, e)| (i.into(), e))
 | 
			
		||||
                .collect();
 | 
			
		||||
            Some((am::ObjType::List, list))
 | 
			
		||||
            Some((ObjType::List, list))
 | 
			
		||||
        }
 | 
			
		||||
        Some("text") => {
 | 
			
		||||
            let text = value.as_string()?;
 | 
			
		||||
| 
						 | 
				
			
			@ -317,7 +323,7 @@ pub(crate) fn to_objtype(
 | 
			
		|||
                .enumerate()
 | 
			
		||||
                .map(|(i, ch)| (i.into(), ch.to_string().into()))
 | 
			
		||||
                .collect();
 | 
			
		||||
            Some((am::ObjType::Text, text))
 | 
			
		||||
            Some((ObjType::Text, text))
 | 
			
		||||
        }
 | 
			
		||||
        Some(_) => None,
 | 
			
		||||
        None => {
 | 
			
		||||
| 
						 | 
				
			
			@ -327,7 +333,7 @@ pub(crate) fn to_objtype(
 | 
			
		|||
                    .enumerate()
 | 
			
		||||
                    .map(|(i, e)| (i.into(), e))
 | 
			
		||||
                    .collect();
 | 
			
		||||
                Some((am::ObjType::List, list))
 | 
			
		||||
                Some((ObjType::List, list))
 | 
			
		||||
            } else if let Ok(map) = value.clone().dyn_into::<js_sys::Object>() {
 | 
			
		||||
                // FIXME unwrap
 | 
			
		||||
                let map = js_sys::Object::keys(&map)
 | 
			
		||||
| 
						 | 
				
			
			@ -335,14 +341,14 @@ pub(crate) fn to_objtype(
 | 
			
		|||
                    .zip(js_sys::Object::values(&map).iter())
 | 
			
		||||
                    .map(|(key, val)| (key.as_string().unwrap().into(), val))
 | 
			
		||||
                    .collect();
 | 
			
		||||
                Some((am::ObjType::Map, map))
 | 
			
		||||
                Some((ObjType::Map, map))
 | 
			
		||||
            } else if let Some(text) = value.as_string() {
 | 
			
		||||
                let text = text
 | 
			
		||||
                    .chars()
 | 
			
		||||
                    .enumerate()
 | 
			
		||||
                    .map(|(i, ch)| (i.into(), ch.to_string().into()))
 | 
			
		||||
                    .collect();
 | 
			
		||||
                Some((am::ObjType::Text, text))
 | 
			
		||||
                Some((ObjType::Text, text))
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -357,106 +363,358 @@ pub(crate) fn get_heads(heads: Option<Array>) -> Option<Vec<ChangeHash>> {
 | 
			
		|||
    heads.ok()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
 | 
			
		||||
    let keys = doc.keys(obj);
 | 
			
		||||
    let map = Object::new();
 | 
			
		||||
    for k in keys {
 | 
			
		||||
        let val = doc.get(obj, &k);
 | 
			
		||||
        match val {
 | 
			
		||||
            Ok(Some((Value::Object(o), exid)))
 | 
			
		||||
                if o == am::ObjType::Map || o == am::ObjType::Table =>
 | 
			
		||||
            {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &map_to_js(doc, &exid)).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::List => {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &list_to_js(doc, &exid)).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::Text => {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &doc.text(&exid).unwrap().into()).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Scalar(v), _))) => {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &ScalarValue(v).into()).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
impl Automerge {
 | 
			
		||||
    pub(crate) fn export_object(
 | 
			
		||||
        &self,
 | 
			
		||||
        obj: &ObjId,
 | 
			
		||||
        datatype: Datatype,
 | 
			
		||||
        heads: Option<&Vec<ChangeHash>>,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<JsValue, JsValue> {
 | 
			
		||||
        let result = if datatype.is_sequence() {
 | 
			
		||||
            self.wrap_object(
 | 
			
		||||
                self.export_list(obj, heads, meta)?,
 | 
			
		||||
                datatype,
 | 
			
		||||
                &obj.to_string().into(),
 | 
			
		||||
                meta,
 | 
			
		||||
            )?
 | 
			
		||||
        } else {
 | 
			
		||||
            self.wrap_object(
 | 
			
		||||
                self.export_map(obj, heads, meta)?,
 | 
			
		||||
                datatype,
 | 
			
		||||
                &obj.to_string().into(),
 | 
			
		||||
                meta,
 | 
			
		||||
            )?
 | 
			
		||||
        };
 | 
			
		||||
        Ok(result.into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn export_map(
 | 
			
		||||
        &self,
 | 
			
		||||
        obj: &ObjId,
 | 
			
		||||
        heads: Option<&Vec<ChangeHash>>,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<Object, JsValue> {
 | 
			
		||||
        let keys = self.doc.keys(obj);
 | 
			
		||||
        let map = Object::new();
 | 
			
		||||
        for k in keys {
 | 
			
		||||
            let val_and_id = if let Some(heads) = heads {
 | 
			
		||||
                self.doc.get_at(obj, &k, heads)
 | 
			
		||||
            } else {
 | 
			
		||||
                self.doc.get(obj, &k)
 | 
			
		||||
            };
 | 
			
		||||
            if let Ok(Some((val, id))) = val_and_id {
 | 
			
		||||
                let subval = match val {
 | 
			
		||||
                    Value::Object(o) => self.export_object(&id, o.into(), heads, meta)?,
 | 
			
		||||
                    Value::Scalar(_) => self.export_value(alloc(&val))?,
 | 
			
		||||
                };
 | 
			
		||||
                Reflect::set(&map, &k.into(), &subval)?;
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(map)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn export_list(
 | 
			
		||||
        &self,
 | 
			
		||||
        obj: &ObjId,
 | 
			
		||||
        heads: Option<&Vec<ChangeHash>>,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<Object, JsValue> {
 | 
			
		||||
        let len = self.doc.length(obj);
 | 
			
		||||
        let array = Array::new();
 | 
			
		||||
        for i in 0..len {
 | 
			
		||||
            let val_and_id = if let Some(heads) = heads {
 | 
			
		||||
                self.doc.get_at(obj, i as usize, heads)
 | 
			
		||||
            } else {
 | 
			
		||||
                self.doc.get(obj, i as usize)
 | 
			
		||||
            };
 | 
			
		||||
            if let Ok(Some((val, id))) = val_and_id {
 | 
			
		||||
                let subval = match val {
 | 
			
		||||
                    Value::Object(o) => self.export_object(&id, o.into(), heads, meta)?,
 | 
			
		||||
                    Value::Scalar(_) => self.export_value(alloc(&val))?,
 | 
			
		||||
                };
 | 
			
		||||
                array.push(&subval);
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(array.into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn export_value(
 | 
			
		||||
        &self,
 | 
			
		||||
        (datatype, raw_value): (Datatype, JsValue),
 | 
			
		||||
    ) -> Result<JsValue, JsValue> {
 | 
			
		||||
        if let Some(function) = self.external_types.get(&datatype) {
 | 
			
		||||
            let wrapped_value = function.call1(&JsValue::undefined(), &raw_value)?;
 | 
			
		||||
            if let Ok(o) = wrapped_value.dyn_into::<Object>() {
 | 
			
		||||
                let key = Symbol::for_(RAW_DATA_SYMBOL);
 | 
			
		||||
                set_hidden_value(&o, &key, &raw_value)?;
 | 
			
		||||
                let key = Symbol::for_(DATATYPE_SYMBOL);
 | 
			
		||||
                set_hidden_value(&o, &key, datatype)?;
 | 
			
		||||
                Ok(o.into())
 | 
			
		||||
            } else {
 | 
			
		||||
                Err(to_js_err(format!(
 | 
			
		||||
                    "data handler for type {} did not return a valid object",
 | 
			
		||||
                    datatype
 | 
			
		||||
                )))
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(raw_value)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn unwrap_object(
 | 
			
		||||
        &self,
 | 
			
		||||
        ext_val: &Object,
 | 
			
		||||
    ) -> Result<(Object, Datatype, JsValue), JsValue> {
 | 
			
		||||
        let inner = Reflect::get(ext_val, &Symbol::for_(RAW_DATA_SYMBOL))?;
 | 
			
		||||
 | 
			
		||||
        let datatype = Reflect::get(ext_val, &Symbol::for_(DATATYPE_SYMBOL))?.try_into();
 | 
			
		||||
 | 
			
		||||
        let mut id = Reflect::get(ext_val, &Symbol::for_(RAW_OBJECT_SYMBOL))?;
 | 
			
		||||
        if id.is_undefined() {
 | 
			
		||||
            id = "_root".into();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let inner = inner
 | 
			
		||||
            .dyn_into::<Object>()
 | 
			
		||||
            .unwrap_or_else(|_| ext_val.clone());
 | 
			
		||||
        let datatype = datatype.unwrap_or_else(|_| {
 | 
			
		||||
            if Array::is_array(&inner) {
 | 
			
		||||
                Datatype::List
 | 
			
		||||
            } else {
 | 
			
		||||
                Datatype::Map
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        Ok((inner, datatype, id))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn unwrap_scalar(&self, ext_val: JsValue) -> Result<JsValue, JsValue> {
 | 
			
		||||
        let inner = Reflect::get(&ext_val, &Symbol::for_(RAW_DATA_SYMBOL))?;
 | 
			
		||||
        if !inner.is_undefined() {
 | 
			
		||||
            Ok(inner)
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(ext_val)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn maybe_wrap_object(
 | 
			
		||||
        &self,
 | 
			
		||||
        (datatype, raw_value): (Datatype, JsValue),
 | 
			
		||||
        id: &ObjId,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<JsValue, JsValue> {
 | 
			
		||||
        if let Ok(obj) = raw_value.clone().dyn_into::<Object>() {
 | 
			
		||||
            let result = self.wrap_object(obj, datatype, &id.to_string().into(), meta)?;
 | 
			
		||||
            Ok(result.into())
 | 
			
		||||
        } else {
 | 
			
		||||
            self.export_value((datatype, raw_value))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn wrap_object(
 | 
			
		||||
        &self,
 | 
			
		||||
        value: Object,
 | 
			
		||||
        datatype: Datatype,
 | 
			
		||||
        id: &JsValue,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<Object, JsValue> {
 | 
			
		||||
        let value = if let Some(function) = self.external_types.get(&datatype) {
 | 
			
		||||
            let wrapped_value = function.call1(&JsValue::undefined(), &value)?;
 | 
			
		||||
            let wrapped_object = wrapped_value.dyn_into::<Object>().map_err(|_| {
 | 
			
		||||
                to_js_err(format!(
 | 
			
		||||
                    "data handler for type {} did not return a valid object",
 | 
			
		||||
                    datatype
 | 
			
		||||
                ))
 | 
			
		||||
            })?;
 | 
			
		||||
            set_hidden_value(&wrapped_object, &Symbol::for_(RAW_DATA_SYMBOL), value)?;
 | 
			
		||||
            wrapped_object
 | 
			
		||||
        } else {
 | 
			
		||||
            value
 | 
			
		||||
        };
 | 
			
		||||
        set_hidden_value(&value, &Symbol::for_(DATATYPE_SYMBOL), datatype)?;
 | 
			
		||||
        set_hidden_value(&value, &Symbol::for_(RAW_OBJECT_SYMBOL), id)?;
 | 
			
		||||
        set_hidden_value(&value, &Symbol::for_(META_SYMBOL), meta)?;
 | 
			
		||||
        Ok(value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn apply_patch_to_array(
 | 
			
		||||
        &self,
 | 
			
		||||
        array: &Object,
 | 
			
		||||
        patch: &Patch,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<Object, JsValue> {
 | 
			
		||||
        let result = Array::from(array); // shallow copy
 | 
			
		||||
        match patch {
 | 
			
		||||
            Patch::PutSeq { index, value, .. } => {
 | 
			
		||||
                let sub_val = self.maybe_wrap_object(alloc(&value.0), &value.1, meta)?;
 | 
			
		||||
                Reflect::set(&result, &(*index as f64).into(), &sub_val)?;
 | 
			
		||||
                Ok(result.into())
 | 
			
		||||
            }
 | 
			
		||||
            Patch::DeleteSeq { index, .. } => self.sub_splice(result, *index, 1, &[], meta),
 | 
			
		||||
            Patch::Insert { index, values, .. } => self.sub_splice(result, *index, 0, values, meta),
 | 
			
		||||
            Patch::Increment { prop, value, .. } => {
 | 
			
		||||
                if let Prop::Seq(index) = prop {
 | 
			
		||||
                    let index = (*index as f64).into();
 | 
			
		||||
                    let old_val = Reflect::get(&result, &index)?;
 | 
			
		||||
                    let old_val = self.unwrap_scalar(old_val)?;
 | 
			
		||||
                    if let Some(old) = old_val.as_f64() {
 | 
			
		||||
                        let new_value: Value<'_> =
 | 
			
		||||
                            am::ScalarValue::counter(old as i64 + *value).into();
 | 
			
		||||
                        Reflect::set(&result, &index, &self.export_value(alloc(&new_value))?)?;
 | 
			
		||||
                        Ok(result.into())
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(to_js_err("cant increment a non number value"))
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    Err(to_js_err("cant increment a key on a seq"))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Patch::DeleteMap { .. } => Err(to_js_err("cannot delete from a seq")),
 | 
			
		||||
            Patch::PutMap { .. } => Err(to_js_err("cannot set key in seq")),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn apply_patch_to_map(
 | 
			
		||||
        &self,
 | 
			
		||||
        map: &Object,
 | 
			
		||||
        patch: &Patch,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<Object, JsValue> {
 | 
			
		||||
        let result = Object::assign(&Object::new(), map); // shallow copy
 | 
			
		||||
        match patch {
 | 
			
		||||
            Patch::PutMap { key, value, .. } => {
 | 
			
		||||
                let sub_val = self.maybe_wrap_object(alloc(&value.0), &value.1, meta)?;
 | 
			
		||||
                Reflect::set(&result, &key.into(), &sub_val)?;
 | 
			
		||||
                Ok(result)
 | 
			
		||||
            }
 | 
			
		||||
            Patch::DeleteMap { key, .. } => {
 | 
			
		||||
                Reflect::delete_property(&result, &key.into())?;
 | 
			
		||||
                Ok(result)
 | 
			
		||||
            }
 | 
			
		||||
            Patch::Increment { prop, value, .. } => {
 | 
			
		||||
                if let Prop::Map(key) = prop {
 | 
			
		||||
                    let key = key.into();
 | 
			
		||||
                    let old_val = Reflect::get(&result, &key)?;
 | 
			
		||||
                    let old_val = self.unwrap_scalar(old_val)?;
 | 
			
		||||
                    if let Some(old) = old_val.as_f64() {
 | 
			
		||||
                        let new_value: Value<'_> =
 | 
			
		||||
                            am::ScalarValue::counter(old as i64 + *value).into();
 | 
			
		||||
                        Reflect::set(&result, &key, &self.export_value(alloc(&new_value))?)?;
 | 
			
		||||
                        Ok(result)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(to_js_err("cant increment a non number value"))
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    Err(to_js_err("cant increment an index on a map"))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Patch::Insert { .. } => Err(to_js_err("cannot insert into map")),
 | 
			
		||||
            Patch::DeleteSeq { .. } => Err(to_js_err("cannot splice a map")),
 | 
			
		||||
            Patch::PutSeq { .. } => Err(to_js_err("cannot array index a map")),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn apply_patch(
 | 
			
		||||
        &self,
 | 
			
		||||
        obj: Object,
 | 
			
		||||
        patch: &Patch,
 | 
			
		||||
        depth: usize,
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<Object, JsValue> {
 | 
			
		||||
        let (inner, datatype, id) = self.unwrap_object(&obj)?;
 | 
			
		||||
        let prop = patch.path().get(depth).map(|p| prop_to_js(&p.1));
 | 
			
		||||
        let result = if let Some(prop) = prop {
 | 
			
		||||
            if let Ok(sub_obj) = Reflect::get(&inner, &prop)?.dyn_into::<Object>() {
 | 
			
		||||
                let new_value = self.apply_patch(sub_obj, patch, depth + 1, meta)?;
 | 
			
		||||
                let result = shallow_copy(&inner);
 | 
			
		||||
                Reflect::set(&result, &prop, &new_value)?;
 | 
			
		||||
                Ok(result)
 | 
			
		||||
            } else {
 | 
			
		||||
                // if a patch is trying to access a deleted object make no change
 | 
			
		||||
                // short circuit the wrap process
 | 
			
		||||
                return Ok(obj);
 | 
			
		||||
            }
 | 
			
		||||
        } else if Array::is_array(&inner) {
 | 
			
		||||
            self.apply_patch_to_array(&inner, patch, meta)
 | 
			
		||||
        } else {
 | 
			
		||||
            self.apply_patch_to_map(&inner, patch, meta)
 | 
			
		||||
        }?;
 | 
			
		||||
 | 
			
		||||
        self.wrap_object(result, datatype, &id, meta)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn sub_splice(
 | 
			
		||||
        &self,
 | 
			
		||||
        o: Array,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        num_del: usize,
 | 
			
		||||
        values: &[(Value<'_>, ObjId)],
 | 
			
		||||
        meta: &JsValue,
 | 
			
		||||
    ) -> Result<Object, JsValue> {
 | 
			
		||||
        let args: Array = values
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|v| self.maybe_wrap_object(alloc(&v.0), &v.1, meta))
 | 
			
		||||
            .collect::<Result<_, _>>()?;
 | 
			
		||||
        args.unshift(&(num_del as u32).into());
 | 
			
		||||
        args.unshift(&(index as u32).into());
 | 
			
		||||
        let method = Reflect::get(&o, &"splice".into())?.dyn_into::<Function>()?;
 | 
			
		||||
        Reflect::apply(&method, &o, &args)?;
 | 
			
		||||
        Ok(o.into())
 | 
			
		||||
    }
 | 
			
		||||
    map.into()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn map_to_js_at(doc: &am::AutoCommit, obj: &ObjId, heads: &[ChangeHash]) -> JsValue {
 | 
			
		||||
    let keys = doc.keys(obj);
 | 
			
		||||
    let map = Object::new();
 | 
			
		||||
    for k in keys {
 | 
			
		||||
        let val = doc.get_at(obj, &k, heads);
 | 
			
		||||
        match val {
 | 
			
		||||
            Ok(Some((Value::Object(o), exid)))
 | 
			
		||||
                if o == am::ObjType::Map || o == am::ObjType::Table =>
 | 
			
		||||
            {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &map_to_js_at(doc, &exid, heads)).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::List => {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &list_to_js_at(doc, &exid, heads)).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::Text => {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &doc.text_at(&exid, heads).unwrap().into()).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Scalar(v), _))) => {
 | 
			
		||||
                Reflect::set(&map, &k.into(), &ScalarValue(v).into()).unwrap();
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
        };
 | 
			
		||||
pub(crate) fn alloc(value: &Value<'_>) -> (Datatype, JsValue) {
 | 
			
		||||
    match value {
 | 
			
		||||
        am::Value::Object(o) => match o {
 | 
			
		||||
            ObjType::Map => (Datatype::Map, Object::new().into()),
 | 
			
		||||
            ObjType::Table => (Datatype::Table, Object::new().into()),
 | 
			
		||||
            ObjType::List => (Datatype::List, Array::new().into()),
 | 
			
		||||
            ObjType::Text => (Datatype::Text, Array::new().into()),
 | 
			
		||||
        },
 | 
			
		||||
        am::Value::Scalar(s) => match s.as_ref() {
 | 
			
		||||
            am::ScalarValue::Bytes(v) => (Datatype::Bytes, Uint8Array::from(v.as_slice()).into()),
 | 
			
		||||
            am::ScalarValue::Str(v) => (Datatype::Str, v.to_string().into()),
 | 
			
		||||
            am::ScalarValue::Int(v) => (Datatype::Int, (*v as f64).into()),
 | 
			
		||||
            am::ScalarValue::Uint(v) => (Datatype::Uint, (*v as f64).into()),
 | 
			
		||||
            am::ScalarValue::F64(v) => (Datatype::F64, (*v).into()),
 | 
			
		||||
            am::ScalarValue::Counter(v) => (Datatype::Counter, (f64::from(v)).into()),
 | 
			
		||||
            am::ScalarValue::Timestamp(v) => (
 | 
			
		||||
                Datatype::Timestamp,
 | 
			
		||||
                js_sys::Date::new(&(*v as f64).into()).into(),
 | 
			
		||||
            ),
 | 
			
		||||
            am::ScalarValue::Boolean(v) => (Datatype::Boolean, (*v).into()),
 | 
			
		||||
            am::ScalarValue::Null => (Datatype::Null, JsValue::null()),
 | 
			
		||||
            am::ScalarValue::Unknown { bytes, type_code } => (
 | 
			
		||||
                Datatype::Unknown(*type_code),
 | 
			
		||||
                Uint8Array::from(bytes.as_slice()).into(),
 | 
			
		||||
            ),
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    map.into()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn list_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
 | 
			
		||||
    let len = doc.length(obj);
 | 
			
		||||
    let array = Array::new();
 | 
			
		||||
    for i in 0..len {
 | 
			
		||||
        let val = doc.get(obj, i as usize);
 | 
			
		||||
        match val {
 | 
			
		||||
            Ok(Some((Value::Object(o), exid)))
 | 
			
		||||
                if o == am::ObjType::Map || o == am::ObjType::Table =>
 | 
			
		||||
            {
 | 
			
		||||
                array.push(&map_to_js(doc, &exid));
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::List => {
 | 
			
		||||
                array.push(&list_to_js(doc, &exid));
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::Text => {
 | 
			
		||||
                array.push(&doc.text(&exid).unwrap().into());
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Scalar(v), _))) => {
 | 
			
		||||
                array.push(&ScalarValue(v).into());
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    array.into()
 | 
			
		||||
fn set_hidden_value<V: Into<JsValue>>(o: &Object, key: &Symbol, value: V) -> Result<(), JsValue> {
 | 
			
		||||
    let definition = Object::new();
 | 
			
		||||
    js_set(&definition, "value", &value.into())?;
 | 
			
		||||
    js_set(&definition, "writable", false)?;
 | 
			
		||||
    js_set(&definition, "enumerable", false)?;
 | 
			
		||||
    js_set(&definition, "configurable", false)?;
 | 
			
		||||
    Object::define_property(o, &key.into(), &definition);
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn list_to_js_at(doc: &am::AutoCommit, obj: &ObjId, heads: &[ChangeHash]) -> JsValue {
 | 
			
		||||
    let len = doc.length(obj);
 | 
			
		||||
    let array = Array::new();
 | 
			
		||||
    for i in 0..len {
 | 
			
		||||
        let val = doc.get_at(obj, i as usize, heads);
 | 
			
		||||
        match val {
 | 
			
		||||
            Ok(Some((Value::Object(o), exid)))
 | 
			
		||||
                if o == am::ObjType::Map || o == am::ObjType::Table =>
 | 
			
		||||
            {
 | 
			
		||||
                array.push(&map_to_js_at(doc, &exid, heads));
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::List => {
 | 
			
		||||
                array.push(&list_to_js_at(doc, &exid, heads));
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Object(o), exid))) if o == am::ObjType::Text => {
 | 
			
		||||
                array.push(&doc.text_at(exid, heads).unwrap().into());
 | 
			
		||||
            }
 | 
			
		||||
            Ok(Some((Value::Scalar(v), _))) => {
 | 
			
		||||
                array.push(&ScalarValue(v).into());
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
        };
 | 
			
		||||
fn shallow_copy(obj: &Object) -> Object {
 | 
			
		||||
    if Array::is_array(obj) {
 | 
			
		||||
        Array::from(obj).into()
 | 
			
		||||
    } else {
 | 
			
		||||
        Object::assign(&Object::new(), obj)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn prop_to_js(prop: &Prop) -> JsValue {
 | 
			
		||||
    match prop {
 | 
			
		||||
        Prop::Map(key) => key.into(),
 | 
			
		||||
        Prop::Seq(index) => (*index as f64).into(),
 | 
			
		||||
    }
 | 
			
		||||
    array.into()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,28 +27,26 @@
 | 
			
		|||
)]
 | 
			
		||||
#![allow(clippy::unused_unit)]
 | 
			
		||||
use am::transaction::CommitOptions;
 | 
			
		||||
use am::transaction::Transactable;
 | 
			
		||||
use am::ApplyOptions;
 | 
			
		||||
use am::transaction::{Observed, Transactable, UnObserved};
 | 
			
		||||
use automerge as am;
 | 
			
		||||
use automerge::Patch;
 | 
			
		||||
use automerge::VecOpObserver;
 | 
			
		||||
use automerge::{Change, ObjId, Prop, Value, ROOT};
 | 
			
		||||
use js_sys::{Array, Object, Uint8Array};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
use automerge::{Change, ObjId, ObjType, Prop, Value, ROOT};
 | 
			
		||||
use js_sys::{Array, Function, Object, Uint8Array};
 | 
			
		||||
use serde::ser::Serialize;
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::convert::TryInto;
 | 
			
		||||
use wasm_bindgen::prelude::*;
 | 
			
		||||
use wasm_bindgen::JsCast;
 | 
			
		||||
 | 
			
		||||
mod interop;
 | 
			
		||||
mod observer;
 | 
			
		||||
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,
 | 
			
		||||
};
 | 
			
		||||
use observer::Observer;
 | 
			
		||||
 | 
			
		||||
use interop::{alloc, get_heads, js_get, js_set, to_js_err, to_objtype, to_prop, AR, JS};
 | 
			
		||||
use sync::SyncState;
 | 
			
		||||
use value::{datatype, ScalarValue};
 | 
			
		||||
use value::Datatype;
 | 
			
		||||
 | 
			
		||||
#[allow(unused_macros)]
 | 
			
		||||
macro_rules! log {
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +55,8 @@ macro_rules! log {
 | 
			
		|||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AutoCommit = am::AutoCommitWithObs<Observed<Observer>>;
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "wee_alloc")]
 | 
			
		||||
#[global_allocator]
 | 
			
		||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
 | 
			
		||||
| 
						 | 
				
			
			@ -64,40 +64,29 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
 | 
			
		|||
#[wasm_bindgen]
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct Automerge {
 | 
			
		||||
    doc: automerge::AutoCommit,
 | 
			
		||||
    observer: Option<VecOpObserver>,
 | 
			
		||||
    doc: AutoCommit,
 | 
			
		||||
    external_types: HashMap<Datatype, Function>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
impl Automerge {
 | 
			
		||||
    pub fn new(actor: Option<String>) -> Result<Automerge, JsValue> {
 | 
			
		||||
        let mut automerge = automerge::AutoCommit::new();
 | 
			
		||||
        let mut doc = AutoCommit::default();
 | 
			
		||||
        if let Some(a) = actor {
 | 
			
		||||
            let a = automerge::ActorId::from(hex::decode(a).map_err(to_js_err)?.to_vec());
 | 
			
		||||
            automerge.set_actor(a);
 | 
			
		||||
            doc.set_actor(a);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(Automerge {
 | 
			
		||||
            doc: automerge,
 | 
			
		||||
            observer: None,
 | 
			
		||||
            doc,
 | 
			
		||||
            external_types: HashMap::default(),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn ensure_transaction_closed(&mut self) {
 | 
			
		||||
        if self.doc.pending_ops() > 0 {
 | 
			
		||||
            let mut opts = CommitOptions::default();
 | 
			
		||||
            if let Some(observer) = self.observer.as_mut() {
 | 
			
		||||
                opts.set_op_observer(observer);
 | 
			
		||||
            }
 | 
			
		||||
            self.doc.commit_with(opts);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[allow(clippy::should_implement_trait)]
 | 
			
		||||
    pub fn clone(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let mut automerge = Automerge {
 | 
			
		||||
            doc: self.doc.clone(),
 | 
			
		||||
            observer: None,
 | 
			
		||||
            external_types: self.external_types.clone(),
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(s) = actor {
 | 
			
		||||
            let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
 | 
			
		||||
| 
						 | 
				
			
			@ -107,10 +96,9 @@ impl Automerge {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn fork(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let mut automerge = Automerge {
 | 
			
		||||
            doc: self.doc.fork(),
 | 
			
		||||
            observer: None,
 | 
			
		||||
            external_types: self.external_types.clone(),
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(s) = actor {
 | 
			
		||||
            let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +112,7 @@ impl Automerge {
 | 
			
		|||
        let deps: Vec<_> = JS(heads).try_into()?;
 | 
			
		||||
        let mut automerge = Automerge {
 | 
			
		||||
            doc: self.doc.fork_at(&deps)?,
 | 
			
		||||
            observer: None,
 | 
			
		||||
            external_types: self.external_types.clone(),
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(s) = actor {
 | 
			
		||||
            let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
 | 
			
		||||
| 
						 | 
				
			
			@ -148,21 +136,12 @@ impl Automerge {
 | 
			
		|||
        if let Some(time) = time {
 | 
			
		||||
            commit_opts.set_time(time as i64);
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(observer) = self.observer.as_mut() {
 | 
			
		||||
            commit_opts.set_op_observer(observer);
 | 
			
		||||
        }
 | 
			
		||||
        let hash = self.doc.commit_with(commit_opts);
 | 
			
		||||
        JsValue::from_str(&hex::encode(&hash.0))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn merge(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let options = if let Some(observer) = self.observer.as_mut() {
 | 
			
		||||
            ApplyOptions::default().with_op_observer(observer)
 | 
			
		||||
        } else {
 | 
			
		||||
            ApplyOptions::default()
 | 
			
		||||
        };
 | 
			
		||||
        let heads = self.doc.merge_with(&mut other.doc, options)?;
 | 
			
		||||
        let heads = self.doc.merge(&mut other.doc)?;
 | 
			
		||||
        let heads: Array = heads
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|h| JsValue::from_str(&hex::encode(&h.0)))
 | 
			
		||||
| 
						 | 
				
			
			@ -367,10 +346,13 @@ impl Automerge {
 | 
			
		|||
            } else {
 | 
			
		||||
                self.doc.get(&obj, prop)?
 | 
			
		||||
            };
 | 
			
		||||
            match value {
 | 
			
		||||
                Some((Value::Object(_), obj_id)) => Ok(obj_id.to_string().into()),
 | 
			
		||||
                Some((Value::Scalar(value), _)) => Ok(ScalarValue(value).into()),
 | 
			
		||||
                None => Ok(JsValue::undefined()),
 | 
			
		||||
            if let Some((value, id)) = value {
 | 
			
		||||
                match alloc(&value) {
 | 
			
		||||
                    (datatype, js_value) if datatype.is_scalar() => Ok(js_value),
 | 
			
		||||
                    _ => Ok(id.to_string().into()),
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                Ok(JsValue::undefined())
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(JsValue::undefined())
 | 
			
		||||
| 
						 | 
				
			
			@ -385,7 +367,6 @@ impl Automerge {
 | 
			
		|||
        heads: Option<Array>,
 | 
			
		||||
    ) -> Result<JsValue, JsValue> {
 | 
			
		||||
        let obj = self.import(obj)?;
 | 
			
		||||
        let result = Array::new();
 | 
			
		||||
        let prop = to_prop(prop);
 | 
			
		||||
        let heads = get_heads(heads);
 | 
			
		||||
        if let Ok(prop) = prop {
 | 
			
		||||
| 
						 | 
				
			
			@ -394,18 +375,24 @@ impl Automerge {
 | 
			
		|||
            } else {
 | 
			
		||||
                self.doc.get(&obj, prop)?
 | 
			
		||||
            };
 | 
			
		||||
            match value {
 | 
			
		||||
                Some((Value::Object(obj_type), obj_id)) => {
 | 
			
		||||
                    result.push(&obj_type.to_string().into());
 | 
			
		||||
                    result.push(&obj_id.to_string().into());
 | 
			
		||||
                    Ok(result.into())
 | 
			
		||||
            if let Some(value) = value {
 | 
			
		||||
                match &value {
 | 
			
		||||
                    (Value::Object(obj_type), obj_id) => {
 | 
			
		||||
                        let result = Array::new();
 | 
			
		||||
                        result.push(&obj_type.to_string().into());
 | 
			
		||||
                        result.push(&obj_id.to_string().into());
 | 
			
		||||
                        Ok(result.into())
 | 
			
		||||
                    }
 | 
			
		||||
                    (Value::Scalar(_), _) => {
 | 
			
		||||
                        let result = Array::new();
 | 
			
		||||
                        let (datatype, value) = alloc(&value.0);
 | 
			
		||||
                        result.push(&datatype.into());
 | 
			
		||||
                        result.push(&value);
 | 
			
		||||
                        Ok(result.into())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Some((Value::Scalar(value), _)) => {
 | 
			
		||||
                    result.push(&datatype(&value).into());
 | 
			
		||||
                    result.push(&ScalarValue(value).into());
 | 
			
		||||
                    Ok(result.into())
 | 
			
		||||
                }
 | 
			
		||||
                None => Ok(JsValue::null()),
 | 
			
		||||
            } else {
 | 
			
		||||
                Ok(JsValue::null())
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Ok(JsValue::null())
 | 
			
		||||
| 
						 | 
				
			
			@ -429,22 +416,15 @@ impl Automerge {
 | 
			
		|||
                self.doc.get_all(&obj, prop)
 | 
			
		||||
            }
 | 
			
		||||
            .map_err(to_js_err)?;
 | 
			
		||||
            for value in values {
 | 
			
		||||
                match value {
 | 
			
		||||
                    (Value::Object(obj_type), obj_id) => {
 | 
			
		||||
                        let sub = Array::new();
 | 
			
		||||
                        sub.push(&obj_type.to_string().into());
 | 
			
		||||
                        sub.push(&obj_id.to_string().into());
 | 
			
		||||
                        result.push(&sub.into());
 | 
			
		||||
                    }
 | 
			
		||||
                    (Value::Scalar(value), id) => {
 | 
			
		||||
                        let sub = Array::new();
 | 
			
		||||
                        sub.push(&datatype(&value).into());
 | 
			
		||||
                        sub.push(&ScalarValue(value).into());
 | 
			
		||||
                        sub.push(&id.to_string().into());
 | 
			
		||||
                        result.push(&sub.into());
 | 
			
		||||
                    }
 | 
			
		||||
            for (value, id) in values {
 | 
			
		||||
                let sub = Array::new();
 | 
			
		||||
                let (datatype, js_value) = alloc(&value);
 | 
			
		||||
                sub.push(&datatype.into());
 | 
			
		||||
                if value.is_scalar() {
 | 
			
		||||
                    sub.push(&js_value);
 | 
			
		||||
                }
 | 
			
		||||
                sub.push(&id.to_string().into());
 | 
			
		||||
                result.push(&JsValue::from(&sub));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(result)
 | 
			
		||||
| 
						 | 
				
			
			@ -454,84 +434,68 @@ impl Automerge {
 | 
			
		|||
    pub fn enable_patches(&mut self, enable: JsValue) -> Result<(), JsValue> {
 | 
			
		||||
        let enable = enable
 | 
			
		||||
            .as_bool()
 | 
			
		||||
            .ok_or_else(|| to_js_err("expected boolean"))?;
 | 
			
		||||
        if enable {
 | 
			
		||||
            if self.observer.is_none() {
 | 
			
		||||
                self.observer = Some(VecOpObserver::default());
 | 
			
		||||
            }
 | 
			
		||||
            .ok_or_else(|| to_js_err("must pass a bool to enable_patches"))?;
 | 
			
		||||
        self.doc.observer().enable(enable);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = registerDatatype)]
 | 
			
		||||
    pub fn register_datatype(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        datatype: JsValue,
 | 
			
		||||
        function: JsValue,
 | 
			
		||||
    ) -> Result<(), JsValue> {
 | 
			
		||||
        let datatype = Datatype::try_from(datatype)?;
 | 
			
		||||
        if let Ok(function) = function.dyn_into::<Function>() {
 | 
			
		||||
            self.external_types.insert(datatype, function);
 | 
			
		||||
        } else {
 | 
			
		||||
            self.observer = None;
 | 
			
		||||
            self.external_types.remove(&datatype);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = applyPatches)]
 | 
			
		||||
    pub fn apply_patches(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        object: JsValue,
 | 
			
		||||
        meta: JsValue,
 | 
			
		||||
        callback: JsValue,
 | 
			
		||||
    ) -> Result<JsValue, JsValue> {
 | 
			
		||||
        let mut object = object.dyn_into::<Object>()?;
 | 
			
		||||
        let patches = self.doc.observer().take_patches();
 | 
			
		||||
        let callback = callback.dyn_into::<Function>().ok();
 | 
			
		||||
 | 
			
		||||
        // even if there are no patches we may need to update the meta object
 | 
			
		||||
        // which requires that we update the object too
 | 
			
		||||
        if patches.is_empty() && !meta.is_undefined() {
 | 
			
		||||
            let (obj, datatype, id) = self.unwrap_object(&object)?;
 | 
			
		||||
            object = Object::assign(&Object::new(), &obj);
 | 
			
		||||
            object = self.wrap_object(object, datatype, &id, &meta)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for p in patches {
 | 
			
		||||
            if let Some(c) = &callback {
 | 
			
		||||
                let before = object.clone();
 | 
			
		||||
                object = self.apply_patch(object, &p, 0, &meta)?;
 | 
			
		||||
                c.call3(&JsValue::undefined(), &p.try_into()?, &before, &object)?;
 | 
			
		||||
            } else {
 | 
			
		||||
                object = self.apply_patch(object, &p, 0, &meta)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(object.into())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = popPatches)]
 | 
			
		||||
    pub fn pop_patches(&mut self) -> Result<Array, JsValue> {
 | 
			
		||||
        // transactions send out observer updates as they occur, not waiting for them to be
 | 
			
		||||
        // committed.
 | 
			
		||||
        // If we pop the patches then we won't be able to revert them.
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
 | 
			
		||||
        let patches = self
 | 
			
		||||
            .observer
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .map_or_else(Vec::new, |o| o.take_patches());
 | 
			
		||||
        let patches = self.doc.observer().take_patches();
 | 
			
		||||
        let result = Array::new();
 | 
			
		||||
        for p in patches {
 | 
			
		||||
            let patch = Object::new();
 | 
			
		||||
            match p {
 | 
			
		||||
                Patch::Put {
 | 
			
		||||
                    obj,
 | 
			
		||||
                    key,
 | 
			
		||||
                    value,
 | 
			
		||||
                    conflict,
 | 
			
		||||
                } => {
 | 
			
		||||
                    js_set(&patch, "action", "put")?;
 | 
			
		||||
                    js_set(&patch, "obj", obj.to_string())?;
 | 
			
		||||
                    js_set(&patch, "key", key)?;
 | 
			
		||||
                    match value {
 | 
			
		||||
                        (Value::Object(obj_type), obj_id) => {
 | 
			
		||||
                            js_set(&patch, "datatype", obj_type.to_string())?;
 | 
			
		||||
                            js_set(&patch, "value", obj_id.to_string())?;
 | 
			
		||||
                        }
 | 
			
		||||
                        (Value::Scalar(value), _) => {
 | 
			
		||||
                            js_set(&patch, "datatype", datatype(&value))?;
 | 
			
		||||
                            js_set(&patch, "value", ScalarValue(value))?;
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
                    js_set(&patch, "conflict", conflict)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Patch::Insert { obj, index, value } => {
 | 
			
		||||
                    js_set(&patch, "action", "insert")?;
 | 
			
		||||
                    js_set(&patch, "obj", obj.to_string())?;
 | 
			
		||||
                    js_set(&patch, "key", index as f64)?;
 | 
			
		||||
                    match value {
 | 
			
		||||
                        (Value::Object(obj_type), obj_id) => {
 | 
			
		||||
                            js_set(&patch, "datatype", obj_type.to_string())?;
 | 
			
		||||
                            js_set(&patch, "value", obj_id.to_string())?;
 | 
			
		||||
                        }
 | 
			
		||||
                        (Value::Scalar(value), _) => {
 | 
			
		||||
                            js_set(&patch, "datatype", datatype(&value))?;
 | 
			
		||||
                            js_set(&patch, "value", ScalarValue(value))?;
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Patch::Increment { obj, key, value } => {
 | 
			
		||||
                    js_set(&patch, "action", "increment")?;
 | 
			
		||||
                    js_set(&patch, "obj", obj.to_string())?;
 | 
			
		||||
                    js_set(&patch, "key", key)?;
 | 
			
		||||
                    js_set(&patch, "value", value.0)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Patch::Delete { obj, key } => {
 | 
			
		||||
                    js_set(&patch, "action", "delete")?;
 | 
			
		||||
                    js_set(&patch, "obj", obj.to_string())?;
 | 
			
		||||
                    js_set(&patch, "key", key)?;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            result.push(&patch);
 | 
			
		||||
            result.push(&p.try_into()?);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -553,51 +517,31 @@ impl Automerge {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn save(&mut self) -> Uint8Array {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        Uint8Array::from(self.doc.save().as_slice())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = saveIncremental)]
 | 
			
		||||
    pub fn save_incremental(&mut self) -> Uint8Array {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let bytes = self.doc.save_incremental();
 | 
			
		||||
        Uint8Array::from(bytes.as_slice())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = loadIncremental)]
 | 
			
		||||
    pub fn load_incremental(&mut self, data: Uint8Array) -> Result<f64, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let data = data.to_vec();
 | 
			
		||||
        let options = if let Some(observer) = self.observer.as_mut() {
 | 
			
		||||
            ApplyOptions::default().with_op_observer(observer)
 | 
			
		||||
        } else {
 | 
			
		||||
            ApplyOptions::default()
 | 
			
		||||
        };
 | 
			
		||||
        let len = self
 | 
			
		||||
            .doc
 | 
			
		||||
            .load_incremental_with(&data, options)
 | 
			
		||||
            .map_err(to_js_err)?;
 | 
			
		||||
        let len = self.doc.load_incremental(&data).map_err(to_js_err)?;
 | 
			
		||||
        Ok(len as f64)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = applyChanges)]
 | 
			
		||||
    pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let changes: Vec<_> = JS(changes).try_into()?;
 | 
			
		||||
        let options = if let Some(observer) = self.observer.as_mut() {
 | 
			
		||||
            ApplyOptions::default().with_op_observer(observer)
 | 
			
		||||
        } else {
 | 
			
		||||
            ApplyOptions::default()
 | 
			
		||||
        };
 | 
			
		||||
        self.doc
 | 
			
		||||
            .apply_changes_with(changes, options)
 | 
			
		||||
            .map_err(to_js_err)?;
 | 
			
		||||
        self.doc.apply_changes(changes).map_err(to_js_err)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = getChanges)]
 | 
			
		||||
    pub fn get_changes(&mut self, have_deps: JsValue) -> Result<Array, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let deps: Vec<_> = JS(have_deps).try_into()?;
 | 
			
		||||
        let changes = self.doc.get_changes(&deps)?;
 | 
			
		||||
        let changes: Array = changes
 | 
			
		||||
| 
						 | 
				
			
			@ -609,7 +553,6 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    #[wasm_bindgen(js_name = getChangeByHash)]
 | 
			
		||||
    pub fn get_change_by_hash(&mut self, hash: JsValue) -> Result<JsValue, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let hash = serde_wasm_bindgen::from_value(hash).map_err(to_js_err)?;
 | 
			
		||||
        let change = self.doc.get_change_by_hash(&hash);
 | 
			
		||||
        if let Some(c) = change {
 | 
			
		||||
| 
						 | 
				
			
			@ -621,7 +564,6 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    #[wasm_bindgen(js_name = getChangesAdded)]
 | 
			
		||||
    pub fn get_changes_added(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let changes = self.doc.get_changes_added(&mut other.doc);
 | 
			
		||||
        let changes: Array = changes
 | 
			
		||||
            .iter()
 | 
			
		||||
| 
						 | 
				
			
			@ -632,7 +574,6 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    #[wasm_bindgen(js_name = getHeads)]
 | 
			
		||||
    pub fn get_heads(&mut self) -> Array {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let heads = self.doc.get_heads();
 | 
			
		||||
        let heads: Array = heads
 | 
			
		||||
            .iter()
 | 
			
		||||
| 
						 | 
				
			
			@ -649,7 +590,6 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    #[wasm_bindgen(js_name = getLastLocalChange)]
 | 
			
		||||
    pub fn get_last_local_change(&mut self) -> Result<JsValue, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        if let Some(change) = self.doc.get_last_local_change() {
 | 
			
		||||
            Ok(Uint8Array::from(change.raw_bytes()).into())
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -658,13 +598,11 @@ impl Automerge {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn dump(&mut self) {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.doc.dump()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = getMissingDeps)]
 | 
			
		||||
    pub fn get_missing_deps(&mut self, heads: Option<Array>) -> Result<Array, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let heads = get_heads(heads).unwrap_or_default();
 | 
			
		||||
        let deps = self.doc.get_missing_deps(&heads);
 | 
			
		||||
        let deps: Array = deps
 | 
			
		||||
| 
						 | 
				
			
			@ -680,23 +618,16 @@ impl Automerge {
 | 
			
		|||
        state: &mut SyncState,
 | 
			
		||||
        message: Uint8Array,
 | 
			
		||||
    ) -> Result<(), JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        let message = message.to_vec();
 | 
			
		||||
        let message = am::sync::Message::decode(message.as_slice()).map_err(to_js_err)?;
 | 
			
		||||
        let options = if let Some(observer) = self.observer.as_mut() {
 | 
			
		||||
            ApplyOptions::default().with_op_observer(observer)
 | 
			
		||||
        } else {
 | 
			
		||||
            ApplyOptions::default()
 | 
			
		||||
        };
 | 
			
		||||
        self.doc
 | 
			
		||||
            .receive_sync_message_with(&mut state.0, message, options)
 | 
			
		||||
            .receive_sync_message(&mut state.0, message)
 | 
			
		||||
            .map_err(to_js_err)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = generateSyncMessage)]
 | 
			
		||||
    pub fn generate_sync_message(&mut self, state: &mut SyncState) -> Result<JsValue, JsValue> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        if let Some(message) = self.doc.generate_sync_message(&mut state.0) {
 | 
			
		||||
            Ok(Uint8Array::from(message.encode().as_slice()).into())
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -705,30 +636,24 @@ impl Automerge {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(js_name = toJS)]
 | 
			
		||||
    pub fn to_js(&self) -> JsValue {
 | 
			
		||||
        map_to_js(&self.doc, &ROOT)
 | 
			
		||||
    pub fn to_js(&self, meta: JsValue) -> Result<JsValue, JsValue> {
 | 
			
		||||
        self.export_object(&ROOT, Datatype::Map, None, &meta)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn materialize(&self, obj: JsValue, heads: Option<Array>) -> Result<JsValue, JsValue> {
 | 
			
		||||
    pub fn materialize(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        obj: JsValue,
 | 
			
		||||
        heads: Option<Array>,
 | 
			
		||||
        meta: JsValue,
 | 
			
		||||
    ) -> Result<JsValue, JsValue> {
 | 
			
		||||
        let obj = self.import(obj).unwrap_or(ROOT);
 | 
			
		||||
        let heads = get_heads(heads);
 | 
			
		||||
        if let Some(heads) = heads {
 | 
			
		||||
            match self.doc.object_type(&obj) {
 | 
			
		||||
                Some(am::ObjType::Map) => Ok(map_to_js_at(&self.doc, &obj, heads.as_slice())),
 | 
			
		||||
                Some(am::ObjType::List) => Ok(list_to_js_at(&self.doc, &obj, heads.as_slice())),
 | 
			
		||||
                Some(am::ObjType::Text) => Ok(self.doc.text_at(&obj, heads.as_slice())?.into()),
 | 
			
		||||
                Some(am::ObjType::Table) => Ok(map_to_js_at(&self.doc, &obj, heads.as_slice())),
 | 
			
		||||
                None => Err(to_js_err(format!("invalid obj {}", obj))),
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            match self.doc.object_type(&obj) {
 | 
			
		||||
                Some(am::ObjType::Map) => Ok(map_to_js(&self.doc, &obj)),
 | 
			
		||||
                Some(am::ObjType::List) => Ok(list_to_js(&self.doc, &obj)),
 | 
			
		||||
                Some(am::ObjType::Text) => Ok(self.doc.text(&obj)?.into()),
 | 
			
		||||
                Some(am::ObjType::Table) => Ok(map_to_js(&self.doc, &obj)),
 | 
			
		||||
                None => Err(to_js_err(format!("invalid obj {}", obj))),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        let obj_type = self
 | 
			
		||||
            .doc
 | 
			
		||||
            .object_type(&obj)
 | 
			
		||||
            .ok_or_else(|| to_js_err(format!("invalid obj {}", obj)))?;
 | 
			
		||||
        let _patches = self.doc.observer().take_patches(); // throw away patches
 | 
			
		||||
        self.export_object(&obj, obj_type.into(), heads.as_ref(), &meta)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn import(&self, id: JsValue) -> Result<ObjId, JsValue> {
 | 
			
		||||
| 
						 | 
				
			
			@ -747,11 +672,11 @@ impl Automerge {
 | 
			
		|||
                        self.doc.get(obj, am::Prop::Seq(prop.parse().unwrap()))?
 | 
			
		||||
                    };
 | 
			
		||||
                    match val {
 | 
			
		||||
                        Some((am::Value::Object(am::ObjType::Map), id)) => {
 | 
			
		||||
                        Some((am::Value::Object(ObjType::Map), id)) => {
 | 
			
		||||
                            is_map = true;
 | 
			
		||||
                            obj = id;
 | 
			
		||||
                        }
 | 
			
		||||
                        Some((am::Value::Object(am::ObjType::Table), id)) => {
 | 
			
		||||
                        Some((am::Value::Object(ObjType::Table), id)) => {
 | 
			
		||||
                            is_map = true;
 | 
			
		||||
                            obj = id;
 | 
			
		||||
                        }
 | 
			
		||||
| 
						 | 
				
			
			@ -856,16 +781,16 @@ pub fn init(actor: Option<String>) -> Result<Automerge, JsValue> {
 | 
			
		|||
#[wasm_bindgen(js_name = load)]
 | 
			
		||||
pub fn load(data: Uint8Array, actor: Option<String>) -> Result<Automerge, JsValue> {
 | 
			
		||||
    let data = data.to_vec();
 | 
			
		||||
    let observer = None;
 | 
			
		||||
    let options = ApplyOptions::<()>::default();
 | 
			
		||||
    let mut automerge = am::AutoCommit::load_with(&data, options).map_err(to_js_err)?;
 | 
			
		||||
    let mut doc = am::AutoCommitWithObs::<UnObserved>::load(&data)
 | 
			
		||||
        .map_err(to_js_err)?
 | 
			
		||||
        .with_observer(Observer::default());
 | 
			
		||||
    if let Some(s) = actor {
 | 
			
		||||
        let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
 | 
			
		||||
        automerge.set_actor(actor);
 | 
			
		||||
        doc.set_actor(actor);
 | 
			
		||||
    }
 | 
			
		||||
    Ok(Automerge {
 | 
			
		||||
        doc: automerge,
 | 
			
		||||
        observer,
 | 
			
		||||
        doc,
 | 
			
		||||
        external_types: HashMap::default(),
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										313
									
								
								automerge-wasm/src/observer.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								automerge-wasm/src/observer.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,313 @@
 | 
			
		|||
#![allow(dead_code)]
 | 
			
		||||
 | 
			
		||||
use crate::interop::{alloc, js_set};
 | 
			
		||||
use automerge::{ObjId, OpObserver, Parents, Prop, Value};
 | 
			
		||||
use js_sys::{Array, Object};
 | 
			
		||||
use wasm_bindgen::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Default)]
 | 
			
		||||
pub(crate) struct Observer {
 | 
			
		||||
    enabled: bool,
 | 
			
		||||
    patches: Vec<Patch>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Observer {
 | 
			
		||||
    pub(crate) fn take_patches(&mut self) -> Vec<Patch> {
 | 
			
		||||
        std::mem::take(&mut self.patches)
 | 
			
		||||
    }
 | 
			
		||||
    pub(crate) fn enable(&mut self, enable: bool) {
 | 
			
		||||
        if self.enabled && !enable {
 | 
			
		||||
            self.patches.truncate(0)
 | 
			
		||||
        }
 | 
			
		||||
        self.enabled = enable;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub(crate) enum Patch {
 | 
			
		||||
    PutMap {
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        path: Vec<(ObjId, Prop)>,
 | 
			
		||||
        key: String,
 | 
			
		||||
        value: (Value<'static>, ObjId),
 | 
			
		||||
        conflict: bool,
 | 
			
		||||
    },
 | 
			
		||||
    PutSeq {
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        path: Vec<(ObjId, Prop)>,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        value: (Value<'static>, ObjId),
 | 
			
		||||
        conflict: bool,
 | 
			
		||||
    },
 | 
			
		||||
    Insert {
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        path: Vec<(ObjId, Prop)>,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        values: Vec<(Value<'static>, ObjId)>,
 | 
			
		||||
    },
 | 
			
		||||
    Increment {
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        path: Vec<(ObjId, Prop)>,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        value: i64,
 | 
			
		||||
    },
 | 
			
		||||
    DeleteMap {
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        path: Vec<(ObjId, Prop)>,
 | 
			
		||||
        key: String,
 | 
			
		||||
    },
 | 
			
		||||
    DeleteSeq {
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        path: Vec<(ObjId, Prop)>,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        length: usize,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl OpObserver for Observer {
 | 
			
		||||
    fn insert(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        mut parents: Parents<'_>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        tagged_value: (Value<'_>, ObjId),
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enabled {
 | 
			
		||||
            let value = (tagged_value.0.to_owned(), tagged_value.1);
 | 
			
		||||
            if let Some(Patch::Insert {
 | 
			
		||||
                obj: tail_obj,
 | 
			
		||||
                index: tail_index,
 | 
			
		||||
                values,
 | 
			
		||||
                ..
 | 
			
		||||
            }) = self.patches.last_mut()
 | 
			
		||||
            {
 | 
			
		||||
                if tail_obj == &obj && *tail_index + values.len() == index {
 | 
			
		||||
                    values.push(value);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            let path = parents.path();
 | 
			
		||||
            let patch = Patch::Insert {
 | 
			
		||||
                path,
 | 
			
		||||
                obj,
 | 
			
		||||
                index,
 | 
			
		||||
                values: vec![value],
 | 
			
		||||
            };
 | 
			
		||||
            self.patches.push(patch);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn put(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        mut parents: Parents<'_>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        tagged_value: (Value<'_>, ObjId),
 | 
			
		||||
        conflict: bool,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enabled {
 | 
			
		||||
            let path = parents.path();
 | 
			
		||||
            let value = (tagged_value.0.to_owned(), tagged_value.1);
 | 
			
		||||
            let patch = match prop {
 | 
			
		||||
                Prop::Map(key) => Patch::PutMap {
 | 
			
		||||
                    path,
 | 
			
		||||
                    obj,
 | 
			
		||||
                    key,
 | 
			
		||||
                    value,
 | 
			
		||||
                    conflict,
 | 
			
		||||
                },
 | 
			
		||||
                Prop::Seq(index) => Patch::PutSeq {
 | 
			
		||||
                    path,
 | 
			
		||||
                    obj,
 | 
			
		||||
                    index,
 | 
			
		||||
                    value,
 | 
			
		||||
                    conflict,
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
            self.patches.push(patch);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn increment(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        mut parents: Parents<'_>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        tagged_value: (i64, ObjId),
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enabled {
 | 
			
		||||
            let path = parents.path();
 | 
			
		||||
            let value = tagged_value.0;
 | 
			
		||||
            self.patches.push(Patch::Increment {
 | 
			
		||||
                path,
 | 
			
		||||
                obj,
 | 
			
		||||
                prop,
 | 
			
		||||
                value,
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn delete(&mut self, mut parents: Parents<'_>, obj: ObjId, prop: Prop) {
 | 
			
		||||
        if self.enabled {
 | 
			
		||||
            let path = parents.path();
 | 
			
		||||
            let patch = match prop {
 | 
			
		||||
                Prop::Map(key) => Patch::DeleteMap { path, obj, key },
 | 
			
		||||
                Prop::Seq(index) => Patch::DeleteSeq {
 | 
			
		||||
                    path,
 | 
			
		||||
                    obj,
 | 
			
		||||
                    index,
 | 
			
		||||
                    length: 1,
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
            self.patches.push(patch)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn merge(&mut self, other: &Self) {
 | 
			
		||||
        self.patches.extend_from_slice(other.patches.as_slice())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn branch(&self) -> Self {
 | 
			
		||||
        Observer {
 | 
			
		||||
            patches: vec![],
 | 
			
		||||
            enabled: self.enabled,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn prop_to_js(p: &Prop) -> JsValue {
 | 
			
		||||
    match p {
 | 
			
		||||
        Prop::Map(key) => JsValue::from_str(key),
 | 
			
		||||
        Prop::Seq(index) => JsValue::from_f64(*index as f64),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn export_path(path: &[(ObjId, Prop)], end: &Prop) -> Array {
 | 
			
		||||
    let result = Array::new();
 | 
			
		||||
    for p in path {
 | 
			
		||||
        result.push(&prop_to_js(&p.1));
 | 
			
		||||
    }
 | 
			
		||||
    result.push(&prop_to_js(end));
 | 
			
		||||
    result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Patch {
 | 
			
		||||
    pub(crate) fn path(&self) -> &[(ObjId, Prop)] {
 | 
			
		||||
        match &self {
 | 
			
		||||
            Self::PutMap { path, .. } => path.as_slice(),
 | 
			
		||||
            Self::PutSeq { path, .. } => path.as_slice(),
 | 
			
		||||
            Self::Increment { path, .. } => path.as_slice(),
 | 
			
		||||
            Self::Insert { path, .. } => path.as_slice(),
 | 
			
		||||
            Self::DeleteMap { path, .. } => path.as_slice(),
 | 
			
		||||
            Self::DeleteSeq { path, .. } => path.as_slice(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn obj(&self) -> &ObjId {
 | 
			
		||||
        match &self {
 | 
			
		||||
            Self::PutMap { obj, .. } => obj,
 | 
			
		||||
            Self::PutSeq { obj, .. } => obj,
 | 
			
		||||
            Self::Increment { obj, .. } => obj,
 | 
			
		||||
            Self::Insert { obj, .. } => obj,
 | 
			
		||||
            Self::DeleteMap { obj, .. } => obj,
 | 
			
		||||
            Self::DeleteSeq { obj, .. } => obj,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<Patch> for JsValue {
 | 
			
		||||
    type Error = JsValue;
 | 
			
		||||
 | 
			
		||||
    fn try_from(p: Patch) -> Result<Self, Self::Error> {
 | 
			
		||||
        let result = Object::new();
 | 
			
		||||
        match p {
 | 
			
		||||
            Patch::PutMap {
 | 
			
		||||
                path,
 | 
			
		||||
                key,
 | 
			
		||||
                value,
 | 
			
		||||
                conflict,
 | 
			
		||||
                ..
 | 
			
		||||
            } => {
 | 
			
		||||
                js_set(&result, "action", "put")?;
 | 
			
		||||
                js_set(
 | 
			
		||||
                    &result,
 | 
			
		||||
                    "path",
 | 
			
		||||
                    export_path(path.as_slice(), &Prop::Map(key)),
 | 
			
		||||
                )?;
 | 
			
		||||
                js_set(&result, "value", alloc(&value.0).1)?;
 | 
			
		||||
                js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
 | 
			
		||||
                Ok(result.into())
 | 
			
		||||
            }
 | 
			
		||||
            Patch::PutSeq {
 | 
			
		||||
                path,
 | 
			
		||||
                index,
 | 
			
		||||
                value,
 | 
			
		||||
                conflict,
 | 
			
		||||
                ..
 | 
			
		||||
            } => {
 | 
			
		||||
                js_set(&result, "action", "put")?;
 | 
			
		||||
                js_set(
 | 
			
		||||
                    &result,
 | 
			
		||||
                    "path",
 | 
			
		||||
                    export_path(path.as_slice(), &Prop::Seq(index)),
 | 
			
		||||
                )?;
 | 
			
		||||
                js_set(&result, "value", alloc(&value.0).1)?;
 | 
			
		||||
                js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
 | 
			
		||||
                Ok(result.into())
 | 
			
		||||
            }
 | 
			
		||||
            Patch::Insert {
 | 
			
		||||
                path,
 | 
			
		||||
                index,
 | 
			
		||||
                values,
 | 
			
		||||
                ..
 | 
			
		||||
            } => {
 | 
			
		||||
                js_set(&result, "action", "splice")?;
 | 
			
		||||
                js_set(
 | 
			
		||||
                    &result,
 | 
			
		||||
                    "path",
 | 
			
		||||
                    export_path(path.as_slice(), &Prop::Seq(index)),
 | 
			
		||||
                )?;
 | 
			
		||||
                js_set(
 | 
			
		||||
                    &result,
 | 
			
		||||
                    "values",
 | 
			
		||||
                    values.iter().map(|v| alloc(&v.0).1).collect::<Array>(),
 | 
			
		||||
                )?;
 | 
			
		||||
                Ok(result.into())
 | 
			
		||||
            }
 | 
			
		||||
            Patch::Increment {
 | 
			
		||||
                path, prop, value, ..
 | 
			
		||||
            } => {
 | 
			
		||||
                js_set(&result, "action", "inc")?;
 | 
			
		||||
                js_set(&result, "path", export_path(path.as_slice(), &prop))?;
 | 
			
		||||
                js_set(&result, "value", &JsValue::from_f64(value as f64))?;
 | 
			
		||||
                Ok(result.into())
 | 
			
		||||
            }
 | 
			
		||||
            Patch::DeleteMap { path, key, .. } => {
 | 
			
		||||
                js_set(&result, "action", "del")?;
 | 
			
		||||
                js_set(
 | 
			
		||||
                    &result,
 | 
			
		||||
                    "path",
 | 
			
		||||
                    export_path(path.as_slice(), &Prop::Map(key)),
 | 
			
		||||
                )?;
 | 
			
		||||
                Ok(result.into())
 | 
			
		||||
            }
 | 
			
		||||
            Patch::DeleteSeq {
 | 
			
		||||
                path,
 | 
			
		||||
                index,
 | 
			
		||||
                length,
 | 
			
		||||
                ..
 | 
			
		||||
            } => {
 | 
			
		||||
                js_set(&result, "action", "del")?;
 | 
			
		||||
                js_set(
 | 
			
		||||
                    &result,
 | 
			
		||||
                    "path",
 | 
			
		||||
                    export_path(path.as_slice(), &Prop::Seq(index)),
 | 
			
		||||
                )?;
 | 
			
		||||
                if length > 1 {
 | 
			
		||||
                    js_set(&result, "length", length)?;
 | 
			
		||||
                }
 | 
			
		||||
                Ok(result.into())
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,40 +1,151 @@
 | 
			
		|||
use std::borrow::Cow;
 | 
			
		||||
 | 
			
		||||
use automerge as am;
 | 
			
		||||
use js_sys::Uint8Array;
 | 
			
		||||
use crate::to_js_err;
 | 
			
		||||
use automerge::{ObjType, ScalarValue, Value};
 | 
			
		||||
use wasm_bindgen::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct ScalarValue<'a>(pub(crate) Cow<'a, am::ScalarValue>);
 | 
			
		||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
 | 
			
		||||
pub(crate) enum Datatype {
 | 
			
		||||
    Map,
 | 
			
		||||
    Table,
 | 
			
		||||
    List,
 | 
			
		||||
    Text,
 | 
			
		||||
    Bytes,
 | 
			
		||||
    Str,
 | 
			
		||||
    Int,
 | 
			
		||||
    Uint,
 | 
			
		||||
    F64,
 | 
			
		||||
    Counter,
 | 
			
		||||
    Timestamp,
 | 
			
		||||
    Boolean,
 | 
			
		||||
    Null,
 | 
			
		||||
    Unknown(u8),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> From<ScalarValue<'a>> for JsValue {
 | 
			
		||||
    fn from(val: ScalarValue<'a>) -> Self {
 | 
			
		||||
        match &*val.0 {
 | 
			
		||||
            am::ScalarValue::Bytes(v) => Uint8Array::from(v.as_slice()).into(),
 | 
			
		||||
            am::ScalarValue::Str(v) => v.to_string().into(),
 | 
			
		||||
            am::ScalarValue::Int(v) => (*v as f64).into(),
 | 
			
		||||
            am::ScalarValue::Uint(v) => (*v as f64).into(),
 | 
			
		||||
            am::ScalarValue::F64(v) => (*v).into(),
 | 
			
		||||
            am::ScalarValue::Counter(v) => (f64::from(v)).into(),
 | 
			
		||||
            am::ScalarValue::Timestamp(v) => js_sys::Date::new(&(*v as f64).into()).into(),
 | 
			
		||||
            am::ScalarValue::Boolean(v) => (*v).into(),
 | 
			
		||||
            am::ScalarValue::Null => JsValue::null(),
 | 
			
		||||
            am::ScalarValue::Unknown { bytes, .. } => Uint8Array::from(bytes.as_slice()).into(),
 | 
			
		||||
impl Datatype {
 | 
			
		||||
    pub(crate) fn is_sequence(&self) -> bool {
 | 
			
		||||
        matches!(self, Self::List | Self::Text)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn is_scalar(&self) -> bool {
 | 
			
		||||
        !matches!(self, Self::Map | Self::Table | Self::List | Self::Text)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&ObjType> for Datatype {
 | 
			
		||||
    fn from(o: &ObjType) -> Self {
 | 
			
		||||
        (*o).into()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<ObjType> for Datatype {
 | 
			
		||||
    fn from(o: ObjType) -> Self {
 | 
			
		||||
        match o {
 | 
			
		||||
            ObjType::Map => Self::Map,
 | 
			
		||||
            ObjType::List => Self::List,
 | 
			
		||||
            ObjType::Table => Self::Table,
 | 
			
		||||
            ObjType::Text => Self::Text,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn datatype(s: &am::ScalarValue) -> String {
 | 
			
		||||
    match s {
 | 
			
		||||
        am::ScalarValue::Bytes(_) => "bytes".into(),
 | 
			
		||||
        am::ScalarValue::Str(_) => "str".into(),
 | 
			
		||||
        am::ScalarValue::Int(_) => "int".into(),
 | 
			
		||||
        am::ScalarValue::Uint(_) => "uint".into(),
 | 
			
		||||
        am::ScalarValue::F64(_) => "f64".into(),
 | 
			
		||||
        am::ScalarValue::Counter(_) => "counter".into(),
 | 
			
		||||
        am::ScalarValue::Timestamp(_) => "timestamp".into(),
 | 
			
		||||
        am::ScalarValue::Boolean(_) => "boolean".into(),
 | 
			
		||||
        am::ScalarValue::Null => "null".into(),
 | 
			
		||||
        am::ScalarValue::Unknown { type_code, .. } => format!("unknown{}", type_code),
 | 
			
		||||
impl std::fmt::Display for Datatype {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
 | 
			
		||||
        write!(f, "{}", String::from(self.clone()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&ScalarValue> for Datatype {
 | 
			
		||||
    fn from(s: &ScalarValue) -> Self {
 | 
			
		||||
        match s {
 | 
			
		||||
            ScalarValue::Bytes(_) => Self::Bytes,
 | 
			
		||||
            ScalarValue::Str(_) => Self::Str,
 | 
			
		||||
            ScalarValue::Int(_) => Self::Int,
 | 
			
		||||
            ScalarValue::Uint(_) => Self::Uint,
 | 
			
		||||
            ScalarValue::F64(_) => Self::F64,
 | 
			
		||||
            ScalarValue::Counter(_) => Self::Counter,
 | 
			
		||||
            ScalarValue::Timestamp(_) => Self::Timestamp,
 | 
			
		||||
            ScalarValue::Boolean(_) => Self::Boolean,
 | 
			
		||||
            ScalarValue::Null => Self::Null,
 | 
			
		||||
            ScalarValue::Unknown { type_code, .. } => Self::Unknown(*type_code),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<&Value<'_>> for Datatype {
 | 
			
		||||
    fn from(v: &Value<'_>) -> Self {
 | 
			
		||||
        match v {
 | 
			
		||||
            Value::Object(o) => o.into(),
 | 
			
		||||
            Value::Scalar(s) => s.as_ref().into(),
 | 
			
		||||
            /*
 | 
			
		||||
                            ScalarValue::Bytes(_) => Self::Bytes,
 | 
			
		||||
                            ScalarValue::Str(_) => Self::Str,
 | 
			
		||||
                            ScalarValue::Int(_) => Self::Int,
 | 
			
		||||
                            ScalarValue::Uint(_) => Self::Uint,
 | 
			
		||||
                            ScalarValue::F64(_) => Self::F64,
 | 
			
		||||
                            ScalarValue::Counter(_) => Self::Counter,
 | 
			
		||||
                            ScalarValue::Timestamp(_) => Self::Timestamp,
 | 
			
		||||
                            ScalarValue::Boolean(_) => Self::Boolean,
 | 
			
		||||
                            ScalarValue::Null => Self::Null,
 | 
			
		||||
                            ScalarValue::Unknown { type_code, .. } => Self::Unknown(*type_code),
 | 
			
		||||
            */
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Datatype> for String {
 | 
			
		||||
    fn from(d: Datatype) -> Self {
 | 
			
		||||
        match d {
 | 
			
		||||
            Datatype::Map => "map".into(),
 | 
			
		||||
            Datatype::Table => "table".into(),
 | 
			
		||||
            Datatype::List => "list".into(),
 | 
			
		||||
            Datatype::Text => "text".into(),
 | 
			
		||||
            Datatype::Bytes => "bytes".into(),
 | 
			
		||||
            Datatype::Str => "str".into(),
 | 
			
		||||
            Datatype::Int => "int".into(),
 | 
			
		||||
            Datatype::Uint => "uint".into(),
 | 
			
		||||
            Datatype::F64 => "f64".into(),
 | 
			
		||||
            Datatype::Counter => "counter".into(),
 | 
			
		||||
            Datatype::Timestamp => "timestamp".into(),
 | 
			
		||||
            Datatype::Boolean => "boolean".into(),
 | 
			
		||||
            Datatype::Null => "null".into(),
 | 
			
		||||
            Datatype::Unknown(type_code) => format!("unknown{}", type_code),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<JsValue> for Datatype {
 | 
			
		||||
    type Error = JsValue;
 | 
			
		||||
 | 
			
		||||
    fn try_from(datatype: JsValue) -> Result<Self, Self::Error> {
 | 
			
		||||
        let datatype = datatype
 | 
			
		||||
            .as_string()
 | 
			
		||||
            .ok_or_else(|| to_js_err("datatype is not a string"))?;
 | 
			
		||||
        match datatype.as_str() {
 | 
			
		||||
            "map" => Ok(Datatype::Map),
 | 
			
		||||
            "table" => Ok(Datatype::Table),
 | 
			
		||||
            "list" => Ok(Datatype::List),
 | 
			
		||||
            "text" => Ok(Datatype::Text),
 | 
			
		||||
            "bytes" => Ok(Datatype::Bytes),
 | 
			
		||||
            "str" => Ok(Datatype::Str),
 | 
			
		||||
            "int" => Ok(Datatype::Int),
 | 
			
		||||
            "uint" => Ok(Datatype::Uint),
 | 
			
		||||
            "f64" => Ok(Datatype::F64),
 | 
			
		||||
            "counter" => Ok(Datatype::Counter),
 | 
			
		||||
            "timestamp" => Ok(Datatype::Timestamp),
 | 
			
		||||
            "boolean" => Ok(Datatype::Boolean),
 | 
			
		||||
            "null" => Ok(Datatype::Null),
 | 
			
		||||
            d => {
 | 
			
		||||
                if d.starts_with("unknown") {
 | 
			
		||||
                    todo!() // handle "unknown{}",
 | 
			
		||||
                } else {
 | 
			
		||||
                    Err(to_js_err(format!("unknown datatype {}", d)))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Datatype> for JsValue {
 | 
			
		||||
    fn from(d: Datatype) -> Self {
 | 
			
		||||
        String::from(d).into()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										192
									
								
								automerge-wasm/test/apply.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								automerge-wasm/test/apply.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,192 @@
 | 
			
		|||
 | 
			
		||||
import { describe, it } from 'mocha';
 | 
			
		||||
import assert from 'assert'
 | 
			
		||||
import { create, Value } from '..'
 | 
			
		||||
 | 
			
		||||
export const OBJECT_ID  = Symbol.for('_am_objectId')     // object containing metadata about current 
 | 
			
		||||
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
function _obj(doc: any) : any {
 | 
			
		||||
  if (typeof doc === 'object' && doc !== null) {
 | 
			
		||||
    return doc[OBJECT_ID]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sample classes for testing
 | 
			
		||||
class Counter {
 | 
			
		||||
  value: number;
 | 
			
		||||
  constructor(n: number) {
 | 
			
		||||
    this.value = n
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('Automerge', () => {
 | 
			
		||||
  describe('Patch Apply', () => {
 | 
			
		||||
    it('apply nested sets on maps', () => {
 | 
			
		||||
      const start = { hello: { mellow: { yellow: "world", x: 1 }, y : 2 } }
 | 
			
		||||
      const doc1 = create()
 | 
			
		||||
      doc1.putObject("/", "hello", start.hello);
 | 
			
		||||
      let mat = doc1.materialize("/")
 | 
			
		||||
      const doc2 = create()
 | 
			
		||||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.merge(doc1)
 | 
			
		||||
 | 
			
		||||
      let base = doc2.applyPatches({})
 | 
			
		||||
      assert.deepEqual(mat, start)
 | 
			
		||||
      assert.deepEqual(base, start)
 | 
			
		||||
 | 
			
		||||
      doc2.delete("/hello/mellow", "yellow");
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      delete start.hello.mellow.yellow;
 | 
			
		||||
      base = doc2.applyPatches(base)
 | 
			
		||||
      mat = doc2.materialize("/")
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(mat, start)
 | 
			
		||||
      assert.deepEqual(base, start)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('apply patches on lists', () => {
 | 
			
		||||
      const start = { list: [1,2,3,4] }
 | 
			
		||||
      const doc1 = create()
 | 
			
		||||
      doc1.putObject("/", "list", start.list);
 | 
			
		||||
      let mat = doc1.materialize("/")
 | 
			
		||||
      const doc2 = create()
 | 
			
		||||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.merge(doc1)
 | 
			
		||||
      mat = doc1.materialize("/")
 | 
			
		||||
      let base = doc2.applyPatches({})
 | 
			
		||||
      assert.deepEqual(mat, start)
 | 
			
		||||
      assert.deepEqual(base, start)
 | 
			
		||||
 | 
			
		||||
      doc2.delete("/list", 3);
 | 
			
		||||
      start.list.splice(3,1)
 | 
			
		||||
      base = doc2.applyPatches(base)
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(base, start)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('apply patches on lists of lists of lists', () => {
 | 
			
		||||
      const start = { list:
 | 
			
		||||
        [
 | 
			
		||||
          [
 | 
			
		||||
            [ 1, 2, 3, 4, 5, 6],
 | 
			
		||||
            [ 7, 8, 9,10,11,12],
 | 
			
		||||
          ],
 | 
			
		||||
          [
 | 
			
		||||
            [ 7, 8, 9,10,11,12],
 | 
			
		||||
            [ 1, 2, 3, 4, 5, 6],
 | 
			
		||||
          ]
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
      const doc1 = create()
 | 
			
		||||
      doc1.enablePatches(true)
 | 
			
		||||
      doc1.putObject("/", "list", start.list);
 | 
			
		||||
      let base = doc1.applyPatches({})
 | 
			
		||||
      let mat = doc1.clone().materialize("/")
 | 
			
		||||
      assert.deepEqual(mat, start)
 | 
			
		||||
      assert.deepEqual(base, start)
 | 
			
		||||
 | 
			
		||||
      doc1.delete("/list/0/1", 3)
 | 
			
		||||
      start.list[0][1].splice(3,1)
 | 
			
		||||
 | 
			
		||||
      doc1.delete("/list/0", 0)
 | 
			
		||||
      start.list[0].splice(0,1)
 | 
			
		||||
 | 
			
		||||
      mat = doc1.clone().materialize("/")
 | 
			
		||||
      base = doc1.applyPatches(base)
 | 
			
		||||
      assert.deepEqual(mat, start)
 | 
			
		||||
      assert.deepEqual(base, start)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('large inserts should make one splice patch', () => {
 | 
			
		||||
      const doc1 = create()
 | 
			
		||||
      doc1.enablePatches(true)
 | 
			
		||||
      doc1.putObject("/", "list", "abc");
 | 
			
		||||
      const patches = doc1.popPatches()
 | 
			
		||||
      assert.deepEqual( patches, [
 | 
			
		||||
        { action: 'put', conflict: false, path: [ 'list' ], value: [] },
 | 
			
		||||
        { action: 'splice', path: [ 'list', 0 ], values: [ 'a', 'b', 'c' ] }])
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('it should allow registering type wrappers', () => {
 | 
			
		||||
      const doc1 = create()
 | 
			
		||||
      doc1.enablePatches(true)
 | 
			
		||||
      doc1.registerDatatype("counter", (n: number) => new Counter(n))
 | 
			
		||||
      const doc2 = doc1.fork()
 | 
			
		||||
      doc1.put("/", "n", 10, "counter")
 | 
			
		||||
      doc1.put("/", "m", 10, "int")
 | 
			
		||||
 | 
			
		||||
      let mat = doc1.materialize("/")
 | 
			
		||||
      assert.deepEqual( mat, { n: new Counter(10), m: 10 } )
 | 
			
		||||
 | 
			
		||||
      doc2.merge(doc1)
 | 
			
		||||
      let apply = doc2.applyPatches({})
 | 
			
		||||
      assert.deepEqual( apply, { n: new Counter(10), m: 10 } )
 | 
			
		||||
 | 
			
		||||
      doc1.increment("/","n", 5)
 | 
			
		||||
      mat = doc1.materialize("/")
 | 
			
		||||
      assert.deepEqual( mat, { n: new Counter(15), m: 10 } )
 | 
			
		||||
 | 
			
		||||
      doc2.merge(doc1)
 | 
			
		||||
      apply = doc2.applyPatches(apply)
 | 
			
		||||
      assert.deepEqual( apply, { n: new Counter(15), m: 10 } )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('text can be managed as an array or a string', () => {
 | 
			
		||||
      const doc1 = create("aaaa")
 | 
			
		||||
      doc1.enablePatches(true)
 | 
			
		||||
 | 
			
		||||
      doc1.putObject("/", "notes", "hello world")
 | 
			
		||||
 | 
			
		||||
      let mat = doc1.materialize("/")
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual( mat, { notes: "hello world".split("") } )
 | 
			
		||||
 | 
			
		||||
      const doc2 = create()
 | 
			
		||||
      let apply : any = doc2.materialize("/") 
 | 
			
		||||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.registerDatatype("text", (n: Value[]) => new String(n.join("")))
 | 
			
		||||
      apply = doc2.applyPatches(apply)
 | 
			
		||||
 | 
			
		||||
      doc2.merge(doc1);
 | 
			
		||||
      apply = doc2.applyPatches(apply)
 | 
			
		||||
      assert.deepEqual(_obj(apply), "_root")
 | 
			
		||||
      assert.deepEqual(_obj(apply['notes']), "1@aaaa")
 | 
			
		||||
      assert.deepEqual( apply, { notes: new String("hello world") } )
 | 
			
		||||
 | 
			
		||||
      doc2.splice("/notes", 6, 5, "everyone");
 | 
			
		||||
      apply = doc2.applyPatches(apply)
 | 
			
		||||
      assert.deepEqual( apply, { notes: new String("hello everyone") } )
 | 
			
		||||
 | 
			
		||||
      mat = doc2.materialize("/")
 | 
			
		||||
      assert.deepEqual(_obj(mat), "_root")
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      assert.deepEqual(_obj(mat.notes), "1@aaaa")
 | 
			
		||||
      assert.deepEqual( mat, { notes: new String("hello everyone") } )
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it.skip('it can patch quickly', () => {
 | 
			
		||||
/*
 | 
			
		||||
      console.time("init")
 | 
			
		||||
      let doc1 = create()
 | 
			
		||||
      doc1.enablePatches(true)
 | 
			
		||||
      doc1.putObject("/", "notes", "");
 | 
			
		||||
      let mat = doc1.materialize("/")
 | 
			
		||||
      let doc2 = doc1.fork()
 | 
			
		||||
      let testData = new Array( 100000 ).join("x")
 | 
			
		||||
      console.timeEnd("init")
 | 
			
		||||
      console.time("splice")
 | 
			
		||||
      doc2.splice("/notes", 0, 0, testData);
 | 
			
		||||
      console.timeEnd("splice")
 | 
			
		||||
      console.time("merge")
 | 
			
		||||
      doc1.merge(doc2)
 | 
			
		||||
      console.timeEnd("merge")
 | 
			
		||||
      console.time("patch")
 | 
			
		||||
      mat = doc1.applyPatches(mat)
 | 
			
		||||
      console.timeEnd("patch")
 | 
			
		||||
*/
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// TODO: squash puts & deletes
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
			
		||||
import { describe, it } from 'mocha';
 | 
			
		||||
import * as assert from 'assert'
 | 
			
		||||
//@ts-ignore
 | 
			
		||||
import { create, load } from '..'
 | 
			
		||||
 | 
			
		||||
describe('Automerge', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -273,6 +273,6 @@ describe('Automerge', () => {
 | 
			
		|||
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free(); doc4.free()
 | 
			
		||||
    })
 | 
			
		||||
    it.skip('Syncing (1)', () => { })
 | 
			
		||||
    //it.skip('Syncing (1)', () => { })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,9 @@
 | 
			
		|||
import { describe, it } from 'mocha';
 | 
			
		||||
//@ts-ignore
 | 
			
		||||
import assert from 'assert'
 | 
			
		||||
//@ts-ignore
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { BloomFilter } from './helpers/sync'
 | 
			
		||||
import { create, load, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..'
 | 
			
		||||
import { DecodedSyncMessage, Hash } from '..';
 | 
			
		||||
import { create, load, SyncState, Automerge, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..'
 | 
			
		||||
import { Value, DecodedSyncMessage, Hash } from '..';
 | 
			
		||||
 | 
			
		||||
function sync(a: Automerge, b: Automerge, aSyncState = initSyncState(), bSyncState = initSyncState()) {
 | 
			
		||||
  const MAX_ITER = 10
 | 
			
		||||
| 
						 | 
				
			
			@ -311,7 +310,7 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.put("_root", "hello", "world")
 | 
			
		||||
      const doc2 = load(doc1.save(), "bbbb");
 | 
			
		||||
      const doc3 = load(doc1.save(), "cccc");
 | 
			
		||||
      let heads = doc1.getHeads()
 | 
			
		||||
      const heads = doc1.getHeads()
 | 
			
		||||
      doc1.put("_root", "cnt", 20)
 | 
			
		||||
      doc2.put("_root", "cnt", 0, "counter")
 | 
			
		||||
      doc3.put("_root", "cnt", 10, "counter")
 | 
			
		||||
| 
						 | 
				
			
			@ -345,7 +344,7 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.insert(seq, 0, "hello")
 | 
			
		||||
      const doc2 = load(doc1.save(), "bbbb");
 | 
			
		||||
      const doc3 = load(doc1.save(), "cccc");
 | 
			
		||||
      let heads = doc1.getHeads()
 | 
			
		||||
      const heads = doc1.getHeads()
 | 
			
		||||
      doc1.put(seq, 0, 20)
 | 
			
		||||
      doc2.put(seq, 0, 0, "counter")
 | 
			
		||||
      doc3.put(seq, 0, 10, "counter")
 | 
			
		||||
| 
						 | 
				
			
			@ -397,20 +396,21 @@ describe('Automerge', () => {
 | 
			
		|||
 | 
			
		||||
    it('recursive sets are possible', () => {
 | 
			
		||||
      const doc = create("aaaa")
 | 
			
		||||
      doc.registerDatatype("text", (n: Value[]) => new String(n.join("")))
 | 
			
		||||
      const l1 = doc.putObject("_root", "list", [{ foo: "bar" }, [1, 2, 3]])
 | 
			
		||||
      const l2 = doc.insertObject(l1, 0, { zip: ["a", "b"] })
 | 
			
		||||
      const l3 = doc.putObject("_root", "info1", "hello world") // 'text' object
 | 
			
		||||
      doc.putObject("_root", "info1", "hello world") // 'text' object
 | 
			
		||||
      doc.put("_root", "info2", "hello world")  // 'str'
 | 
			
		||||
      const l4 = doc.putObject("_root", "info3", "hello world")
 | 
			
		||||
      assert.deepEqual(doc.materialize(), {
 | 
			
		||||
        "list": [{ zip: ["a", "b"] }, { foo: "bar" }, [1, 2, 3]],
 | 
			
		||||
        "info1": "hello world",
 | 
			
		||||
        "info1": new String("hello world"),
 | 
			
		||||
        "info2": "hello world",
 | 
			
		||||
        "info3": "hello world",
 | 
			
		||||
        "info3": new String("hello world"),
 | 
			
		||||
      })
 | 
			
		||||
      assert.deepEqual(doc.materialize(l2), { zip: ["a", "b"] })
 | 
			
		||||
      assert.deepEqual(doc.materialize(l1), [{ zip: ["a", "b"] }, { foo: "bar" }, [1, 2, 3]])
 | 
			
		||||
      assert.deepEqual(doc.materialize(l4), "hello world")
 | 
			
		||||
      assert.deepEqual(doc.materialize(l4), new String("hello world"))
 | 
			
		||||
      doc.free()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -442,7 +442,7 @@ describe('Automerge', () => {
 | 
			
		|||
      const a = doc1.putObject("_root", "a", {});
 | 
			
		||||
      const b = doc1.putObject("_root", "b", {});
 | 
			
		||||
      const c = doc1.putObject("_root", "c", {});
 | 
			
		||||
      const d = doc1.put(c, "d", "dd");
 | 
			
		||||
                doc1.put(c, "d", "dd");
 | 
			
		||||
      const saved = doc1.save();
 | 
			
		||||
      const doc2 = load(saved);
 | 
			
		||||
      assert.deepEqual(doc2.getWithType("_root", "a"), ["map", a])
 | 
			
		||||
| 
						 | 
				
			
			@ -503,7 +503,7 @@ describe('Automerge', () => {
 | 
			
		|||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'hello', value: 'world', datatype: 'str', conflict: false }
 | 
			
		||||
        { action: 'put', path: ['hello'], value: 'world', conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
      doc2.free()
 | 
			
		||||
| 
						 | 
				
			
			@ -515,9 +515,9 @@ describe('Automerge', () => {
 | 
			
		|||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'birds', value: '1@aaaa', datatype: 'map', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 'friday', value: '2@aaaa', datatype: 'map', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '2@aaaa', key: 'robins', value: 3, datatype: 'int', conflict: false }
 | 
			
		||||
        { action: 'put', path: [ 'birds' ], value: {}, conflict: false },
 | 
			
		||||
        { action: 'put', path: [ 'birds', 'friday' ], value: {}, conflict: false },
 | 
			
		||||
        { action: 'put', path: [ 'birds', 'friday', 'robins' ], value: 3, conflict: false},
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
      doc2.free()
 | 
			
		||||
| 
						 | 
				
			
			@ -531,8 +531,8 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.delete('_root', 'favouriteBird')
 | 
			
		||||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'favouriteBird', value: 'Robin', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'delete', obj: '_root', key: 'favouriteBird' }
 | 
			
		||||
        { action: 'put', path: [ 'favouriteBird' ], value: 'Robin', conflict: false },
 | 
			
		||||
        { action: 'del', path: [ 'favouriteBird' ] }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
      doc2.free()
 | 
			
		||||
| 
						 | 
				
			
			@ -544,9 +544,8 @@ describe('Automerge', () => {
 | 
			
		|||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'birds', value: '1@aaaa', datatype: 'list', conflict: false },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 0, value: 'Goldfinch', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 1, value: 'Chaffinch', datatype: 'str' }
 | 
			
		||||
        { action: 'put', path: [ 'birds' ], value: [], conflict: false },
 | 
			
		||||
        { action: 'splice', path: [ 'birds', 0 ], values: ['Goldfinch', 'Chaffinch'] },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
      doc2.free()
 | 
			
		||||
| 
						 | 
				
			
			@ -560,9 +559,9 @@ describe('Automerge', () => {
 | 
			
		|||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 0, value: '2@aaaa', datatype: 'map' },
 | 
			
		||||
        { action: 'put', obj: '2@aaaa', key: 'species', value: 'Goldfinch', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '2@aaaa', key: 'count', value: 3, datatype: 'int', conflict: false }
 | 
			
		||||
        { action: 'splice', path: [ 'birds', 0 ], values: [{}] },
 | 
			
		||||
        { action: 'put', path: [ 'birds', 0, 'species' ], value: 'Goldfinch', conflict: false },
 | 
			
		||||
        { action: 'put', path: [ 'birds', 0, 'count', ], value: 3, conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
      doc2.free()
 | 
			
		||||
| 
						 | 
				
			
			@ -579,8 +578,8 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepEqual(doc1.getWithType('1@aaaa', 0), ['str', 'Chaffinch'])
 | 
			
		||||
      assert.deepEqual(doc1.getWithType('1@aaaa', 1), ['str', 'Greenfinch'])
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'delete', obj: '1@aaaa', key: 0 },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 1, value: 'Greenfinch', datatype: 'str' }
 | 
			
		||||
        { action: 'del', path: ['birds', 0] },
 | 
			
		||||
        { action: 'splice', path: ['birds', 1], values: ['Greenfinch'] }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
      doc2.free()
 | 
			
		||||
| 
						 | 
				
			
			@ -605,16 +604,11 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepEqual([0, 1, 2, 3].map(i => (doc3.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd'])
 | 
			
		||||
      assert.deepEqual([0, 1, 2, 3].map(i => (doc4.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd'])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 0, value: 'c', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 1, value: 'd', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 0, value: 'a', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 1, value: 'b', datatype: 'str' }
 | 
			
		||||
        { action: 'splice', path: ['values', 0], values:['c','d'] },
 | 
			
		||||
        { action: 'splice', path: ['values', 0], values:['a','b'] },
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc4.popPatches(), [
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 0, value: 'a', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 1, value: 'b', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 2, value: 'c', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 3, value: 'd', datatype: 'str' }
 | 
			
		||||
        { action: 'splice', path: ['values',0], values:['a','b','c','d'] },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free(); doc4.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -638,16 +632,11 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepEqual([0, 1, 2, 3, 4, 5].map(i => (doc3.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd', 'e', 'f'])
 | 
			
		||||
      assert.deepEqual([0, 1, 2, 3, 4, 5].map(i => (doc4.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd', 'e', 'f'])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 2, value: 'e', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 3, value: 'f', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 2, value: 'c', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 3, value: 'd', datatype: 'str' }
 | 
			
		||||
        { action: 'splice', path: ['values', 2], values: ['e','f'] },
 | 
			
		||||
        { action: 'splice', path: ['values', 2], values: ['c','d'] },
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc4.popPatches(), [
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 2, value: 'c', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 3, value: 'd', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 4, value: 'e', datatype: 'str' },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 5, value: 'f', datatype: 'str' }
 | 
			
		||||
        { action: 'splice', path: ['values', 2], values: ['c','d','e','f'] },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free(); doc4.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -666,12 +655,12 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepEqual(doc4.getWithType('_root', 'bird'), ['str', 'Goldfinch'])
 | 
			
		||||
      assert.deepEqual(doc4.getAll('_root', 'bird'), [['str', 'Greenfinch', '1@aaaa'], ['str', 'Goldfinch', '1@bbbb']])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Greenfinch', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Greenfinch', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc4.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free(); doc4.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -701,16 +690,16 @@ describe('Automerge', () => {
 | 
			
		|||
        ['str', 'Greenfinch', '1@aaaa'], ['str', 'Chaffinch', '1@bbbb'], ['str', 'Goldfinch', '1@cccc']
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Chaffinch', datatype: 'str', conflict: true },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Chaffinch', conflict: true },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -727,9 +716,9 @@ describe('Automerge', () => {
 | 
			
		|||
      doc3.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc3.getAll('_root', 'bird'), [['str', 'Goldfinch', '2@aaaa']])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Greenfinch', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Chaffinch', datatype: 'str', conflict: true },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Greenfinch', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Chaffinch', conflict: true },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -750,10 +739,10 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepEqual(doc2.getWithType('_root', 'bird'), ['str', 'Goldfinch'])
 | 
			
		||||
      assert.deepEqual(doc2.getAll('_root', 'bird'), [['str', 'Goldfinch', '2@aaaa']])
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -777,12 +766,12 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepEqual(doc4.getWithType('1@aaaa', 0), ['str', 'Redwing'])
 | 
			
		||||
      assert.deepEqual(doc4.getAll('1@aaaa', 0), [['str', 'Song Thrush', '4@aaaa'], ['str', 'Redwing', '4@bbbb']])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 0, value: 'Song Thrush', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 0, value: 'Redwing', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['birds',0], value: 'Song Thrush', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['birds',0], value: 'Redwing', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc4.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 0, value: 'Redwing', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 0, value: 'Redwing', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['birds',0], value: 'Redwing', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['birds',0], value: 'Redwing', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free(); doc4.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -808,16 +797,16 @@ describe('Automerge', () => {
 | 
			
		|||
      assert.deepEqual(doc4.getAll('1@aaaa', 0), [['str', 'Ring-necked parakeet', '5@bbbb']])
 | 
			
		||||
      assert.deepEqual(doc4.getAll('1@aaaa', 2), [['str', 'Song Thrush', '6@aaaa'], ['str', 'Redwing', '6@bbbb']])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'delete', obj: '1@aaaa', key: 0 },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 1, value: 'Song Thrush', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 0, value: 'Ring-necked parakeet', datatype: 'str' },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 2, value: 'Redwing', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'del', path: ['birds',0], },
 | 
			
		||||
        { action: 'put', path: ['birds',1], value: 'Song Thrush', conflict: false },
 | 
			
		||||
        { action: 'splice', path: ['birds',0], values: ['Ring-necked parakeet'] },
 | 
			
		||||
        { action: 'put', path: ['birds',2], value: 'Redwing', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc4.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 0, value: 'Ring-necked parakeet', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 2, value: 'Redwing', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 0, value: 'Ring-necked parakeet', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '1@aaaa', key: 2, value: 'Redwing', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['birds',0], value: 'Ring-necked parakeet', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['birds',2], value: 'Redwing', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['birds',0], value: 'Ring-necked parakeet', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['birds',2], value: 'Redwing', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free(); doc4.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -834,14 +823,14 @@ describe('Automerge', () => {
 | 
			
		|||
      doc3.loadIncremental(change2)
 | 
			
		||||
      assert.deepEqual(doc3.getAll('_root', 'bird'), [['str', 'Robin', '1@aaaa'], ['str', 'Wren', '1@bbbb']])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Robin', datatype: 'str', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Wren', datatype: 'str', conflict: true }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Robin', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Wren', conflict: true }
 | 
			
		||||
      ])
 | 
			
		||||
      doc3.loadIncremental(change3)
 | 
			
		||||
      assert.deepEqual(doc3.getWithType('_root', 'bird'), ['str', 'Robin'])
 | 
			
		||||
      assert.deepEqual(doc3.getAll('_root', 'bird'), [['str', 'Robin', '1@aaaa']])
 | 
			
		||||
      assert.deepEqual(doc3.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'bird', value: 'Robin', datatype: 'str', conflict: false }
 | 
			
		||||
        { action: 'put', path: ['bird'], value: 'Robin', conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free(); doc3.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -857,26 +846,25 @@ describe('Automerge', () => {
 | 
			
		|||
      doc2.loadIncremental(change1)
 | 
			
		||||
      assert.deepEqual(doc1.getAll('_root', 'birds'), [['list', '1@aaaa'], ['map', '1@bbbb']])
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'birds', value: '1@bbbb', datatype: 'map', conflict: true },
 | 
			
		||||
        { action: 'put', obj: '1@bbbb', key: 'Sparrowhawk', value: 1, datatype: 'int', conflict: false }
 | 
			
		||||
        { action: 'put', path: ['birds'], value: {}, conflict: true },
 | 
			
		||||
        { action: 'put', path: ['birds', 'Sparrowhawk'], value: 1, conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      assert.deepEqual(doc2.getAll('_root', 'birds'), [['list', '1@aaaa'], ['map', '1@bbbb']])
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'birds', value: '1@bbbb', datatype: 'map', conflict: true },
 | 
			
		||||
        { action: 'insert', obj: '1@aaaa', key: 0, value: 'Parakeet', datatype: 'str' }
 | 
			
		||||
        { action: 'put', path: ['birds'], value: {}, conflict: true },
 | 
			
		||||
        { action: 'splice', path: ['birds',0], values: ['Parakeet'] }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('should support date objects', () => {
 | 
			
		||||
      // FIXME: either use Date objects or use numbers consistently
 | 
			
		||||
      const doc1 = create('aaaa'), doc2 = create('bbbb'), now = new Date()
 | 
			
		||||
      doc1.put('_root', 'createdAt', now.getTime(), 'timestamp')
 | 
			
		||||
      doc1.put('_root', 'createdAt', now)
 | 
			
		||||
      doc2.enablePatches(true)
 | 
			
		||||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc2.getWithType('_root', 'createdAt'), ['timestamp', now])
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'createdAt', value: now, datatype: 'timestamp', conflict: false }
 | 
			
		||||
        { action: 'put', path: ['createdAt'], value: now, conflict: false }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -887,15 +875,15 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.put('_root', 'key1', 1)
 | 
			
		||||
      doc1.put('_root', 'key1', 2)
 | 
			
		||||
      doc1.put('_root', 'key2', 3)
 | 
			
		||||
      const map = doc1.putObject('_root', 'map', {})
 | 
			
		||||
      const list = doc1.putObject('_root', 'list', [])
 | 
			
		||||
      doc1.putObject('_root', 'map', {})
 | 
			
		||||
      doc1.putObject('_root', 'list', [])
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'key1', value: 1, datatype: 'int', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'key1', value: 2, datatype: 'int', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'key2', value: 3, datatype: 'int', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'map', value: map, datatype: 'map', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
 | 
			
		||||
        { action: 'put', path: ['key1'], value: 1, conflict: false },
 | 
			
		||||
        { action: 'put', path: ['key1'], value: 2, conflict: false },
 | 
			
		||||
        { action: 'put', path: ['key2'], value: 3, conflict: false },
 | 
			
		||||
        { action: 'put', path: ['map'], value: {}, conflict: false },
 | 
			
		||||
        { action: 'put', path: ['list'], value: [], conflict: false },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -907,16 +895,16 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.insert(list, 0, 1)
 | 
			
		||||
      doc1.insert(list, 0, 2)
 | 
			
		||||
      doc1.insert(list, 2, 3)
 | 
			
		||||
      const map = doc1.insertObject(list, 2, {})
 | 
			
		||||
      const list2 = doc1.insertObject(list, 2, [])
 | 
			
		||||
      doc1.insertObject(list, 2, {})
 | 
			
		||||
      doc1.insertObject(list, 2, [])
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
 | 
			
		||||
        { action: 'insert', obj: list, key: 0, value: 1, datatype: 'int' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 0, value: 2, datatype: 'int' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 2, value: 3, datatype: 'int' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 2, value: map, datatype: 'map' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 2, value: list2, datatype: 'list' },
 | 
			
		||||
        { action: 'put', path: ['list'], value: [], conflict: false },
 | 
			
		||||
        { action: 'splice', path: ['list', 0], values: [1]  },
 | 
			
		||||
        { action: 'splice', path: ['list', 0], values: [2] },
 | 
			
		||||
        { action: 'splice', path: ['list', 2], values: [3] },
 | 
			
		||||
        { action: 'splice', path: ['list', 2], values: [{}] },
 | 
			
		||||
        { action: 'splice', path: ['list', 2], values: [[]] },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -926,14 +914,12 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.enablePatches(true)
 | 
			
		||||
      const list = doc1.putObject('_root', 'list', [])
 | 
			
		||||
      doc1.push(list, 1)
 | 
			
		||||
      const map = doc1.pushObject(list, {})
 | 
			
		||||
      const list2 = doc1.pushObject(list, [])
 | 
			
		||||
      doc1.pushObject(list, {})
 | 
			
		||||
      doc1.pushObject(list, [])
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
 | 
			
		||||
        { action: 'insert', obj: list, key: 0, value: 1, datatype: 'int' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 1, value: map, datatype: 'map' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 2, value: list2, datatype: 'list' },
 | 
			
		||||
        { action: 'put', path: ['list'], value: [], conflict: false },
 | 
			
		||||
        { action: 'splice', path: ['list',0], values: [1,{},[]] },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -946,13 +932,10 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.splice(list, 1, 2)
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
 | 
			
		||||
        { action: 'insert', obj: list, key: 0, value: 1, datatype: 'int' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 1, value: 2, datatype: 'int' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 2, value: 3, datatype: 'int' },
 | 
			
		||||
        { action: 'insert', obj: list, key: 3, value: 4, datatype: 'int' },
 | 
			
		||||
        { action: 'delete', obj: list, key: 1 },
 | 
			
		||||
        { action: 'delete', obj: list, key: 1 },
 | 
			
		||||
        { action: 'put', path: ['list'],  value: [], conflict: false },
 | 
			
		||||
        { action: 'splice', path: ['list',0], values: [1,2,3,4] },
 | 
			
		||||
        { action: 'del', path: ['list',1] },
 | 
			
		||||
        { action: 'del', path: ['list',1] },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -964,8 +947,8 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.increment('_root', 'counter', 4)
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'counter', value: 2, datatype: 'counter', conflict: false },
 | 
			
		||||
        { action: 'increment', obj: '_root', key: 'counter', value: 4 },
 | 
			
		||||
        { action: 'put', path: ['counter'], value: 2, conflict: false },
 | 
			
		||||
        { action: 'inc', path: ['counter'], value: 4 },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -979,10 +962,10 @@ describe('Automerge', () => {
 | 
			
		|||
      doc1.delete('_root', 'key1')
 | 
			
		||||
      doc1.delete('_root', 'key2')
 | 
			
		||||
      assert.deepEqual(doc1.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'key1', value: 1, datatype: 'int', conflict: false },
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'key2', value: 2, datatype: 'int', conflict: false },
 | 
			
		||||
        { action: 'delete', obj: '_root', key: 'key1' },
 | 
			
		||||
        { action: 'delete', obj: '_root', key: 'key2' },
 | 
			
		||||
        { action: 'put', path: ['key1'], value: 1, conflict: false },
 | 
			
		||||
        { action: 'put', path: ['key2'], value: 2, conflict: false },
 | 
			
		||||
        { action: 'del', path: ['key1'], },
 | 
			
		||||
        { action: 'del', path: ['key2'], },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -996,8 +979,8 @@ describe('Automerge', () => {
 | 
			
		|||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
      assert.deepEqual(doc2.getWithType('_root', 'starlings'), ['counter', 3])
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'starlings', value: 2, datatype: 'counter', conflict: false },
 | 
			
		||||
        { action: 'increment', obj: '_root', key: 'starlings', value: 1 }
 | 
			
		||||
        { action: 'put', path: ['starlings'], value: 2, conflict: false },
 | 
			
		||||
        { action: 'inc', path: ['starlings'], value: 1 }
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -1015,10 +998,10 @@ describe('Automerge', () => {
 | 
			
		|||
      doc2.loadIncremental(doc1.saveIncremental())
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(doc2.popPatches(), [
 | 
			
		||||
        { action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
 | 
			
		||||
        { action: 'insert', obj: list, key: 0, value: 1, datatype: 'counter' },
 | 
			
		||||
        { action: 'increment', obj: list, key: 0, value: 2 },
 | 
			
		||||
        { action: 'increment', obj: list, key: 0, value: -5 },
 | 
			
		||||
        { action: 'put', path: ['list'], value: [], conflict: false },
 | 
			
		||||
        { action: 'splice', path: ['list',0], values: [1] },
 | 
			
		||||
        { action: 'inc', path: ['list',0], value: 2 },
 | 
			
		||||
        { action: 'inc', path: ['list',0], value: -5 },
 | 
			
		||||
      ])
 | 
			
		||||
      doc1.free(); doc2.free()
 | 
			
		||||
    })
 | 
			
		||||
| 
						 | 
				
			
			@ -1136,7 +1119,7 @@ describe('Automerge', () => {
 | 
			
		|||
      const n1 = create('abc123'), n2 = create('def456')
 | 
			
		||||
      const s1 = initSyncState(), s2 = initSyncState()
 | 
			
		||||
 | 
			
		||||
      let message, patch
 | 
			
		||||
      let message
 | 
			
		||||
      for (let i = 0; i < 5; i++) {
 | 
			
		||||
        n1.put("_root", "x", i)
 | 
			
		||||
        n1.commit("", 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -1320,7 +1303,7 @@ describe('Automerge', () => {
 | 
			
		|||
 | 
			
		||||
      // create two peers both with divergent commits
 | 
			
		||||
      const n1 = create('01234567'), n2 = create('89abcdef')
 | 
			
		||||
      const s1 = initSyncState(), s2 = initSyncState()
 | 
			
		||||
      //const s1 = initSyncState(), s2 = initSyncState()
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < 10; i++) {
 | 
			
		||||
        n1.put("_root", "x", i)
 | 
			
		||||
| 
						 | 
				
			
			@ -1445,6 +1428,7 @@ describe('Automerge', () => {
 | 
			
		|||
      sync(n1, r, s1, rSyncState)
 | 
			
		||||
      assert.deepStrictEqual(n1.getHeads(), r.getHeads())
 | 
			
		||||
      assert.deepStrictEqual(n1.materialize(), r.materialize())
 | 
			
		||||
      r = null
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    it('should re-sync after one node experiences data loss without disconnecting', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -1496,7 +1480,7 @@ describe('Automerge', () => {
 | 
			
		|||
      // simulate transmission over a network (see https://github.com/automerge/automerge/pull/362)
 | 
			
		||||
      let change = n3.getLastLocalChange()
 | 
			
		||||
      if (change === null) throw new RangeError("no local change")
 | 
			
		||||
      //@ts-ignore
 | 
			
		||||
      //ts-ignore
 | 
			
		||||
      if (typeof Buffer === 'function') change = Buffer.from(change)
 | 
			
		||||
      if (change === undefined) { throw new RangeError("last local change failed") }
 | 
			
		||||
      n2.applyChanges([change])
 | 
			
		||||
| 
						 | 
				
			
			@ -1510,10 +1494,10 @@ describe('Automerge', () => {
 | 
			
		|||
    it('should handle histories with lots of branching and merging', () => {
 | 
			
		||||
      const n1 = create('01234567'), n2 = create('89abcdef'), n3 = create('fedcba98')
 | 
			
		||||
      n1.put("_root", "x", 0); n1.commit("", 0)
 | 
			
		||||
      let change1 = n1.getLastLocalChange()
 | 
			
		||||
      const change1 = n1.getLastLocalChange()
 | 
			
		||||
      if (change1 === null) throw new RangeError("no local change")
 | 
			
		||||
      n2.applyChanges([change1])
 | 
			
		||||
      let change2 = n1.getLastLocalChange()
 | 
			
		||||
      const change2 = n1.getLastLocalChange()
 | 
			
		||||
      if (change2 === null) throw new RangeError("no local change")
 | 
			
		||||
      n3.applyChanges([change2])
 | 
			
		||||
      n3.put("_root", "x", 1); n3.commit("", 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -1730,7 +1714,8 @@ describe('Automerge', () => {
 | 
			
		|||
      //                                   `-- n2c1 <-- n2c2 <-- n2c3
 | 
			
		||||
      // where n2c1 and n2c2 are both false positives in the Bloom filter containing {c5}.
 | 
			
		||||
      // lastSync is c4.
 | 
			
		||||
      let n1 = create('01234567'), n2 = create('89abcdef')
 | 
			
		||||
      const n1 = create('01234567')
 | 
			
		||||
      let n2 = create('89abcdef')
 | 
			
		||||
      let s1 = initSyncState(), s2 = initSyncState()
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < 5; i++) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1831,9 +1816,11 @@ describe('Automerge', () => {
 | 
			
		|||
        // n2 has {c0, c1, c2, n1c1, n1c2, n2c1, n2c2, n2c3};
 | 
			
		||||
        // n3 has {c0, c1, c2, n3c1, n3c2, n3c3}.
 | 
			
		||||
        const n1 = create('01234567'), n2 = create('89abcdef'), n3 = create('76543210')
 | 
			
		||||
        let s13 = initSyncState(), s12 = initSyncState(), s21 = initSyncState()
 | 
			
		||||
        let s13 = initSyncState()
 | 
			
		||||
        const s12 = initSyncState()
 | 
			
		||||
        const s21 = initSyncState()
 | 
			
		||||
        let s32 = initSyncState(), s31 = initSyncState(), s23 = initSyncState()
 | 
			
		||||
        let message1, message2, message3
 | 
			
		||||
        let message1, message3
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < 3; i++) {
 | 
			
		||||
          n1.put("_root", "x", i); n1.commit("", 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -1886,7 +1873,7 @@ describe('Automerge', () => {
 | 
			
		|||
        n2.receiveSyncMessage(s23, encodeSyncMessage(modifiedMessage))
 | 
			
		||||
 | 
			
		||||
        // n2 replies to n3, sending only n2c3 (the one change that n2 has but n1 doesn't)
 | 
			
		||||
        message2 = n2.generateSyncMessage(s23)
 | 
			
		||||
        const message2 = n2.generateSyncMessage(s23)
 | 
			
		||||
        if (message2 === null) { throw new RangeError("message should not be null") }
 | 
			
		||||
        assert.strictEqual(decodeSyncMessage(message2).changes.length, 1) // {n2c3}
 | 
			
		||||
        n3.receiveSyncMessage(s32, message2)
 | 
			
		||||
| 
						 | 
				
			
			@ -1953,7 +1940,7 @@ describe('Automerge', () => {
 | 
			
		|||
        //       `-- c3 <-- c4 <-- c5 <-- c6 <-- c7 <-- c8
 | 
			
		||||
        const n1 = create('01234567'), n2 = create('89abcdef'), n3 = create('76543210')
 | 
			
		||||
        let s1 = initSyncState(), s2 = initSyncState()
 | 
			
		||||
        let msg, decodedMsg
 | 
			
		||||
        let msg
 | 
			
		||||
 | 
			
		||||
        n1.put("_root", "x", 0); n1.commit("", 0)
 | 
			
		||||
        n3.applyChanges(n3.getChangesAdded(n1)) // merge()
 | 
			
		||||
| 
						 | 
				
			
			@ -1992,13 +1979,14 @@ describe('Automerge', () => {
 | 
			
		|||
        n2.receiveSyncMessage(s2, msg)
 | 
			
		||||
        msg = n2.generateSyncMessage(s2)
 | 
			
		||||
        if (msg === null) { throw new RangeError("message should not be null") }
 | 
			
		||||
        decodedMsg = decodeSyncMessage(msg)
 | 
			
		||||
        const decodedMsg = decodeSyncMessage(msg)
 | 
			
		||||
        decodedMsg.changes = [change5, change6]
 | 
			
		||||
        msg = encodeSyncMessage(decodedMsg)
 | 
			
		||||
        const sentHashes: any = {}
 | 
			
		||||
 | 
			
		||||
        sentHashes[decodeChange(change5).hash] = true
 | 
			
		||||
        sentHashes[decodeChange(change6).hash] = true
 | 
			
		||||
 | 
			
		||||
        s2.sentHashes = sentHashes
 | 
			
		||||
        n1.receiveSyncMessage(s1, msg)
 | 
			
		||||
        assert.deepStrictEqual(s1.sharedHeads, [c2, c6].sort())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ use automerge::{Automerge, ROOT};
 | 
			
		|||
fn main() {
 | 
			
		||||
    let mut doc1 = Automerge::new();
 | 
			
		||||
    let (cards, card1) = doc1
 | 
			
		||||
        .transact_with::<_, _, AutomergeError, _, ()>(
 | 
			
		||||
        .transact_with::<_, _, AutomergeError, _>(
 | 
			
		||||
            |_| CommitOptions::default().with_message("Add card".to_owned()),
 | 
			
		||||
            |tx| {
 | 
			
		||||
                let cards = tx.put_object(ROOT, "cards", ObjType::List).unwrap();
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ fn main() {
 | 
			
		|||
    let binary = doc1.save();
 | 
			
		||||
    let mut doc2 = Automerge::load(&binary).unwrap();
 | 
			
		||||
 | 
			
		||||
    doc1.transact_with::<_, _, AutomergeError, _, ()>(
 | 
			
		||||
    doc1.transact_with::<_, _, AutomergeError, _>(
 | 
			
		||||
        |_| CommitOptions::default().with_message("Mark card as done".to_owned()),
 | 
			
		||||
        |tx| {
 | 
			
		||||
            tx.put(&card1, "done", true)?;
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ fn main() {
 | 
			
		|||
    )
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    doc2.transact_with::<_, _, AutomergeError, _, ()>(
 | 
			
		||||
    doc2.transact_with::<_, _, AutomergeError, _>(
 | 
			
		||||
        |_| CommitOptions::default().with_message("Delete card".to_owned()),
 | 
			
		||||
        |tx| {
 | 
			
		||||
            tx.delete(&cards, 0)?;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,19 +9,19 @@ use automerge::ROOT;
 | 
			
		|||
fn main() {
 | 
			
		||||
    let mut doc = Automerge::new();
 | 
			
		||||
 | 
			
		||||
    let mut observer = VecOpObserver::default();
 | 
			
		||||
    // a simple scalar change in the root object
 | 
			
		||||
    doc.transact_with::<_, _, AutomergeError, _, _>(
 | 
			
		||||
        |_result| CommitOptions::default().with_op_observer(&mut observer),
 | 
			
		||||
        |tx| {
 | 
			
		||||
            tx.put(ROOT, "hello", "world").unwrap();
 | 
			
		||||
            Ok(())
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    .unwrap();
 | 
			
		||||
    get_changes(&doc, observer.take_patches());
 | 
			
		||||
    let mut result = doc
 | 
			
		||||
        .transact_observed_with::<_, _, AutomergeError, _, VecOpObserver>(
 | 
			
		||||
            |_result| CommitOptions::default(),
 | 
			
		||||
            |tx| {
 | 
			
		||||
                tx.put(ROOT, "hello", "world").unwrap();
 | 
			
		||||
                Ok(())
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    get_changes(&doc, result.op_observer.take_patches());
 | 
			
		||||
 | 
			
		||||
    let mut tx = doc.transaction();
 | 
			
		||||
    let mut tx = doc.transaction_with_observer(VecOpObserver::default());
 | 
			
		||||
    let map = tx
 | 
			
		||||
        .put_object(ROOT, "my new map", automerge::ObjType::Map)
 | 
			
		||||
        .unwrap();
 | 
			
		||||
| 
						 | 
				
			
			@ -36,28 +36,28 @@ fn main() {
 | 
			
		|||
    tx.insert(&list, 1, "woo").unwrap();
 | 
			
		||||
    let m = tx.insert_object(&list, 2, automerge::ObjType::Map).unwrap();
 | 
			
		||||
    tx.put(&m, "hi", 2).unwrap();
 | 
			
		||||
    let _heads3 = tx.commit_with(CommitOptions::default().with_op_observer(&mut observer));
 | 
			
		||||
    get_changes(&doc, observer.take_patches());
 | 
			
		||||
    let patches = tx.observer().take_patches();
 | 
			
		||||
    let _heads3 = tx.commit_with(CommitOptions::default());
 | 
			
		||||
    get_changes(&doc, patches);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_changes(doc: &Automerge, patches: Vec<Patch>) {
 | 
			
		||||
    for patch in patches {
 | 
			
		||||
        match patch {
 | 
			
		||||
            Patch::Put {
 | 
			
		||||
                obj,
 | 
			
		||||
                key,
 | 
			
		||||
                value,
 | 
			
		||||
                conflict: _,
 | 
			
		||||
                obj, prop, value, ..
 | 
			
		||||
            } => {
 | 
			
		||||
                println!(
 | 
			
		||||
                    "put {:?} at {:?} in obj {:?}, object path {:?}",
 | 
			
		||||
                    value,
 | 
			
		||||
                    key,
 | 
			
		||||
                    prop,
 | 
			
		||||
                    obj,
 | 
			
		||||
                    doc.path_to_object(&obj)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Patch::Insert { obj, index, value } => {
 | 
			
		||||
            Patch::Insert {
 | 
			
		||||
                obj, index, value, ..
 | 
			
		||||
            } => {
 | 
			
		||||
                println!(
 | 
			
		||||
                    "insert {:?} at {:?} in obj {:?}, object path {:?}",
 | 
			
		||||
                    value,
 | 
			
		||||
| 
						 | 
				
			
			@ -66,18 +66,20 @@ fn get_changes(doc: &Automerge, patches: Vec<Patch>) {
 | 
			
		|||
                    doc.path_to_object(&obj)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Patch::Increment { obj, key, value } => {
 | 
			
		||||
            Patch::Increment {
 | 
			
		||||
                obj, prop, value, ..
 | 
			
		||||
            } => {
 | 
			
		||||
                println!(
 | 
			
		||||
                    "increment {:?} in obj {:?} by {:?}, object path {:?}",
 | 
			
		||||
                    key,
 | 
			
		||||
                    prop,
 | 
			
		||||
                    obj,
 | 
			
		||||
                    value,
 | 
			
		||||
                    doc.path_to_object(&obj)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Patch::Delete { obj, key } => println!(
 | 
			
		||||
            Patch::Delete { obj, prop, .. } => println!(
 | 
			
		||||
                "delete {:?} in obj {:?}, object path {:?}",
 | 
			
		||||
                key,
 | 
			
		||||
                prop,
 | 
			
		||||
                obj,
 | 
			
		||||
                doc.path_to_object(&obj)
 | 
			
		||||
            ),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,32 +4,101 @@ use crate::exid::ExId;
 | 
			
		|||
use crate::op_observer::OpObserver;
 | 
			
		||||
use crate::transaction::{CommitOptions, Transactable};
 | 
			
		||||
use crate::{
 | 
			
		||||
    sync, ApplyOptions, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType,
 | 
			
		||||
    Parents, ScalarValue,
 | 
			
		||||
    sync, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Parents, ScalarValue,
 | 
			
		||||
};
 | 
			
		||||
use crate::{
 | 
			
		||||
    transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop,
 | 
			
		||||
    Value, Values,
 | 
			
		||||
    transaction::{Observation, Observed, TransactionInner, UnObserved},
 | 
			
		||||
    ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, Value, Values,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// An automerge document that automatically manages transactions.
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct AutoCommit {
 | 
			
		||||
pub struct AutoCommitWithObs<Obs: Observation> {
 | 
			
		||||
    doc: Automerge,
 | 
			
		||||
    transaction: Option<TransactionInner>,
 | 
			
		||||
    transaction: Option<(Obs, TransactionInner)>,
 | 
			
		||||
    observation: Obs,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for AutoCommit {
 | 
			
		||||
pub type AutoCommit = AutoCommitWithObs<UnObserved>;
 | 
			
		||||
 | 
			
		||||
impl<O: Observation> AutoCommitWithObs<O> {
 | 
			
		||||
    pub fn unobserved() -> AutoCommitWithObs<UnObserved> {
 | 
			
		||||
        AutoCommitWithObs {
 | 
			
		||||
            doc: Automerge::new(),
 | 
			
		||||
            transaction: None,
 | 
			
		||||
            observation: UnObserved::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<O: OpObserver> Default for AutoCommitWithObs<Observed<O>> {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self::new()
 | 
			
		||||
        let op_observer = O::default();
 | 
			
		||||
        AutoCommitWithObs {
 | 
			
		||||
            doc: Automerge::new(),
 | 
			
		||||
            transaction: None,
 | 
			
		||||
            observation: Observed::new(op_observer),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AutoCommit {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
    pub fn new() -> AutoCommit {
 | 
			
		||||
        AutoCommitWithObs {
 | 
			
		||||
            doc: Automerge::new(),
 | 
			
		||||
            transaction: None,
 | 
			
		||||
            observation: UnObserved,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn load(data: &[u8]) -> Result<Self, AutomergeError> {
 | 
			
		||||
        let doc = Automerge::load(data)?;
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            doc,
 | 
			
		||||
            transaction: None,
 | 
			
		||||
            observation: UnObserved,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<Obs: OpObserver> AutoCommitWithObs<Observed<Obs>> {
 | 
			
		||||
    pub fn observer(&mut self) -> &mut Obs {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.observation.observer()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<Obs: Observation + Clone> AutoCommitWithObs<Obs> {
 | 
			
		||||
    pub fn fork(&mut self) -> Self {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        Self {
 | 
			
		||||
            doc: self.doc.fork(),
 | 
			
		||||
            transaction: self.transaction.clone(),
 | 
			
		||||
            observation: self.observation.clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn fork_at(&mut self, heads: &[ChangeHash]) -> Result<Self, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            doc: self.doc.fork_at(heads)?,
 | 
			
		||||
            transaction: self.transaction.clone(),
 | 
			
		||||
            observation: self.observation.clone(),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<Obs: Observation> AutoCommitWithObs<Obs> {
 | 
			
		||||
    pub fn with_observer<Obs2: OpObserver>(
 | 
			
		||||
        self,
 | 
			
		||||
        op_observer: Obs2,
 | 
			
		||||
    ) -> AutoCommitWithObs<Observed<Obs2>> {
 | 
			
		||||
        AutoCommitWithObs {
 | 
			
		||||
            doc: self.doc,
 | 
			
		||||
            transaction: self
 | 
			
		||||
                .transaction
 | 
			
		||||
                .map(|(_, t)| (Observed::new(op_observer.branch()), t)),
 | 
			
		||||
            observation: Observed::new(op_observer),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -58,63 +127,25 @@ impl AutoCommit {
 | 
			
		|||
 | 
			
		||||
    fn ensure_transaction_open(&mut self) {
 | 
			
		||||
        if self.transaction.is_none() {
 | 
			
		||||
            self.transaction = Some(self.doc.transaction_inner());
 | 
			
		||||
            self.transaction = Some((self.observation.branch(), self.doc.transaction_inner()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn fork(&mut self) -> Self {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        Self {
 | 
			
		||||
            doc: self.doc.fork(),
 | 
			
		||||
            transaction: self.transaction.clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn fork_at(&mut self, heads: &[ChangeHash]) -> Result<Self, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            doc: self.doc.fork_at(heads)?,
 | 
			
		||||
            transaction: self.transaction.clone(),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn ensure_transaction_closed(&mut self) {
 | 
			
		||||
        if let Some(tx) = self.transaction.take() {
 | 
			
		||||
            tx.commit::<()>(&mut self.doc, None, None, None);
 | 
			
		||||
        if let Some((current, tx)) = self.transaction.take() {
 | 
			
		||||
            self.observation.merge(¤t);
 | 
			
		||||
            tx.commit(&mut self.doc, None, None);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn load(data: &[u8]) -> Result<Self, AutomergeError> {
 | 
			
		||||
        let doc = Automerge::load(data)?;
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            doc,
 | 
			
		||||
            transaction: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn load_with<Obs: OpObserver>(
 | 
			
		||||
        data: &[u8],
 | 
			
		||||
        options: ApplyOptions<'_, Obs>,
 | 
			
		||||
    ) -> Result<Self, AutomergeError> {
 | 
			
		||||
        let doc = Automerge::load_with(data, options)?;
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            doc,
 | 
			
		||||
            transaction: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn load_incremental(&mut self, data: &[u8]) -> Result<usize, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.doc.load_incremental(data)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn load_incremental_with<'a, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        data: &[u8],
 | 
			
		||||
        options: ApplyOptions<'a, Obs>,
 | 
			
		||||
    ) -> Result<usize, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.doc.load_incremental_with(data, options)
 | 
			
		||||
        // TODO - would be nice to pass None here instead of &mut ()
 | 
			
		||||
        if let Some(observer) = self.observation.observer() {
 | 
			
		||||
            self.doc.load_incremental_with(data, Some(observer))
 | 
			
		||||
        } else {
 | 
			
		||||
            self.doc.load_incremental(data)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn apply_changes(
 | 
			
		||||
| 
						 | 
				
			
			@ -122,34 +153,25 @@ impl AutoCommit {
 | 
			
		|||
        changes: impl IntoIterator<Item = Change>,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.doc.apply_changes(changes)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn apply_changes_with<I: IntoIterator<Item = Change>, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        changes: I,
 | 
			
		||||
        options: ApplyOptions<'_, Obs>,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.doc.apply_changes_with(changes, options)
 | 
			
		||||
        if let Some(observer) = self.observation.observer() {
 | 
			
		||||
            self.doc.apply_changes_with(changes, Some(observer))
 | 
			
		||||
        } else {
 | 
			
		||||
            self.doc.apply_changes(changes)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Takes all the changes in `other` which are not in `self` and applies them
 | 
			
		||||
    pub fn merge(&mut self, other: &mut Self) -> Result<Vec<ChangeHash>, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        other.ensure_transaction_closed();
 | 
			
		||||
        self.doc.merge(&mut other.doc)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Takes all the changes in `other` which are not in `self` and applies them
 | 
			
		||||
    pub fn merge_with<'a, Obs: OpObserver>(
 | 
			
		||||
    pub fn merge<Obs2: Observation>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        other: &mut Self,
 | 
			
		||||
        options: ApplyOptions<'a, Obs>,
 | 
			
		||||
        other: &mut AutoCommitWithObs<Obs2>,
 | 
			
		||||
    ) -> Result<Vec<ChangeHash>, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        other.ensure_transaction_closed();
 | 
			
		||||
        self.doc.merge_with(&mut other.doc, options)
 | 
			
		||||
        if let Some(observer) = self.observation.observer() {
 | 
			
		||||
            self.doc.merge_with(&mut other.doc, Some(observer))
 | 
			
		||||
        } else {
 | 
			
		||||
            self.doc.merge(&mut other.doc)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn save(&mut self) -> Vec<u8> {
 | 
			
		||||
| 
						 | 
				
			
			@ -217,18 +239,12 @@ impl AutoCommit {
 | 
			
		|||
        message: sync::Message,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.doc.receive_sync_message(sync_state, message)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn receive_sync_message_with<'a, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        sync_state: &mut sync::State,
 | 
			
		||||
        message: sync::Message,
 | 
			
		||||
        options: ApplyOptions<'a, Obs>,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_closed();
 | 
			
		||||
        self.doc
 | 
			
		||||
            .receive_sync_message_with(sync_state, message, options)
 | 
			
		||||
        if let Some(observer) = self.observation.observer() {
 | 
			
		||||
            self.doc
 | 
			
		||||
                .receive_sync_message_with(sync_state, message, Some(observer))
 | 
			
		||||
        } else {
 | 
			
		||||
            self.doc.receive_sync_message(sync_state, message)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Return a graphviz representation of the opset.
 | 
			
		||||
| 
						 | 
				
			
			@ -251,7 +267,7 @@ impl AutoCommit {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn commit(&mut self) -> ChangeHash {
 | 
			
		||||
        self.commit_with::<()>(CommitOptions::default())
 | 
			
		||||
        self.commit_with(CommitOptions::default())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Commit the current operations with some options.
 | 
			
		||||
| 
						 | 
				
			
			@ -267,41 +283,32 @@ impl AutoCommit {
 | 
			
		|||
    /// doc.put_object(&ROOT, "todos", ObjType::List).unwrap();
 | 
			
		||||
    /// let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as
 | 
			
		||||
    /// i64;
 | 
			
		||||
    /// doc.commit_with::<()>(CommitOptions::default().with_message("Create todos list").with_time(now));
 | 
			
		||||
    /// doc.commit_with(CommitOptions::default().with_message("Create todos list").with_time(now));
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn commit_with<Obs: OpObserver>(&mut self, options: CommitOptions<'_, Obs>) -> ChangeHash {
 | 
			
		||||
    pub fn commit_with(&mut self, options: CommitOptions) -> ChangeHash {
 | 
			
		||||
        // ensure that even no changes triggers a change
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.take().unwrap();
 | 
			
		||||
        tx.commit(
 | 
			
		||||
            &mut self.doc,
 | 
			
		||||
            options.message,
 | 
			
		||||
            options.time,
 | 
			
		||||
            options.op_observer,
 | 
			
		||||
        )
 | 
			
		||||
        let (current, tx) = self.transaction.take().unwrap();
 | 
			
		||||
        self.observation.merge(¤t);
 | 
			
		||||
        tx.commit(&mut self.doc, options.message, options.time)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn rollback(&mut self) -> usize {
 | 
			
		||||
        self.transaction
 | 
			
		||||
            .take()
 | 
			
		||||
            .map(|tx| tx.rollback(&mut self.doc))
 | 
			
		||||
            .map(|(_, tx)| tx.rollback(&mut self.doc))
 | 
			
		||||
            .unwrap_or(0)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Transactable for AutoCommit {
 | 
			
		||||
impl<Obs: Observation> Transactable for AutoCommitWithObs<Obs> {
 | 
			
		||||
    fn pending_ops(&self) -> usize {
 | 
			
		||||
        self.transaction
 | 
			
		||||
            .as_ref()
 | 
			
		||||
            .map(|t| t.pending_ops())
 | 
			
		||||
            .map(|(_, t)| t.pending_ops())
 | 
			
		||||
            .unwrap_or(0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // KeysAt::()
 | 
			
		||||
    // LenAt::()
 | 
			
		||||
    // PropAt::()
 | 
			
		||||
    // NthAt::()
 | 
			
		||||
 | 
			
		||||
    fn keys<O: AsRef<ExId>>(&self, obj: O) -> Keys<'_, '_> {
 | 
			
		||||
        self.doc.keys(obj)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -364,24 +371,6 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        self.doc.object_type(obj)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // set(obj, prop, value) - value can be scalar or objtype
 | 
			
		||||
    // del(obj, prop)
 | 
			
		||||
    // inc(obj, prop, value)
 | 
			
		||||
    // insert(obj, index, value)
 | 
			
		||||
 | 
			
		||||
    /// Set the value of property `P` to value `V` in object `obj`.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Returns
 | 
			
		||||
    ///
 | 
			
		||||
    /// The opid of the operation which was created, or None if this operation doesn't change the
 | 
			
		||||
    /// document or create a new object.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Errors
 | 
			
		||||
    ///
 | 
			
		||||
    /// This will return an error if
 | 
			
		||||
    /// - The object does not exist
 | 
			
		||||
    /// - The key is the wrong type for the object
 | 
			
		||||
    /// - The key does not exist in the object
 | 
			
		||||
    fn put<O: AsRef<ExId>, P: Into<Prop>, V: Into<ScalarValue>>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        obj: O,
 | 
			
		||||
| 
						 | 
				
			
			@ -389,8 +378,8 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        value: V,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.put(&mut self.doc, obj.as_ref(), prop, value)
 | 
			
		||||
        let (current, tx) = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.put(&mut self.doc, current.observer(), obj.as_ref(), prop, value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn put_object<O: AsRef<ExId>, P: Into<Prop>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -400,8 +389,8 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        value: ObjType,
 | 
			
		||||
    ) -> Result<ExId, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.put_object(&mut self.doc, obj.as_ref(), prop, value)
 | 
			
		||||
        let (current, tx) = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.put_object(&mut self.doc, current.observer(), obj.as_ref(), prop, value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn insert<O: AsRef<ExId>, V: Into<ScalarValue>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -411,8 +400,14 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        value: V,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.insert(&mut self.doc, obj.as_ref(), index, value)
 | 
			
		||||
        let (current, tx) = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.insert(
 | 
			
		||||
            &mut self.doc,
 | 
			
		||||
            current.observer(),
 | 
			
		||||
            obj.as_ref(),
 | 
			
		||||
            index,
 | 
			
		||||
            value,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn insert_object<O: AsRef<ExId>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -422,8 +417,14 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        value: ObjType,
 | 
			
		||||
    ) -> Result<ExId, AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.insert_object(&mut self.doc, obj.as_ref(), index, value)
 | 
			
		||||
        let (current, tx) = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.insert_object(
 | 
			
		||||
            &mut self.doc,
 | 
			
		||||
            current.observer(),
 | 
			
		||||
            obj.as_ref(),
 | 
			
		||||
            index,
 | 
			
		||||
            value,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn increment<O: AsRef<ExId>, P: Into<Prop>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -433,8 +434,8 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        value: i64,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.increment(&mut self.doc, obj.as_ref(), prop, value)
 | 
			
		||||
        let (current, tx) = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.increment(&mut self.doc, current.observer(), obj.as_ref(), prop, value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn delete<O: AsRef<ExId>, P: Into<Prop>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -443,8 +444,8 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        prop: P,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.delete(&mut self.doc, obj.as_ref(), prop)
 | 
			
		||||
        let (current, tx) = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.delete(&mut self.doc, current.observer(), obj.as_ref(), prop)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
 | 
			
		||||
| 
						 | 
				
			
			@ -457,8 +458,15 @@ impl Transactable for AutoCommit {
 | 
			
		|||
        vals: V,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.ensure_transaction_open();
 | 
			
		||||
        let tx = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.splice(&mut self.doc, obj.as_ref(), pos, del, vals)
 | 
			
		||||
        let (current, tx) = self.transaction.as_mut().unwrap();
 | 
			
		||||
        tx.splice(
 | 
			
		||||
            &mut self.doc,
 | 
			
		||||
            current.observer(),
 | 
			
		||||
            obj.as_ref(),
 | 
			
		||||
            pos,
 | 
			
		||||
            del,
 | 
			
		||||
            vals,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn text<O: AsRef<ExId>>(&self, obj: O) -> Result<String, AutomergeError> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,14 +13,16 @@ use crate::op_observer::OpObserver;
 | 
			
		|||
use crate::op_set::OpSet;
 | 
			
		||||
use crate::parents::Parents;
 | 
			
		||||
use crate::storage::{self, load, CompressConfig};
 | 
			
		||||
use crate::transaction::{self, CommitOptions, Failure, Success, Transaction, TransactionInner};
 | 
			
		||||
use crate::transaction::{
 | 
			
		||||
    self, CommitOptions, Failure, Observed, Success, Transaction, TransactionInner, UnObserved,
 | 
			
		||||
};
 | 
			
		||||
use crate::types::{
 | 
			
		||||
    ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ObjId, Op, OpId, OpType,
 | 
			
		||||
    ScalarValue, Value,
 | 
			
		||||
};
 | 
			
		||||
use crate::{
 | 
			
		||||
    query, ApplyOptions, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange,
 | 
			
		||||
    MapRangeAt, ObjType, Prop, Values,
 | 
			
		||||
    query, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType,
 | 
			
		||||
    Prop, Values,
 | 
			
		||||
};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -111,10 +113,22 @@ impl Automerge {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /// Start a transaction.
 | 
			
		||||
    pub fn transaction(&mut self) -> Transaction<'_> {
 | 
			
		||||
    pub fn transaction(&mut self) -> Transaction<'_, UnObserved> {
 | 
			
		||||
        Transaction {
 | 
			
		||||
            inner: Some(self.transaction_inner()),
 | 
			
		||||
            doc: self,
 | 
			
		||||
            observation: Some(UnObserved),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn transaction_with_observer<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        op_observer: Obs,
 | 
			
		||||
    ) -> Transaction<'_, Observed<Obs>> {
 | 
			
		||||
        Transaction {
 | 
			
		||||
            inner: Some(self.transaction_inner()),
 | 
			
		||||
            doc: self,
 | 
			
		||||
            observation: Some(Observed::new(op_observer)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -143,17 +157,48 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    /// Run a transaction on this document in a closure, automatically handling commit or rollback
 | 
			
		||||
    /// afterwards.
 | 
			
		||||
    pub fn transact<F, O, E>(&mut self, f: F) -> transaction::Result<O, E>
 | 
			
		||||
    pub fn transact<F, O, E>(&mut self, f: F) -> transaction::Result<O, (), E>
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_>) -> Result<O, E>,
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_, UnObserved>) -> Result<O, E>,
 | 
			
		||||
    {
 | 
			
		||||
        self.transact_with_impl(None::<&dyn Fn(&O) -> CommitOptions>, f)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like [`Self::transact`] but with a function for generating the commit options.
 | 
			
		||||
    pub fn transact_with<F, O, E, C>(&mut self, c: C, f: F) -> transaction::Result<O, (), E>
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_, UnObserved>) -> Result<O, E>,
 | 
			
		||||
        C: FnOnce(&O) -> CommitOptions,
 | 
			
		||||
    {
 | 
			
		||||
        self.transact_with_impl(Some(c), f)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like [`Self::transact`] but with a function for generating the commit options.
 | 
			
		||||
    fn transact_with_impl<F, O, E, C>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        c: Option<C>,
 | 
			
		||||
        f: F,
 | 
			
		||||
    ) -> transaction::Result<O, (), E>
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_, UnObserved>) -> Result<O, E>,
 | 
			
		||||
        C: FnOnce(&O) -> CommitOptions,
 | 
			
		||||
    {
 | 
			
		||||
        let mut tx = self.transaction();
 | 
			
		||||
        let result = f(&mut tx);
 | 
			
		||||
        match result {
 | 
			
		||||
            Ok(result) => Ok(Success {
 | 
			
		||||
                result,
 | 
			
		||||
                hash: tx.commit(),
 | 
			
		||||
            }),
 | 
			
		||||
            Ok(result) => {
 | 
			
		||||
                let hash = if let Some(c) = c {
 | 
			
		||||
                    let commit_options = c(&result);
 | 
			
		||||
                    tx.commit_with(commit_options)
 | 
			
		||||
                } else {
 | 
			
		||||
                    tx.commit()
 | 
			
		||||
                };
 | 
			
		||||
                Ok(Success {
 | 
			
		||||
                    result,
 | 
			
		||||
                    hash,
 | 
			
		||||
                    op_observer: (),
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
            Err(error) => Err(Failure {
 | 
			
		||||
                error,
 | 
			
		||||
                cancelled: tx.rollback(),
 | 
			
		||||
| 
						 | 
				
			
			@ -161,20 +206,56 @@ impl Automerge {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like [`Self::transact`] but with a function for generating the commit options.
 | 
			
		||||
    pub fn transact_with<'a, F, O, E, C, Obs>(&mut self, c: C, f: F) -> transaction::Result<O, E>
 | 
			
		||||
    /// Run a transaction on this document in a closure, observing ops with `Obs`, automatically handling commit or rollback
 | 
			
		||||
    /// afterwards.
 | 
			
		||||
    pub fn transact_observed<F, O, E, Obs>(&mut self, f: F) -> transaction::Result<O, Obs, E>
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_>) -> Result<O, E>,
 | 
			
		||||
        C: FnOnce(&O) -> CommitOptions<'a, Obs>,
 | 
			
		||||
        Obs: 'a + OpObserver,
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_, Observed<Obs>>) -> Result<O, E>,
 | 
			
		||||
        Obs: OpObserver + Default,
 | 
			
		||||
    {
 | 
			
		||||
        let mut tx = self.transaction();
 | 
			
		||||
        self.transact_observed_with_impl(None::<&dyn Fn(&O) -> CommitOptions>, f)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Like [`Self::transact_observed`] but with a function for generating the commit options
 | 
			
		||||
    pub fn transact_observed_with<F, O, E, C, Obs>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        c: C,
 | 
			
		||||
        f: F,
 | 
			
		||||
    ) -> transaction::Result<O, Obs, E>
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_, Observed<Obs>>) -> Result<O, E>,
 | 
			
		||||
        C: FnOnce(&O) -> CommitOptions,
 | 
			
		||||
        Obs: OpObserver + Default,
 | 
			
		||||
    {
 | 
			
		||||
        self.transact_observed_with_impl(Some(c), f)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn transact_observed_with_impl<F, O, Obs, E, C>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        c: Option<C>,
 | 
			
		||||
        f: F,
 | 
			
		||||
    ) -> transaction::Result<O, Obs, E>
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Transaction<'_, Observed<Obs>>) -> Result<O, E>,
 | 
			
		||||
        C: FnOnce(&O) -> CommitOptions,
 | 
			
		||||
        Obs: OpObserver + Default,
 | 
			
		||||
    {
 | 
			
		||||
        let observer = Obs::default();
 | 
			
		||||
        let mut tx = self.transaction_with_observer(observer);
 | 
			
		||||
        let result = f(&mut tx);
 | 
			
		||||
        match result {
 | 
			
		||||
            Ok(result) => {
 | 
			
		||||
                let commit_options = c(&result);
 | 
			
		||||
                let hash = tx.commit_with(commit_options);
 | 
			
		||||
                Ok(Success { result, hash })
 | 
			
		||||
                let (obs, hash) = if let Some(c) = c {
 | 
			
		||||
                    let commit_options = c(&result);
 | 
			
		||||
                    tx.commit_with(commit_options)
 | 
			
		||||
                } else {
 | 
			
		||||
                    tx.commit()
 | 
			
		||||
                };
 | 
			
		||||
                Ok(Success {
 | 
			
		||||
                    result,
 | 
			
		||||
                    hash,
 | 
			
		||||
                    op_observer: obs,
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
            Err(error) => Err(Failure {
 | 
			
		||||
                error,
 | 
			
		||||
| 
						 | 
				
			
			@ -220,17 +301,6 @@ impl Automerge {
 | 
			
		|||
    // PropAt::()
 | 
			
		||||
    // NthAt::()
 | 
			
		||||
 | 
			
		||||
    /// Get the object id of the object that contains this object and the prop that this object is
 | 
			
		||||
    /// at in that object.
 | 
			
		||||
    pub(crate) fn parent_object(&self, obj: ObjId) -> Option<(ObjId, Key)> {
 | 
			
		||||
        if obj == ObjId::root() {
 | 
			
		||||
            // root has no parent
 | 
			
		||||
            None
 | 
			
		||||
        } else {
 | 
			
		||||
            self.ops.parent_object(&obj)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the parents of an object in the document tree.
 | 
			
		||||
    ///
 | 
			
		||||
    /// ### Errors
 | 
			
		||||
| 
						 | 
				
			
			@ -244,10 +314,7 @@ impl Automerge {
 | 
			
		|||
    /// value.
 | 
			
		||||
    pub fn parents<O: AsRef<ExId>>(&self, obj: O) -> Result<Parents<'_>, AutomergeError> {
 | 
			
		||||
        let obj_id = self.exid_to_obj(obj.as_ref())?;
 | 
			
		||||
        Ok(Parents {
 | 
			
		||||
            obj: obj_id,
 | 
			
		||||
            doc: self,
 | 
			
		||||
        })
 | 
			
		||||
        Ok(self.ops.parents(obj_id))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn path_to_object<O: AsRef<ExId>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -259,21 +326,6 @@ impl Automerge {
 | 
			
		|||
        Ok(path)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Export a key to a prop.
 | 
			
		||||
    pub(crate) fn export_key(&self, obj: ObjId, key: Key) -> Prop {
 | 
			
		||||
        match key {
 | 
			
		||||
            Key::Map(m) => Prop::Map(self.ops.m.props.get(m).into()),
 | 
			
		||||
            Key::Seq(opid) => {
 | 
			
		||||
                let i = self
 | 
			
		||||
                    .ops
 | 
			
		||||
                    .search(&obj, query::ElemIdPos::new(opid))
 | 
			
		||||
                    .index()
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                Prop::Seq(i)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the keys of the object `obj`.
 | 
			
		||||
    ///
 | 
			
		||||
    /// For a map this returns the keys of the map.
 | 
			
		||||
| 
						 | 
				
			
			@ -587,14 +639,14 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    /// Load a document.
 | 
			
		||||
    pub fn load(data: &[u8]) -> Result<Self, AutomergeError> {
 | 
			
		||||
        Self::load_with::<()>(data, ApplyOptions::default())
 | 
			
		||||
        Self::load_with::<()>(data, None)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Load a document.
 | 
			
		||||
    #[tracing::instrument(skip(data, options), err)]
 | 
			
		||||
    #[tracing::instrument(skip(data, observer), err)]
 | 
			
		||||
    pub fn load_with<Obs: OpObserver>(
 | 
			
		||||
        data: &[u8],
 | 
			
		||||
        mut options: ApplyOptions<'_, Obs>,
 | 
			
		||||
        mut observer: Option<&mut Obs>,
 | 
			
		||||
    ) -> Result<Self, AutomergeError> {
 | 
			
		||||
        if data.is_empty() {
 | 
			
		||||
            tracing::trace!("no data, initializing empty document");
 | 
			
		||||
| 
						 | 
				
			
			@ -606,7 +658,6 @@ impl Automerge {
 | 
			
		|||
        if !first_chunk.checksum_valid() {
 | 
			
		||||
            return Err(load::Error::BadChecksum.into());
 | 
			
		||||
        }
 | 
			
		||||
        let observer = &mut options.op_observer;
 | 
			
		||||
 | 
			
		||||
        let mut am = match first_chunk {
 | 
			
		||||
            storage::Chunk::Document(d) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -616,7 +667,7 @@ impl Automerge {
 | 
			
		|||
                    result: op_set,
 | 
			
		||||
                    changes,
 | 
			
		||||
                    heads,
 | 
			
		||||
                } = match observer {
 | 
			
		||||
                } = match &mut observer {
 | 
			
		||||
                    Some(o) => storage::load::reconstruct_document(&d, OpSet::observed_builder(*o)),
 | 
			
		||||
                    None => storage::load::reconstruct_document(&d, OpSet::builder()),
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -651,7 +702,7 @@ impl Automerge {
 | 
			
		|||
                let change = Change::new_from_unverified(stored_change.into_owned(), None)
 | 
			
		||||
                    .map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
 | 
			
		||||
                let mut am = Self::new();
 | 
			
		||||
                am.apply_change(change, observer);
 | 
			
		||||
                am.apply_change(change, &mut observer);
 | 
			
		||||
                am
 | 
			
		||||
            }
 | 
			
		||||
            storage::Chunk::CompressedChange(stored_change, compressed) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -662,7 +713,7 @@ impl Automerge {
 | 
			
		|||
                )
 | 
			
		||||
                .map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
 | 
			
		||||
                let mut am = Self::new();
 | 
			
		||||
                am.apply_change(change, observer);
 | 
			
		||||
                am.apply_change(change, &mut observer);
 | 
			
		||||
                am
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
| 
						 | 
				
			
			@ -670,7 +721,7 @@ impl Automerge {
 | 
			
		|||
        match load::load_changes(remaining.reset()) {
 | 
			
		||||
            load::LoadedChanges::Complete(c) => {
 | 
			
		||||
                for change in c {
 | 
			
		||||
                    am.apply_change(change, observer);
 | 
			
		||||
                    am.apply_change(change, &mut observer);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            load::LoadedChanges::Partial { error, .. } => return Err(error.into()),
 | 
			
		||||
| 
						 | 
				
			
			@ -680,14 +731,14 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    /// Load an incremental save of a document.
 | 
			
		||||
    pub fn load_incremental(&mut self, data: &[u8]) -> Result<usize, AutomergeError> {
 | 
			
		||||
        self.load_incremental_with::<()>(data, ApplyOptions::default())
 | 
			
		||||
        self.load_incremental_with::<()>(data, None)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Load an incremental save of a document.
 | 
			
		||||
    pub fn load_incremental_with<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        data: &[u8],
 | 
			
		||||
        options: ApplyOptions<'_, Obs>,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
    ) -> Result<usize, AutomergeError> {
 | 
			
		||||
        let changes = match load::load_changes(storage::parse::Input::new(data)) {
 | 
			
		||||
            load::LoadedChanges::Complete(c) => c,
 | 
			
		||||
| 
						 | 
				
			
			@ -697,7 +748,7 @@ impl Automerge {
 | 
			
		|||
            }
 | 
			
		||||
        };
 | 
			
		||||
        let start = self.ops.len();
 | 
			
		||||
        self.apply_changes_with(changes, options)?;
 | 
			
		||||
        self.apply_changes_with(changes, op_observer)?;
 | 
			
		||||
        let delta = self.ops.len() - start;
 | 
			
		||||
        Ok(delta)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -717,14 +768,14 @@ impl Automerge {
 | 
			
		|||
        &mut self,
 | 
			
		||||
        changes: impl IntoIterator<Item = Change>,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.apply_changes_with::<_, ()>(changes, ApplyOptions::default())
 | 
			
		||||
        self.apply_changes_with::<_, ()>(changes, None)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Apply changes to this document.
 | 
			
		||||
    pub fn apply_changes_with<I: IntoIterator<Item = Change>, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        changes: I,
 | 
			
		||||
        mut options: ApplyOptions<'_, Obs>,
 | 
			
		||||
        mut op_observer: Option<&mut Obs>,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        for c in changes {
 | 
			
		||||
            if !self.history_index.contains_key(&c.hash()) {
 | 
			
		||||
| 
						 | 
				
			
			@ -735,7 +786,7 @@ impl Automerge {
 | 
			
		|||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
                if self.is_causally_ready(&c) {
 | 
			
		||||
                    self.apply_change(c, &mut options.op_observer);
 | 
			
		||||
                    self.apply_change(c, &mut op_observer);
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.queue.push(c);
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -743,7 +794,7 @@ impl Automerge {
 | 
			
		|||
        }
 | 
			
		||||
        while let Some(c) = self.pop_next_causally_ready_change() {
 | 
			
		||||
            if !self.history_index.contains_key(&c.hash()) {
 | 
			
		||||
                self.apply_change(c, &mut options.op_observer);
 | 
			
		||||
                self.apply_change(c, &mut op_observer);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
| 
						 | 
				
			
			@ -831,14 +882,14 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
    /// Takes all the changes in `other` which are not in `self` and applies them
 | 
			
		||||
    pub fn merge(&mut self, other: &mut Self) -> Result<Vec<ChangeHash>, AutomergeError> {
 | 
			
		||||
        self.merge_with::<()>(other, ApplyOptions::default())
 | 
			
		||||
        self.merge_with::<()>(other, None)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Takes all the changes in `other` which are not in `self` and applies them
 | 
			
		||||
    pub fn merge_with<'a, Obs: OpObserver>(
 | 
			
		||||
    pub fn merge_with<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        other: &mut Self,
 | 
			
		||||
        options: ApplyOptions<'a, Obs>,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
    ) -> Result<Vec<ChangeHash>, AutomergeError> {
 | 
			
		||||
        // TODO: Make this fallible and figure out how to do this transactionally
 | 
			
		||||
        let changes = self
 | 
			
		||||
| 
						 | 
				
			
			@ -847,7 +898,7 @@ impl Automerge {
 | 
			
		|||
            .cloned()
 | 
			
		||||
            .collect::<Vec<_>>();
 | 
			
		||||
        tracing::trace!(changes=?changes.iter().map(|c| c.hash()).collect::<Vec<_>>(), "merging new changes");
 | 
			
		||||
        self.apply_changes_with(changes, options)?;
 | 
			
		||||
        self.apply_changes_with(changes, op_observer)?;
 | 
			
		||||
        Ok(self.get_heads())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1437,19 +1437,15 @@ fn observe_counter_change_application_overwrite() {
 | 
			
		|||
    doc1.increment(ROOT, "counter", 5).unwrap();
 | 
			
		||||
    doc1.commit();
 | 
			
		||||
 | 
			
		||||
    let mut observer = VecOpObserver::default();
 | 
			
		||||
    let mut doc3 = doc1.clone();
 | 
			
		||||
    doc3.merge_with(
 | 
			
		||||
        &mut doc2,
 | 
			
		||||
        ApplyOptions::default().with_op_observer(&mut observer),
 | 
			
		||||
    )
 | 
			
		||||
    .unwrap();
 | 
			
		||||
    let mut doc3 = doc1.fork().with_observer(VecOpObserver::default());
 | 
			
		||||
    doc3.merge(&mut doc2).unwrap();
 | 
			
		||||
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        observer.take_patches(),
 | 
			
		||||
        doc3.observer().take_patches(),
 | 
			
		||||
        vec![Patch::Put {
 | 
			
		||||
            obj: ExId::Root,
 | 
			
		||||
            key: Prop::Map("counter".into()),
 | 
			
		||||
            path: vec![],
 | 
			
		||||
            prop: Prop::Map("counter".into()),
 | 
			
		||||
            value: (
 | 
			
		||||
                ScalarValue::Str("mystring".into()).into(),
 | 
			
		||||
                ExId::Id(2, doc2.get_actor().clone(), 1)
 | 
			
		||||
| 
						 | 
				
			
			@ -1458,16 +1454,11 @@ fn observe_counter_change_application_overwrite() {
 | 
			
		|||
        }]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let mut observer = VecOpObserver::default();
 | 
			
		||||
    let mut doc4 = doc2.clone();
 | 
			
		||||
    doc4.merge_with(
 | 
			
		||||
        &mut doc1,
 | 
			
		||||
        ApplyOptions::default().with_op_observer(&mut observer),
 | 
			
		||||
    )
 | 
			
		||||
    .unwrap();
 | 
			
		||||
    let mut doc4 = doc2.clone().with_observer(VecOpObserver::default());
 | 
			
		||||
    doc4.merge(&mut doc1).unwrap();
 | 
			
		||||
 | 
			
		||||
    // no patches as the increments operate on an invisible counter
 | 
			
		||||
    assert_eq!(observer.take_patches(), vec![]);
 | 
			
		||||
    assert_eq!(doc4.observer().take_patches(), vec![]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
| 
						 | 
				
			
			@ -1478,20 +1469,15 @@ fn observe_counter_change_application() {
 | 
			
		|||
    doc.increment(ROOT, "counter", 5).unwrap();
 | 
			
		||||
    let changes = doc.get_changes(&[]).unwrap().into_iter().cloned();
 | 
			
		||||
 | 
			
		||||
    let mut new_doc = AutoCommit::new();
 | 
			
		||||
    let mut observer = VecOpObserver::default();
 | 
			
		||||
    new_doc
 | 
			
		||||
        .apply_changes_with(
 | 
			
		||||
            changes,
 | 
			
		||||
            ApplyOptions::default().with_op_observer(&mut observer),
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap();
 | 
			
		||||
    let mut new_doc = AutoCommit::new().with_observer(VecOpObserver::default());
 | 
			
		||||
    new_doc.apply_changes(changes).unwrap();
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        observer.take_patches(),
 | 
			
		||||
        new_doc.observer().take_patches(),
 | 
			
		||||
        vec![
 | 
			
		||||
            Patch::Put {
 | 
			
		||||
                obj: ExId::Root,
 | 
			
		||||
                key: Prop::Map("counter".into()),
 | 
			
		||||
                path: vec![],
 | 
			
		||||
                prop: Prop::Map("counter".into()),
 | 
			
		||||
                value: (
 | 
			
		||||
                    ScalarValue::counter(1).into(),
 | 
			
		||||
                    ExId::Id(1, doc.get_actor().clone(), 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -1500,12 +1486,14 @@ fn observe_counter_change_application() {
 | 
			
		|||
            },
 | 
			
		||||
            Patch::Increment {
 | 
			
		||||
                obj: ExId::Root,
 | 
			
		||||
                key: Prop::Map("counter".into()),
 | 
			
		||||
                path: vec![],
 | 
			
		||||
                prop: Prop::Map("counter".into()),
 | 
			
		||||
                value: (2, ExId::Id(2, doc.get_actor().clone(), 0)),
 | 
			
		||||
            },
 | 
			
		||||
            Patch::Increment {
 | 
			
		||||
                obj: ExId::Root,
 | 
			
		||||
                key: Prop::Map("counter".into()),
 | 
			
		||||
                path: vec![],
 | 
			
		||||
                prop: Prop::Map("counter".into()),
 | 
			
		||||
                value: (5, ExId::Id(3, doc.get_actor().clone(), 0)),
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
| 
						 | 
				
			
			@ -1514,7 +1502,7 @@ fn observe_counter_change_application() {
 | 
			
		|||
 | 
			
		||||
#[test]
 | 
			
		||||
fn get_changes_heads_empty() {
 | 
			
		||||
    let mut doc = AutoCommit::new();
 | 
			
		||||
    let mut doc = AutoCommit::unobserved();
 | 
			
		||||
    doc.put(ROOT, "key1", 1).unwrap();
 | 
			
		||||
    doc.commit();
 | 
			
		||||
    doc.put(ROOT, "key2", 1).unwrap();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,6 @@ mod map_range_at;
 | 
			
		|||
mod op_observer;
 | 
			
		||||
mod op_set;
 | 
			
		||||
mod op_tree;
 | 
			
		||||
mod options;
 | 
			
		||||
mod parents;
 | 
			
		||||
mod query;
 | 
			
		||||
mod storage;
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +87,7 @@ mod values;
 | 
			
		|||
mod visualisation;
 | 
			
		||||
 | 
			
		||||
pub use crate::automerge::Automerge;
 | 
			
		||||
pub use autocommit::AutoCommit;
 | 
			
		||||
pub use autocommit::{AutoCommit, AutoCommitWithObs};
 | 
			
		||||
pub use autoserde::AutoSerde;
 | 
			
		||||
pub use change::{Change, LoadError as LoadChangeError};
 | 
			
		||||
pub use error::AutomergeError;
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +104,6 @@ pub use map_range_at::MapRangeAt;
 | 
			
		|||
pub use op_observer::OpObserver;
 | 
			
		||||
pub use op_observer::Patch;
 | 
			
		||||
pub use op_observer::VecOpObserver;
 | 
			
		||||
pub use options::ApplyOptions;
 | 
			
		||||
pub use parents::Parents;
 | 
			
		||||
pub use types::{ActorId, ChangeHash, ObjType, OpType, Prop};
 | 
			
		||||
pub use value::{ScalarValue, Value};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,50 +1,113 @@
 | 
			
		|||
use crate::exid::ExId;
 | 
			
		||||
use crate::Parents;
 | 
			
		||||
use crate::Prop;
 | 
			
		||||
use crate::Value;
 | 
			
		||||
 | 
			
		||||
/// An observer of operations applied to the document.
 | 
			
		||||
pub trait OpObserver {
 | 
			
		||||
pub trait OpObserver: Default + Clone {
 | 
			
		||||
    /// A new value has been inserted into the given object.
 | 
			
		||||
    ///
 | 
			
		||||
    /// - `parents`: A parents iterator that can be used to collect path information
 | 
			
		||||
    /// - `objid`: the object that has been inserted into.
 | 
			
		||||
    /// - `index`: the index the new value has been inserted at.
 | 
			
		||||
    /// - `tagged_value`: the value that has been inserted and the id of the operation that did the
 | 
			
		||||
    /// insert.
 | 
			
		||||
    fn insert(&mut self, objid: ExId, index: usize, tagged_value: (Value<'_>, ExId));
 | 
			
		||||
    fn insert(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        parents: Parents<'_>,
 | 
			
		||||
        objid: ExId,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        tagged_value: (Value<'_>, ExId),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /// A new value has been put into the given object.
 | 
			
		||||
    ///
 | 
			
		||||
    /// - `parents`: A parents iterator that can be used to collect path information
 | 
			
		||||
    /// - `objid`: the object that has been put into.
 | 
			
		||||
    /// - `key`: the key that the value as been put at.
 | 
			
		||||
    /// - `prop`: the prop that the value as been put at.
 | 
			
		||||
    /// - `tagged_value`: the value that has been put into the object and the id of the operation
 | 
			
		||||
    /// that did the put.
 | 
			
		||||
    /// - `conflict`: whether this put conflicts with other operations.
 | 
			
		||||
    fn put(&mut self, objid: ExId, key: Prop, tagged_value: (Value<'_>, ExId), conflict: bool);
 | 
			
		||||
    fn put(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        parents: Parents<'_>,
 | 
			
		||||
        objid: ExId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        tagged_value: (Value<'_>, ExId),
 | 
			
		||||
        conflict: bool,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /// A counter has been incremented.
 | 
			
		||||
    ///
 | 
			
		||||
    /// - `parents`: A parents iterator that can be used to collect path information
 | 
			
		||||
    /// - `objid`: the object that contains the counter.
 | 
			
		||||
    /// - `key`: they key that the chounter is at.
 | 
			
		||||
    /// - `prop`: they prop that the chounter is at.
 | 
			
		||||
    /// - `tagged_value`: the amount the counter has been incremented by, and the the id of the
 | 
			
		||||
    /// increment operation.
 | 
			
		||||
    fn increment(&mut self, objid: ExId, key: Prop, tagged_value: (i64, ExId));
 | 
			
		||||
    fn increment(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        parents: Parents<'_>,
 | 
			
		||||
        objid: ExId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        tagged_value: (i64, ExId),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /// A value has beeen deleted.
 | 
			
		||||
    ///
 | 
			
		||||
    /// - `parents`: A parents iterator that can be used to collect path information
 | 
			
		||||
    /// - `objid`: the object that has been deleted in.
 | 
			
		||||
    /// - `key`: the key of the value that has been deleted.
 | 
			
		||||
    fn delete(&mut self, objid: ExId, key: Prop);
 | 
			
		||||
    /// - `prop`: the prop of the value that has been deleted.
 | 
			
		||||
    fn delete(&mut self, parents: Parents<'_>, objid: ExId, prop: Prop);
 | 
			
		||||
 | 
			
		||||
    /// Branch of a new op_observer later to be merged
 | 
			
		||||
    ///
 | 
			
		||||
    /// Called by AutoCommit when creating a new transaction.  Observer branch
 | 
			
		||||
    /// will be merged on `commit()` or thrown away on `rollback()`
 | 
			
		||||
    ///
 | 
			
		||||
    fn branch(&self) -> Self {
 | 
			
		||||
        Self::default()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Merge observed information from a transaction.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Called by AutoCommit on `commit()`
 | 
			
		||||
    ///
 | 
			
		||||
    /// - `other`: Another Op Observer of the same type
 | 
			
		||||
    fn merge(&mut self, other: &Self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl OpObserver for () {
 | 
			
		||||
    fn insert(&mut self, _objid: ExId, _index: usize, _tagged_value: (Value<'_>, ExId)) {}
 | 
			
		||||
 | 
			
		||||
    fn put(&mut self, _objid: ExId, _key: Prop, _tagged_value: (Value<'_>, ExId), _conflict: bool) {
 | 
			
		||||
    fn insert(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        _parents: Parents<'_>,
 | 
			
		||||
        _objid: ExId,
 | 
			
		||||
        _index: usize,
 | 
			
		||||
        _tagged_value: (Value<'_>, ExId),
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn increment(&mut self, _objid: ExId, _key: Prop, _tagged_value: (i64, ExId)) {}
 | 
			
		||||
    fn put(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        _parents: Parents<'_>,
 | 
			
		||||
        _objid: ExId,
 | 
			
		||||
        _prop: Prop,
 | 
			
		||||
        _tagged_value: (Value<'_>, ExId),
 | 
			
		||||
        _conflict: bool,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn delete(&mut self, _objid: ExId, _key: Prop) {}
 | 
			
		||||
    fn increment(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        _parents: Parents<'_>,
 | 
			
		||||
        _objid: ExId,
 | 
			
		||||
        _prop: Prop,
 | 
			
		||||
        _tagged_value: (i64, ExId),
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn delete(&mut self, _parents: Parents<'_>, _objid: ExId, _prop: Prop) {}
 | 
			
		||||
 | 
			
		||||
    fn merge(&mut self, _other: &Self) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Capture operations into a [`Vec`] and store them as patches.
 | 
			
		||||
| 
						 | 
				
			
			@ -62,45 +125,77 @@ impl VecOpObserver {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
impl OpObserver for VecOpObserver {
 | 
			
		||||
    fn insert(&mut self, obj_id: ExId, index: usize, (value, id): (Value<'_>, ExId)) {
 | 
			
		||||
    fn insert(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        mut parents: Parents<'_>,
 | 
			
		||||
        obj: ExId,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        (value, id): (Value<'_>, ExId),
 | 
			
		||||
    ) {
 | 
			
		||||
        let path = parents.path();
 | 
			
		||||
        self.patches.push(Patch::Insert {
 | 
			
		||||
            obj: obj_id,
 | 
			
		||||
            obj,
 | 
			
		||||
            path,
 | 
			
		||||
            index,
 | 
			
		||||
            value: (value.into_owned(), id),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn put(&mut self, objid: ExId, key: Prop, (value, id): (Value<'_>, ExId), conflict: bool) {
 | 
			
		||||
    fn put(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        mut parents: Parents<'_>,
 | 
			
		||||
        obj: ExId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        (value, id): (Value<'_>, ExId),
 | 
			
		||||
        conflict: bool,
 | 
			
		||||
    ) {
 | 
			
		||||
        let path = parents.path();
 | 
			
		||||
        self.patches.push(Patch::Put {
 | 
			
		||||
            obj: objid,
 | 
			
		||||
            key,
 | 
			
		||||
            obj,
 | 
			
		||||
            path,
 | 
			
		||||
            prop,
 | 
			
		||||
            value: (value.into_owned(), id),
 | 
			
		||||
            conflict,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn increment(&mut self, objid: ExId, key: Prop, tagged_value: (i64, ExId)) {
 | 
			
		||||
    fn increment(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        mut parents: Parents<'_>,
 | 
			
		||||
        obj: ExId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        tagged_value: (i64, ExId),
 | 
			
		||||
    ) {
 | 
			
		||||
        let path = parents.path();
 | 
			
		||||
        self.patches.push(Patch::Increment {
 | 
			
		||||
            obj: objid,
 | 
			
		||||
            key,
 | 
			
		||||
            obj,
 | 
			
		||||
            path,
 | 
			
		||||
            prop,
 | 
			
		||||
            value: tagged_value,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn delete(&mut self, objid: ExId, key: Prop) {
 | 
			
		||||
        self.patches.push(Patch::Delete { obj: objid, key })
 | 
			
		||||
    fn delete(&mut self, mut parents: Parents<'_>, obj: ExId, prop: Prop) {
 | 
			
		||||
        let path = parents.path();
 | 
			
		||||
        self.patches.push(Patch::Delete { obj, path, prop })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn merge(&mut self, other: &Self) {
 | 
			
		||||
        self.patches.extend_from_slice(other.patches.as_slice())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A notification to the application that something has changed in a document.
 | 
			
		||||
#[derive(Debug, Clone, PartialEq)]
 | 
			
		||||
pub enum Patch {
 | 
			
		||||
    /// Associating a new value with a key in a map, or an existing list element
 | 
			
		||||
    /// Associating a new value with a prop in a map, or an existing list element
 | 
			
		||||
    Put {
 | 
			
		||||
        /// path to the object
 | 
			
		||||
        path: Vec<(ExId, Prop)>,
 | 
			
		||||
        /// The object that was put into.
 | 
			
		||||
        obj: ExId,
 | 
			
		||||
        /// The key that the new value was put at.
 | 
			
		||||
        key: Prop,
 | 
			
		||||
        /// The prop that the new value was put at.
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        /// The value that was put, and the id of the operation that put it there.
 | 
			
		||||
        value: (Value<'static>, ExId),
 | 
			
		||||
        /// Whether this put conflicts with another.
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +203,8 @@ pub enum Patch {
 | 
			
		|||
    },
 | 
			
		||||
    /// Inserting a new element into a list/text
 | 
			
		||||
    Insert {
 | 
			
		||||
        /// path to the object
 | 
			
		||||
        path: Vec<(ExId, Prop)>,
 | 
			
		||||
        /// The object that was inserted into.
 | 
			
		||||
        obj: ExId,
 | 
			
		||||
        /// The index that the new value was inserted at.
 | 
			
		||||
| 
						 | 
				
			
			@ -117,19 +214,23 @@ pub enum Patch {
 | 
			
		|||
    },
 | 
			
		||||
    /// Incrementing a counter.
 | 
			
		||||
    Increment {
 | 
			
		||||
        /// path to the object
 | 
			
		||||
        path: Vec<(ExId, Prop)>,
 | 
			
		||||
        /// The object that was incremented in.
 | 
			
		||||
        obj: ExId,
 | 
			
		||||
        /// The key that was incremented.
 | 
			
		||||
        key: Prop,
 | 
			
		||||
        /// The prop that was incremented.
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        /// The amount that the counter was incremented by, and the id of the operation that
 | 
			
		||||
        /// did the increment.
 | 
			
		||||
        value: (i64, ExId),
 | 
			
		||||
    },
 | 
			
		||||
    /// Deleting an element from a list/text
 | 
			
		||||
    Delete {
 | 
			
		||||
        /// path to the object
 | 
			
		||||
        path: Vec<(ExId, Prop)>,
 | 
			
		||||
        /// The object that was deleted from.
 | 
			
		||||
        obj: ExId,
 | 
			
		||||
        /// The key that was deleted.
 | 
			
		||||
        key: Prop,
 | 
			
		||||
        /// The prop that was deleted.
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,9 @@ use crate::clock::Clock;
 | 
			
		|||
use crate::exid::ExId;
 | 
			
		||||
use crate::indexed_cache::IndexedCache;
 | 
			
		||||
use crate::op_tree::{self, OpTree};
 | 
			
		||||
use crate::parents::Parents;
 | 
			
		||||
use crate::query::{self, OpIdSearch, TreeQuery};
 | 
			
		||||
use crate::types::{self, ActorId, Key, ObjId, Op, OpId, OpIds, OpType};
 | 
			
		||||
use crate::types::{self, ActorId, Key, ObjId, Op, OpId, OpIds, OpType, Prop};
 | 
			
		||||
use crate::{ObjType, OpObserver};
 | 
			
		||||
use fxhash::FxBuildHasher;
 | 
			
		||||
use std::borrow::Borrow;
 | 
			
		||||
| 
						 | 
				
			
			@ -68,12 +69,29 @@ impl OpSetInternal {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn parents(&self, obj: ObjId) -> Parents<'_> {
 | 
			
		||||
        Parents { obj, ops: self }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn parent_object(&self, obj: &ObjId) -> Option<(ObjId, Key)> {
 | 
			
		||||
        let parent = self.trees.get(obj)?.parent?;
 | 
			
		||||
        let key = self.search(&parent, OpIdSearch::new(obj.0)).key().unwrap();
 | 
			
		||||
        Some((parent, key))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn export_key(&self, obj: ObjId, key: Key) -> Prop {
 | 
			
		||||
        match key {
 | 
			
		||||
            Key::Map(m) => Prop::Map(self.m.props.get(m).into()),
 | 
			
		||||
            Key::Seq(opid) => {
 | 
			
		||||
                let i = self
 | 
			
		||||
                    .search(&obj, query::ElemIdPos::new(opid))
 | 
			
		||||
                    .index()
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
                Prop::Seq(i)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn keys(&self, obj: ObjId) -> Option<query::Keys<'_>> {
 | 
			
		||||
        if let Some(tree) = self.trees.get(&obj) {
 | 
			
		||||
            tree.internal.keys()
 | 
			
		||||
| 
						 | 
				
			
			@ -245,6 +263,8 @@ impl OpSetInternal {
 | 
			
		|||
        } = q;
 | 
			
		||||
 | 
			
		||||
        let ex_obj = self.id_to_exid(obj.0);
 | 
			
		||||
        let parents = self.parents(*obj);
 | 
			
		||||
 | 
			
		||||
        let key = match op.key {
 | 
			
		||||
            Key::Map(index) => self.m.props[index].clone().into(),
 | 
			
		||||
            Key::Seq(_) => seen.into(),
 | 
			
		||||
| 
						 | 
				
			
			@ -252,21 +272,26 @@ impl OpSetInternal {
 | 
			
		|||
 | 
			
		||||
        if op.insert {
 | 
			
		||||
            let value = (op.value(), self.id_to_exid(op.id));
 | 
			
		||||
            observer.insert(ex_obj, seen, value);
 | 
			
		||||
            observer.insert(parents, ex_obj, seen, value);
 | 
			
		||||
        } else if op.is_delete() {
 | 
			
		||||
            if let Some(winner) = &values.last() {
 | 
			
		||||
                let value = (winner.value(), self.id_to_exid(winner.id));
 | 
			
		||||
                let conflict = values.len() > 1;
 | 
			
		||||
                observer.put(ex_obj, key, value, conflict);
 | 
			
		||||
            } else {
 | 
			
		||||
                observer.delete(ex_obj, key);
 | 
			
		||||
                observer.put(parents, ex_obj, key, value, conflict);
 | 
			
		||||
            } else if had_value_before {
 | 
			
		||||
                observer.delete(parents, ex_obj, key);
 | 
			
		||||
            }
 | 
			
		||||
        } else if let Some(value) = op.get_increment_value() {
 | 
			
		||||
            // only observe this increment if the counter is visible, i.e. the counter's
 | 
			
		||||
            // create op is in the values
 | 
			
		||||
            if values.iter().any(|value| op.pred.contains(&value.id)) {
 | 
			
		||||
            //if values.iter().any(|value| op.pred.contains(&value.id)) {
 | 
			
		||||
            if values
 | 
			
		||||
                .last()
 | 
			
		||||
                .map(|value| op.pred.contains(&value.id))
 | 
			
		||||
                .unwrap_or_default()
 | 
			
		||||
            {
 | 
			
		||||
                // we have observed the value
 | 
			
		||||
                observer.increment(ex_obj, key, (value, self.id_to_exid(op.id)));
 | 
			
		||||
                observer.increment(parents, ex_obj, key, (value, self.id_to_exid(op.id)));
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            let winner = if let Some(last_value) = values.last() {
 | 
			
		||||
| 
						 | 
				
			
			@ -280,10 +305,10 @@ impl OpSetInternal {
 | 
			
		|||
            };
 | 
			
		||||
            let value = (winner.value(), self.id_to_exid(winner.id));
 | 
			
		||||
            if op.is_list_op() && !had_value_before {
 | 
			
		||||
                observer.insert(ex_obj, seen, value);
 | 
			
		||||
                observer.insert(parents, ex_obj, seen, value);
 | 
			
		||||
            } else {
 | 
			
		||||
                let conflict = !values.is_empty();
 | 
			
		||||
                observer.put(ex_obj, key, value, conflict);
 | 
			
		||||
                observer.put(parents, ex_obj, key, value, conflict);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
#[derive(Debug, Default)]
 | 
			
		||||
pub struct ApplyOptions<'a, Obs> {
 | 
			
		||||
    pub op_observer: Option<&'a mut Obs>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a, Obs> ApplyOptions<'a, Obs> {
 | 
			
		||||
    pub fn with_op_observer(mut self, op_observer: &'a mut Obs) -> Self {
 | 
			
		||||
        self.op_observer = Some(op_observer);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_op_observer(&mut self, op_observer: &'a mut Obs) -> &mut Self {
 | 
			
		||||
        self.op_observer = Some(op_observer);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +1,33 @@
 | 
			
		|||
use crate::{exid::ExId, types::ObjId, Automerge, Prop};
 | 
			
		||||
use crate::op_set::OpSet;
 | 
			
		||||
use crate::types::ObjId;
 | 
			
		||||
use crate::{exid::ExId, Prop};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct Parents<'a> {
 | 
			
		||||
    pub(crate) obj: ObjId,
 | 
			
		||||
    pub(crate) doc: &'a Automerge,
 | 
			
		||||
    pub(crate) ops: &'a OpSet,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Parents<'a> {
 | 
			
		||||
    pub fn path(&mut self) -> Vec<(ExId, Prop)> {
 | 
			
		||||
        let mut path = self.collect::<Vec<_>>();
 | 
			
		||||
        path.reverse();
 | 
			
		||||
        path
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Iterator for Parents<'a> {
 | 
			
		||||
    type Item = (ExId, Prop);
 | 
			
		||||
 | 
			
		||||
    fn next(&mut self) -> Option<Self::Item> {
 | 
			
		||||
        if let Some((obj, key)) = self.doc.parent_object(self.obj) {
 | 
			
		||||
        if self.obj.is_root() {
 | 
			
		||||
            None
 | 
			
		||||
        } else if let Some((obj, key)) = self.ops.parent_object(&self.obj) {
 | 
			
		||||
            self.obj = obj;
 | 
			
		||||
            Some((self.doc.id_to_exid(obj.0), self.doc.export_key(obj, key)))
 | 
			
		||||
            Some((
 | 
			
		||||
                self.ops.id_to_exid(self.obj.0),
 | 
			
		||||
                self.ops.export_key(self.obj, key),
 | 
			
		||||
            ))
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,8 +8,6 @@ use std::fmt::Debug;
 | 
			
		|||
pub(crate) struct SeekOpWithPatch<'a> {
 | 
			
		||||
    op: Op,
 | 
			
		||||
    pub(crate) pos: usize,
 | 
			
		||||
    /// A position counter for after we find the insert position to record conflicts.
 | 
			
		||||
    later_pos: usize,
 | 
			
		||||
    pub(crate) succ: Vec<usize>,
 | 
			
		||||
    found: bool,
 | 
			
		||||
    pub(crate) seen: usize,
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +24,6 @@ impl<'a> SeekOpWithPatch<'a> {
 | 
			
		|||
            op: op.clone(),
 | 
			
		||||
            succ: vec![],
 | 
			
		||||
            pos: 0,
 | 
			
		||||
            later_pos: 0,
 | 
			
		||||
            found: false,
 | 
			
		||||
            seen: 0,
 | 
			
		||||
            last_seen: None,
 | 
			
		||||
| 
						 | 
				
			
			@ -176,6 +173,10 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> {
 | 
			
		|||
                            self.values.push(e);
 | 
			
		||||
                        }
 | 
			
		||||
                        self.succ.push(self.pos);
 | 
			
		||||
 | 
			
		||||
                        if e.visible() {
 | 
			
		||||
                            self.had_value_before = true;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if e.visible() {
 | 
			
		||||
                        self.values.push(e);
 | 
			
		||||
                    }
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +185,6 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> {
 | 
			
		|||
                    // we reach an op with an opId greater than that of the new operation
 | 
			
		||||
                    if m.lamport_cmp(e.id, self.op.id) == Ordering::Greater {
 | 
			
		||||
                        self.found = true;
 | 
			
		||||
                        self.later_pos = self.pos + 1;
 | 
			
		||||
                        return QueryResult::Next;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -202,7 +202,6 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> {
 | 
			
		|||
                    if e.visible() {
 | 
			
		||||
                        self.values.push(e);
 | 
			
		||||
                    }
 | 
			
		||||
                    self.later_pos += 1;
 | 
			
		||||
                }
 | 
			
		||||
                QueryResult::Next
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
 | 
			
		|||
 | 
			
		||||
use crate::{
 | 
			
		||||
    storage::{parse, Change as StoredChange, ReadChangeOpError},
 | 
			
		||||
    ApplyOptions, Automerge, AutomergeError, Change, ChangeHash, OpObserver,
 | 
			
		||||
    Automerge, AutomergeError, Change, ChangeHash, OpObserver,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod bloom;
 | 
			
		||||
| 
						 | 
				
			
			@ -105,14 +105,14 @@ impl Automerge {
 | 
			
		|||
        sync_state: &mut State,
 | 
			
		||||
        message: Message,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.receive_sync_message_with::<()>(sync_state, message, ApplyOptions::default())
 | 
			
		||||
        self.receive_sync_message_with::<()>(sync_state, message, None)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn receive_sync_message_with<'a, Obs: OpObserver>(
 | 
			
		||||
    pub fn receive_sync_message_with<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        sync_state: &mut State,
 | 
			
		||||
        message: Message,
 | 
			
		||||
        options: ApplyOptions<'a, Obs>,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        let before_heads = self.get_heads();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +125,7 @@ impl Automerge {
 | 
			
		|||
 | 
			
		||||
        let changes_is_empty = message_changes.is_empty();
 | 
			
		||||
        if !changes_is_empty {
 | 
			
		||||
            self.apply_changes_with(message_changes, options)?;
 | 
			
		||||
            self.apply_changes_with(message_changes, op_observer)?;
 | 
			
		||||
            sync_state.shared_heads = advance_heads(
 | 
			
		||||
                &before_heads.iter().collect(),
 | 
			
		||||
                &self.get_heads().into_iter().collect(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
mod commit;
 | 
			
		||||
mod inner;
 | 
			
		||||
mod manual_transaction;
 | 
			
		||||
pub(crate) mod observation;
 | 
			
		||||
mod result;
 | 
			
		||||
mod transactable;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +9,8 @@ pub use self::commit::CommitOptions;
 | 
			
		|||
pub use self::transactable::Transactable;
 | 
			
		||||
pub(crate) use inner::TransactionInner;
 | 
			
		||||
pub use manual_transaction::Transaction;
 | 
			
		||||
pub use observation::{Observation, Observed, UnObserved};
 | 
			
		||||
pub use result::Failure;
 | 
			
		||||
pub use result::Success;
 | 
			
		||||
 | 
			
		||||
pub type Result<O, E> = std::result::Result<Success<O>, Failure<E>>;
 | 
			
		||||
pub type Result<O, Obs, E> = std::result::Result<Success<O, Obs>, Failure<E>>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,11 @@
 | 
			
		|||
/// Optional metadata for a commit.
 | 
			
		||||
#[derive(Debug, Default)]
 | 
			
		||||
pub struct CommitOptions<'a, Obs> {
 | 
			
		||||
pub struct CommitOptions {
 | 
			
		||||
    pub message: Option<String>,
 | 
			
		||||
    pub time: Option<i64>,
 | 
			
		||||
    pub op_observer: Option<&'a mut Obs>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a, Obs> CommitOptions<'a, Obs> {
 | 
			
		||||
impl CommitOptions {
 | 
			
		||||
    /// Add a message to the commit.
 | 
			
		||||
    pub fn with_message<S: Into<String>>(mut self, message: S) -> Self {
 | 
			
		||||
        self.message = Some(message.into());
 | 
			
		||||
| 
						 | 
				
			
			@ -30,14 +29,4 @@ impl<'a, Obs> CommitOptions<'a, Obs> {
 | 
			
		|||
        self.time = Some(time);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_op_observer(mut self, op_observer: &'a mut Obs) -> Self {
 | 
			
		||||
        self.op_observer = Some(op_observer);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_op_observer(&mut self, op_observer: &'a mut Obs) -> &mut Self {
 | 
			
		||||
        self.op_observer = Some(op_observer);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,13 +26,12 @@ impl TransactionInner {
 | 
			
		|||
 | 
			
		||||
    /// Commit the operations performed in this transaction, returning the hashes corresponding to
 | 
			
		||||
    /// the new heads.
 | 
			
		||||
    #[tracing::instrument(skip(self, doc, op_observer))]
 | 
			
		||||
    pub(crate) fn commit<Obs: OpObserver>(
 | 
			
		||||
    #[tracing::instrument(skip(self, doc))]
 | 
			
		||||
    pub(crate) fn commit(
 | 
			
		||||
        mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        message: Option<String>,
 | 
			
		||||
        time: Option<i64>,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
    ) -> ChangeHash {
 | 
			
		||||
        if message.is_some() {
 | 
			
		||||
            self.message = message;
 | 
			
		||||
| 
						 | 
				
			
			@ -42,26 +41,6 @@ impl TransactionInner {
 | 
			
		|||
            self.time = t;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let Some(observer) = op_observer {
 | 
			
		||||
            for (obj, prop, op) in &self.operations {
 | 
			
		||||
                let ex_obj = doc.ops.id_to_exid(obj.0);
 | 
			
		||||
                if op.insert {
 | 
			
		||||
                    let value = (op.value(), doc.id_to_exid(op.id));
 | 
			
		||||
                    match prop {
 | 
			
		||||
                        Prop::Map(_) => panic!("insert into a map"),
 | 
			
		||||
                        Prop::Seq(index) => observer.insert(ex_obj, *index, value),
 | 
			
		||||
                    }
 | 
			
		||||
                } else if op.is_delete() {
 | 
			
		||||
                    observer.delete(ex_obj, prop.clone());
 | 
			
		||||
                } else if let Some(value) = op.get_increment_value() {
 | 
			
		||||
                    observer.increment(ex_obj, prop.clone(), (value, doc.id_to_exid(op.id)));
 | 
			
		||||
                } else {
 | 
			
		||||
                    let value = (op.value(), doc.ops.id_to_exid(op.id));
 | 
			
		||||
                    observer.put(ex_obj, prop.clone(), value, false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let num_ops = self.pending_ops();
 | 
			
		||||
        let change = self.export(&doc.ops.m);
 | 
			
		||||
        let hash = change.hash();
 | 
			
		||||
| 
						 | 
				
			
			@ -150,9 +129,10 @@ impl TransactionInner {
 | 
			
		|||
    /// - The object does not exist
 | 
			
		||||
    /// - The key is the wrong type for the object
 | 
			
		||||
    /// - The key does not exist in the object
 | 
			
		||||
    pub(crate) fn put<P: Into<Prop>, V: Into<ScalarValue>>(
 | 
			
		||||
    pub(crate) fn put<P: Into<Prop>, V: Into<ScalarValue>, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        ex_obj: &ExId,
 | 
			
		||||
        prop: P,
 | 
			
		||||
        value: V,
 | 
			
		||||
| 
						 | 
				
			
			@ -160,7 +140,7 @@ impl TransactionInner {
 | 
			
		|||
        let obj = doc.exid_to_obj(ex_obj)?;
 | 
			
		||||
        let value = value.into();
 | 
			
		||||
        let prop = prop.into();
 | 
			
		||||
        self.local_op(doc, obj, prop, value.into())?;
 | 
			
		||||
        self.local_op(doc, op_observer, obj, prop, value.into())?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -177,16 +157,19 @@ impl TransactionInner {
 | 
			
		|||
    /// - The object does not exist
 | 
			
		||||
    /// - The key is the wrong type for the object
 | 
			
		||||
    /// - The key does not exist in the object
 | 
			
		||||
    pub(crate) fn put_object<P: Into<Prop>>(
 | 
			
		||||
    pub(crate) fn put_object<P: Into<Prop>, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        ex_obj: &ExId,
 | 
			
		||||
        prop: P,
 | 
			
		||||
        value: ObjType,
 | 
			
		||||
    ) -> Result<ExId, AutomergeError> {
 | 
			
		||||
        let obj = doc.exid_to_obj(ex_obj)?;
 | 
			
		||||
        let prop = prop.into();
 | 
			
		||||
        let id = self.local_op(doc, obj, prop, value.into())?.unwrap();
 | 
			
		||||
        let id = self
 | 
			
		||||
            .local_op(doc, op_observer, obj, prop, value.into())?
 | 
			
		||||
            .unwrap();
 | 
			
		||||
        let id = doc.id_to_exid(id);
 | 
			
		||||
        Ok(id)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -195,9 +178,11 @@ impl TransactionInner {
 | 
			
		|||
        OpId(self.start_op.get() + self.pending_ops() as u64, self.actor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn insert_local_op(
 | 
			
		||||
    #[allow(clippy::too_many_arguments)]
 | 
			
		||||
    fn insert_local_op<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        op: Op,
 | 
			
		||||
        pos: usize,
 | 
			
		||||
| 
						 | 
				
			
			@ -210,12 +195,13 @@ impl TransactionInner {
 | 
			
		|||
            doc.ops.insert(pos, &obj, op.clone());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.operations.push((obj, prop, op));
 | 
			
		||||
        self.finalize_op(doc, op_observer, obj, prop, op);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn insert<V: Into<ScalarValue>>(
 | 
			
		||||
    pub(crate) fn insert<V: Into<ScalarValue>, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        ex_obj: &ExId,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        value: V,
 | 
			
		||||
| 
						 | 
				
			
			@ -223,26 +209,28 @@ impl TransactionInner {
 | 
			
		|||
        let obj = doc.exid_to_obj(ex_obj)?;
 | 
			
		||||
        let value = value.into();
 | 
			
		||||
        tracing::trace!(obj=?obj, value=?value, "inserting value");
 | 
			
		||||
        self.do_insert(doc, obj, index, value.into())?;
 | 
			
		||||
        self.do_insert(doc, op_observer, obj, index, value.into())?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn insert_object(
 | 
			
		||||
    pub(crate) fn insert_object<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        ex_obj: &ExId,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        value: ObjType,
 | 
			
		||||
    ) -> Result<ExId, AutomergeError> {
 | 
			
		||||
        let obj = doc.exid_to_obj(ex_obj)?;
 | 
			
		||||
        let id = self.do_insert(doc, obj, index, value.into())?;
 | 
			
		||||
        let id = self.do_insert(doc, op_observer, obj, index, value.into())?;
 | 
			
		||||
        let id = doc.id_to_exid(id);
 | 
			
		||||
        Ok(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn do_insert(
 | 
			
		||||
    fn do_insert<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        action: OpType,
 | 
			
		||||
| 
						 | 
				
			
			@ -263,27 +251,30 @@ impl TransactionInner {
 | 
			
		|||
        };
 | 
			
		||||
 | 
			
		||||
        doc.ops.insert(query.pos(), &obj, op.clone());
 | 
			
		||||
        self.operations.push((obj, Prop::Seq(index), op));
 | 
			
		||||
 | 
			
		||||
        self.finalize_op(doc, op_observer, obj, Prop::Seq(index), op);
 | 
			
		||||
 | 
			
		||||
        Ok(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn local_op(
 | 
			
		||||
    pub(crate) fn local_op<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        action: OpType,
 | 
			
		||||
    ) -> Result<Option<OpId>, AutomergeError> {
 | 
			
		||||
        match prop {
 | 
			
		||||
            Prop::Map(s) => self.local_map_op(doc, obj, s, action),
 | 
			
		||||
            Prop::Seq(n) => self.local_list_op(doc, obj, n, action),
 | 
			
		||||
            Prop::Map(s) => self.local_map_op(doc, op_observer, obj, s, action),
 | 
			
		||||
            Prop::Seq(n) => self.local_list_op(doc, op_observer, obj, n, action),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn local_map_op(
 | 
			
		||||
    fn local_map_op<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        prop: String,
 | 
			
		||||
        action: OpType,
 | 
			
		||||
| 
						 | 
				
			
			@ -324,14 +315,15 @@ impl TransactionInner {
 | 
			
		|||
 | 
			
		||||
        let pos = query.pos;
 | 
			
		||||
        let ops_pos = query.ops_pos;
 | 
			
		||||
        self.insert_local_op(doc, Prop::Map(prop), op, pos, obj, &ops_pos);
 | 
			
		||||
        self.insert_local_op(doc, op_observer, Prop::Map(prop), op, pos, obj, &ops_pos);
 | 
			
		||||
 | 
			
		||||
        Ok(Some(id))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn local_list_op(
 | 
			
		||||
    fn local_list_op<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        index: usize,
 | 
			
		||||
        action: OpType,
 | 
			
		||||
| 
						 | 
				
			
			@ -363,40 +355,43 @@ impl TransactionInner {
 | 
			
		|||
 | 
			
		||||
        let pos = query.pos;
 | 
			
		||||
        let ops_pos = query.ops_pos;
 | 
			
		||||
        self.insert_local_op(doc, Prop::Seq(index), op, pos, obj, &ops_pos);
 | 
			
		||||
        self.insert_local_op(doc, op_observer, Prop::Seq(index), op, pos, obj, &ops_pos);
 | 
			
		||||
 | 
			
		||||
        Ok(Some(id))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn increment<P: Into<Prop>>(
 | 
			
		||||
    pub(crate) fn increment<P: Into<Prop>, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        obj: &ExId,
 | 
			
		||||
        prop: P,
 | 
			
		||||
        value: i64,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        let obj = doc.exid_to_obj(obj)?;
 | 
			
		||||
        self.local_op(doc, obj, prop.into(), OpType::Increment(value))?;
 | 
			
		||||
        self.local_op(doc, op_observer, obj, prop.into(), OpType::Increment(value))?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn delete<P: Into<Prop>>(
 | 
			
		||||
    pub(crate) fn delete<P: Into<Prop>, Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        ex_obj: &ExId,
 | 
			
		||||
        prop: P,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        let obj = doc.exid_to_obj(ex_obj)?;
 | 
			
		||||
        let prop = prop.into();
 | 
			
		||||
        self.local_op(doc, obj, prop, OpType::Delete)?;
 | 
			
		||||
        self.local_op(doc, op_observer, obj, prop, OpType::Delete)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
 | 
			
		||||
    /// the new elements
 | 
			
		||||
    pub(crate) fn splice(
 | 
			
		||||
    pub(crate) fn splice<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        mut op_observer: Option<&mut Obs>,
 | 
			
		||||
        ex_obj: &ExId,
 | 
			
		||||
        mut pos: usize,
 | 
			
		||||
        del: usize,
 | 
			
		||||
| 
						 | 
				
			
			@ -404,16 +399,59 @@ impl TransactionInner {
 | 
			
		|||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        let obj = doc.exid_to_obj(ex_obj)?;
 | 
			
		||||
        for _ in 0..del {
 | 
			
		||||
            // del()
 | 
			
		||||
            self.local_op(doc, obj, pos.into(), OpType::Delete)?;
 | 
			
		||||
            // This unwrap and rewrap of the option is necessary to appeas the borrow checker :(
 | 
			
		||||
            if let Some(obs) = op_observer.as_mut() {
 | 
			
		||||
                self.local_op(doc, Some(*obs), obj, pos.into(), OpType::Delete)?;
 | 
			
		||||
            } else {
 | 
			
		||||
                self.local_op::<Obs>(doc, None, obj, pos.into(), OpType::Delete)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        for v in vals {
 | 
			
		||||
            // insert()
 | 
			
		||||
            self.do_insert(doc, obj, pos, v.clone().into())?;
 | 
			
		||||
            // As above this unwrap and rewrap of the option is necessary to appeas the borrow checker :(
 | 
			
		||||
            if let Some(obs) = op_observer.as_mut() {
 | 
			
		||||
                self.do_insert(doc, Some(*obs), obj, pos, v.clone().into())?;
 | 
			
		||||
            } else {
 | 
			
		||||
                self.do_insert::<Obs>(doc, None, obj, pos, v.clone().into())?;
 | 
			
		||||
            }
 | 
			
		||||
            pos += 1;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn finalize_op<Obs: OpObserver>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        doc: &mut Automerge,
 | 
			
		||||
        op_observer: Option<&mut Obs>,
 | 
			
		||||
        obj: ObjId,
 | 
			
		||||
        prop: Prop,
 | 
			
		||||
        op: Op,
 | 
			
		||||
    ) {
 | 
			
		||||
        // TODO - id_to_exid should be a noop if not used - change type to Into<ExId>?
 | 
			
		||||
        if let Some(op_observer) = op_observer {
 | 
			
		||||
            let ex_obj = doc.ops.id_to_exid(obj.0);
 | 
			
		||||
            let parents = doc.ops.parents(obj);
 | 
			
		||||
            if op.insert {
 | 
			
		||||
                let value = (op.value(), doc.ops.id_to_exid(op.id));
 | 
			
		||||
                match prop {
 | 
			
		||||
                    Prop::Map(_) => panic!("insert into a map"),
 | 
			
		||||
                    Prop::Seq(index) => op_observer.insert(parents, ex_obj, index, value),
 | 
			
		||||
                }
 | 
			
		||||
            } else if op.is_delete() {
 | 
			
		||||
                op_observer.delete(parents, ex_obj, prop.clone());
 | 
			
		||||
            } else if let Some(value) = op.get_increment_value() {
 | 
			
		||||
                op_observer.increment(
 | 
			
		||||
                    parents,
 | 
			
		||||
                    ex_obj,
 | 
			
		||||
                    prop.clone(),
 | 
			
		||||
                    (value, doc.ops.id_to_exid(op.id)),
 | 
			
		||||
                );
 | 
			
		||||
            } else {
 | 
			
		||||
                let value = (op.value(), doc.ops.id_to_exid(op.id));
 | 
			
		||||
                op_observer.put(parents, ex_obj, prop.clone(), value, false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        self.operations.push((obj, prop, op));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ use crate::{Automerge, ChangeHash, KeysAt, ObjType, OpObserver, Prop, ScalarValu
 | 
			
		|||
use crate::{AutomergeError, Keys};
 | 
			
		||||
use crate::{ListRange, ListRangeAt, MapRange, MapRangeAt};
 | 
			
		||||
 | 
			
		||||
use super::{CommitOptions, Transactable, TransactionInner};
 | 
			
		||||
use super::{observation, CommitOptions, Transactable, TransactionInner};
 | 
			
		||||
 | 
			
		||||
/// A transaction on a document.
 | 
			
		||||
/// Transactions group operations into a single change so that no other operations can happen
 | 
			
		||||
| 
						 | 
				
			
			@ -20,14 +20,22 @@ use super::{CommitOptions, Transactable, TransactionInner};
 | 
			
		|||
/// intermediate state.
 | 
			
		||||
/// This is consistent with `?` error handling.
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct Transaction<'a> {
 | 
			
		||||
pub struct Transaction<'a, Obs: observation::Observation> {
 | 
			
		||||
    // this is an option so that we can take it during commit and rollback to prevent it being
 | 
			
		||||
    // rolled back during drop.
 | 
			
		||||
    pub(crate) inner: Option<TransactionInner>,
 | 
			
		||||
    // As with `inner` this is an `Option` so we can `take` it during `commit`
 | 
			
		||||
    pub(crate) observation: Option<Obs>,
 | 
			
		||||
    pub(crate) doc: &'a mut Automerge,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Transaction<'a> {
 | 
			
		||||
impl<'a, Obs: OpObserver> Transaction<'a, observation::Observed<Obs>> {
 | 
			
		||||
    pub fn observer(&mut self) -> &mut Obs {
 | 
			
		||||
        self.observation.as_mut().unwrap().observer()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a, Obs: observation::Observation> Transaction<'a, Obs> {
 | 
			
		||||
    /// Get the heads of the document before this transaction was started.
 | 
			
		||||
    pub fn get_heads(&self) -> Vec<ChangeHash> {
 | 
			
		||||
        self.doc.get_heads()
 | 
			
		||||
| 
						 | 
				
			
			@ -35,11 +43,11 @@ impl<'a> Transaction<'a> {
 | 
			
		|||
 | 
			
		||||
    /// Commit the operations performed in this transaction, returning the hashes corresponding to
 | 
			
		||||
    /// the new heads.
 | 
			
		||||
    pub fn commit(mut self) -> ChangeHash {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .take()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .commit::<()>(self.doc, None, None, None)
 | 
			
		||||
    pub fn commit(mut self) -> Obs::CommitResult {
 | 
			
		||||
        let tx = self.inner.take().unwrap();
 | 
			
		||||
        let hash = tx.commit(self.doc, None, None);
 | 
			
		||||
        let obs = self.observation.take().unwrap();
 | 
			
		||||
        obs.make_result(hash)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Commit the operations in this transaction with some options.
 | 
			
		||||
| 
						 | 
				
			
			@ -56,15 +64,13 @@ impl<'a> Transaction<'a> {
 | 
			
		|||
    /// tx.put_object(ROOT, "todos", ObjType::List).unwrap();
 | 
			
		||||
    /// let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as
 | 
			
		||||
    /// i64;
 | 
			
		||||
    /// tx.commit_with::<()>(CommitOptions::default().with_message("Create todos list").with_time(now));
 | 
			
		||||
    /// tx.commit_with(CommitOptions::default().with_message("Create todos list").with_time(now));
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn commit_with<Obs: OpObserver>(mut self, options: CommitOptions<'_, Obs>) -> ChangeHash {
 | 
			
		||||
        self.inner.take().unwrap().commit(
 | 
			
		||||
            self.doc,
 | 
			
		||||
            options.message,
 | 
			
		||||
            options.time,
 | 
			
		||||
            options.op_observer,
 | 
			
		||||
        )
 | 
			
		||||
    pub fn commit_with(mut self, options: CommitOptions) -> Obs::CommitResult {
 | 
			
		||||
        let tx = self.inner.take().unwrap();
 | 
			
		||||
        let hash = tx.commit(self.doc, options.message, options.time);
 | 
			
		||||
        let obs = self.observation.take().unwrap();
 | 
			
		||||
        obs.make_result(hash)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Undo the operations added in this transaction, returning the number of cancelled
 | 
			
		||||
| 
						 | 
				
			
			@ -72,9 +78,21 @@ impl<'a> Transaction<'a> {
 | 
			
		|||
    pub fn rollback(mut self) -> usize {
 | 
			
		||||
        self.inner.take().unwrap().rollback(self.doc)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn do_tx<F, O>(&mut self, f: F) -> O
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut TransactionInner, &mut Automerge, Option<&mut Obs::Obs>) -> O,
 | 
			
		||||
    {
 | 
			
		||||
        let tx = self.inner.as_mut().unwrap();
 | 
			
		||||
        if let Some(obs) = self.observation.as_mut() {
 | 
			
		||||
            f(tx, self.doc, obs.observer())
 | 
			
		||||
        } else {
 | 
			
		||||
            f(tx, self.doc, None)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Transactable for Transaction<'a> {
 | 
			
		||||
impl<'a, Obs: observation::Observation> Transactable for Transaction<'a, Obs> {
 | 
			
		||||
    /// Get the number of pending operations in this transaction.
 | 
			
		||||
    fn pending_ops(&self) -> usize {
 | 
			
		||||
        self.inner.as_ref().unwrap().pending_ops()
 | 
			
		||||
| 
						 | 
				
			
			@ -94,10 +112,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
        prop: P,
 | 
			
		||||
        value: V,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .put(self.doc, obj.as_ref(), prop, value)
 | 
			
		||||
        self.do_tx(|tx, doc, obs| tx.put(doc, obs, obj.as_ref(), prop, value))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn put_object<O: AsRef<ExId>, P: Into<Prop>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -106,10 +121,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
        prop: P,
 | 
			
		||||
        value: ObjType,
 | 
			
		||||
    ) -> Result<ExId, AutomergeError> {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .put_object(self.doc, obj.as_ref(), prop, value)
 | 
			
		||||
        self.do_tx(|tx, doc, obs| tx.put_object(doc, obs, obj.as_ref(), prop, value))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn insert<O: AsRef<ExId>, V: Into<ScalarValue>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -118,10 +130,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
        index: usize,
 | 
			
		||||
        value: V,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .insert(self.doc, obj.as_ref(), index, value)
 | 
			
		||||
        self.do_tx(|tx, doc, obs| tx.insert(doc, obs, obj.as_ref(), index, value))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn insert_object<O: AsRef<ExId>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -130,10 +139,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
        index: usize,
 | 
			
		||||
        value: ObjType,
 | 
			
		||||
    ) -> Result<ExId, AutomergeError> {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .insert_object(self.doc, obj.as_ref(), index, value)
 | 
			
		||||
        self.do_tx(|tx, doc, obs| tx.insert_object(doc, obs, obj.as_ref(), index, value))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn increment<O: AsRef<ExId>, P: Into<Prop>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -142,10 +148,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
        prop: P,
 | 
			
		||||
        value: i64,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .increment(self.doc, obj.as_ref(), prop, value)
 | 
			
		||||
        self.do_tx(|tx, doc, obs| tx.increment(doc, obs, obj.as_ref(), prop, value))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn delete<O: AsRef<ExId>, P: Into<Prop>>(
 | 
			
		||||
| 
						 | 
				
			
			@ -153,10 +156,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
        obj: O,
 | 
			
		||||
        prop: P,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .delete(self.doc, obj.as_ref(), prop)
 | 
			
		||||
        self.do_tx(|tx, doc, obs| tx.delete(doc, obs, obj.as_ref(), prop))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
 | 
			
		||||
| 
						 | 
				
			
			@ -168,10 +168,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
        del: usize,
 | 
			
		||||
        vals: V,
 | 
			
		||||
    ) -> Result<(), AutomergeError> {
 | 
			
		||||
        self.inner
 | 
			
		||||
            .as_mut()
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .splice(self.doc, obj.as_ref(), pos, del, vals)
 | 
			
		||||
        self.do_tx(|tx, doc, obs| tx.splice(doc, obs, obj.as_ref(), pos, del, vals))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn keys<O: AsRef<ExId>>(&self, obj: O) -> Keys<'_, '_> {
 | 
			
		||||
| 
						 | 
				
			
			@ -291,7 +288,7 @@ impl<'a> Transactable for Transaction<'a> {
 | 
			
		|||
// intermediate state.
 | 
			
		||||
// This defaults to rolling back the transaction to be compatible with `?` error returning before
 | 
			
		||||
// reaching a call to `commit`.
 | 
			
		||||
impl<'a> Drop for Transaction<'a> {
 | 
			
		||||
impl<'a, Obs: observation::Observation> Drop for Transaction<'a, Obs> {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        if let Some(txn) = self.inner.take() {
 | 
			
		||||
            txn.rollback(self.doc);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										78
									
								
								automerge/src/transaction/observation.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								automerge/src/transaction/observation.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
//! This module is essentially a type level Option. It is used in sitations where we know at
 | 
			
		||||
//! compile time whether an `OpObserver` is available to track changes in a transaction.
 | 
			
		||||
use crate::{ChangeHash, OpObserver};
 | 
			
		||||
 | 
			
		||||
mod private {
 | 
			
		||||
    pub trait Sealed {}
 | 
			
		||||
    impl<O: super::OpObserver> Sealed for super::Observed<O> {}
 | 
			
		||||
    impl Sealed for super::UnObserved {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait Observation: private::Sealed {
 | 
			
		||||
    type Obs: OpObserver;
 | 
			
		||||
    type CommitResult;
 | 
			
		||||
 | 
			
		||||
    fn observer(&mut self) -> Option<&mut Self::Obs>;
 | 
			
		||||
    fn make_result(self, hash: ChangeHash) -> Self::CommitResult;
 | 
			
		||||
    fn branch(&self) -> Self;
 | 
			
		||||
    fn merge(&mut self, other: &Self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct Observed<Obs: OpObserver>(Obs);
 | 
			
		||||
 | 
			
		||||
impl<O: OpObserver> Observed<O> {
 | 
			
		||||
    pub(crate) fn new(o: O) -> Self {
 | 
			
		||||
        Self(o)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn observer(&mut self) -> &mut O {
 | 
			
		||||
        &mut self.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<Obs: OpObserver> Observation for Observed<Obs> {
 | 
			
		||||
    type Obs = Obs;
 | 
			
		||||
    type CommitResult = (Obs, ChangeHash);
 | 
			
		||||
    fn observer(&mut self) -> Option<&mut Self::Obs> {
 | 
			
		||||
        Some(&mut self.0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn make_result(self, hash: ChangeHash) -> Self::CommitResult {
 | 
			
		||||
        (self.0, hash)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn branch(&self) -> Self {
 | 
			
		||||
        Self(self.0.branch())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn merge(&mut self, other: &Self) {
 | 
			
		||||
        self.0.merge(&other.0)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Default, Debug)]
 | 
			
		||||
pub struct UnObserved;
 | 
			
		||||
impl UnObserved {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Observation for UnObserved {
 | 
			
		||||
    type Obs = ();
 | 
			
		||||
    type CommitResult = ChangeHash;
 | 
			
		||||
    fn observer(&mut self) -> Option<&mut Self::Obs> {
 | 
			
		||||
        None
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn make_result(self, hash: ChangeHash) -> Self::CommitResult {
 | 
			
		||||
        hash
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn branch(&self) -> Self {
 | 
			
		||||
        Self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn merge(&mut self, _other: &Self) {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,11 +2,12 @@ use crate::ChangeHash;
 | 
			
		|||
 | 
			
		||||
/// The result of a successful, and committed, transaction.
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct Success<O> {
 | 
			
		||||
pub struct Success<O, Obs> {
 | 
			
		||||
    /// The result of the transaction.
 | 
			
		||||
    pub result: O,
 | 
			
		||||
    /// The hash of the change, also the head of the document.
 | 
			
		||||
    pub hash: ChangeHash,
 | 
			
		||||
    pub op_observer: Obs,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The result of a failed, and rolled back, transaction.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
use automerge::transaction::Transactable;
 | 
			
		||||
use automerge::{
 | 
			
		||||
    ActorId, ApplyOptions, AutoCommit, Automerge, AutomergeError, Change, ExpandedChange, ObjType,
 | 
			
		||||
    ScalarValue, VecOpObserver, ROOT,
 | 
			
		||||
    ActorId, AutoCommit, Automerge, AutomergeError, Change, ExpandedChange, ObjType, ScalarValue,
 | 
			
		||||
    VecOpObserver, ROOT,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// set up logging for all the tests
 | 
			
		||||
| 
						 | 
				
			
			@ -1005,13 +1005,8 @@ fn observe_counter_change_application() {
 | 
			
		|||
    doc.increment(ROOT, "counter", 5).unwrap();
 | 
			
		||||
    let changes = doc.get_changes(&[]).unwrap().into_iter().cloned();
 | 
			
		||||
 | 
			
		||||
    let mut doc = AutoCommit::new();
 | 
			
		||||
    let mut observer = VecOpObserver::default();
 | 
			
		||||
    doc.apply_changes_with(
 | 
			
		||||
        changes,
 | 
			
		||||
        ApplyOptions::default().with_op_observer(&mut observer),
 | 
			
		||||
    )
 | 
			
		||||
    .unwrap();
 | 
			
		||||
    let mut doc = AutoCommit::new().with_observer(VecOpObserver::default());
 | 
			
		||||
    doc.apply_changes(changes).unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue