import { describe, it } from 'mocha'; //@ts-ignore import assert from 'assert' //@ts-ignore import init, { create, load, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' import { Prop } from '..'; function patchValue(patch: any) : any { switch (patch.datatype) { case "map": return {} case "list": return [] case "text": return "" default: return patch.value } } function patchTextValue(patch: any) : any { if (typeof patch.value === "string" && patch.value.length == 1) { return patch.value } else { return "\uFFFC" } } function applyPatch(obj: any, path: Prop[], patch: any) : any { let prop = path.shift(); if (typeof prop === 'number' && Array.isArray(obj)) { return applyPatchToArray(obj, prop, path, patch) } if (typeof prop === 'number' && typeof obj === 'string') { return applyPatchToText(obj, prop, path, patch) } if (typeof prop === 'string' && typeof obj === 'object') { return applyPatchToObject(obj, prop, path, patch) } return obj } type Obj = { [key:string]: any } function applyPatchToObject(obj: Obj, prop: string, path: Prop[], patch: any) : any { if (path.length === 0) { switch (patch.action) { case "increment": return { ... obj, [prop]: obj[prop] + patchValue(patch) } case "put": return { ... obj, [prop]: patchValue(patch) } case "delete": let tmp = { ... obj } delete tmp[prop] return tmp default: throw new RangeError(`Invalid patch ${patch}`) } } else { return { ... obj, [prop]: applyPatch(obj[prop], path, patch) } } } function applyPatchToArray(obj: Array, prop: number, path: Prop[], patch: any) : any { if (path.length === 0) { switch (patch.action) { case "increment": return [ ... obj.slice(0,prop), obj[prop] + patchValue(patch), ... obj.slice(prop + 1) ] case "put": return [ ... obj.slice(0,prop), patchValue(patch), ... obj.slice(prop + 1) ] case "insert": return [ ... obj.slice(0,prop), patchValue(patch), ... obj.slice(prop) ] case "delete": return [... obj.slice(0,prop), ... obj.slice(prop + 1) ] default: throw new RangeError(`Invalid patch ${patch}`) } } else { return [ ... obj.slice(0,prop), applyPatch(obj[prop], path, patch), ... obj.slice(prop + 1) ] } } function applyPatchToText(obj: string, prop: number, path: Prop[], patch: any) : any { if (path.length === 0) { switch (patch.action) { case "increment": return obj case "put": return obj.slice(0,prop) + patchTextValue(patch) + obj.slice(prop + 1) case "insert": return obj.slice(0,prop) + patchTextValue(patch) + obj.slice(prop) case "delete": return obj.slice(0,prop) + obj.slice(prop + 1) default: throw new RangeError(`Invalid patch ${patch}`) } } else { return obj } } function applyPatches(obj: any, patches: any) { for (let patch of patches) { obj = applyPatch(obj, patch.path, patch) } return obj } describe('Automerge', () => { describe('patches', () => { it.only('can apply nested patches', () => { const doc1 = create() doc1.enablePatches(true) doc1.put("/", "str", "value") doc1.put("/", "num", 0) doc1.delete("/", "num") doc1.put("/", "counter", 0, "counter") doc1.increment("/", "counter", 100) doc1.increment("/", "counter", 1) doc1.put("/", "bin", new Uint8Array([1,2,3])) doc1.put("/", "bool", true) let sub = doc1.putObject("/", "sub", {}) let list = doc1.putObject("/", "list", [1,2,3,4,5,6]) doc1.push("/list", 100, "counter"); doc1.increment("/list", 6, 10); let sublist = doc1.putObject("/sub", "list", [1,2,3,4,[ 1,2,3,[4,{ five: "six" } ] ] ]) doc1.put(sub, "str", "value") doc1.put("/sub", "num", 0) doc1.put("/sub", "bin", new Uint8Array([1,2,3])) doc1.put("/sub", "bool", true) let subsub = doc1.putObject("/sub", "sub", {}) doc1.put("/sub/sub", "num", 0) doc1.put("/sub/sub", "bin", new Uint8Array([1,2,3])) doc1.put("/sub/sub", "bool", true) let patches = doc1.popPatches() let js = applyPatches({}, patches) assert.deepEqual(js,doc1.materialize("/")) }) it.only('can handle deletes with nested patches', () => { const doc1 = create() doc1.enablePatches(true) let list = doc1.putObject("/", "list", [1,2,3,['a','b','c']]) doc1.delete("/list", 1); doc1.push("/list", 'hello'); let patches = doc1.popPatches() let js = applyPatches({}, patches) assert.deepEqual(js,doc1.materialize("/")) }) it.only('can handle patches with deletes withlists holding objects', () => { const doc1 = create() doc1.enablePatches(true) let list = doc1.putObject("/", "list", [1,2,3,[{n:1},{n:2},{n:3}]]) doc1.delete("/list", 1); doc1.put("/list/2/0", "n", 100); doc1.delete("/list", 1); doc1.put("/list/1/1", "n", 200); doc1.insertObject("/list/1", 3, {n:400}) let text = doc1.putObject("/", "text", "hello world"); doc1.insertObject("/text", 3, {n:1}) let patches = doc1.popPatches() console.log(doc1.materialize("/")) let js = applyPatches({}, patches) assert.deepEqual(js,doc1.materialize("/")) }) }) })