UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

216 lines (215 loc) 7.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonPatch = void 0; const deepEqual_1 = require("@jsonjoy.com/util/lib/json-equal/deepEqual"); const nodes_1 = require("../nodes"); const util_1 = require("@jsonjoy.com/json-pointer/lib/util"); const clock_1 = require("../../json-crdt-patch/clock"); class JsonPatch { constructor(model, pfx = []) { this.model = model; this.pfx = pfx; } apply(ops) { const length = ops.length; this.model.api.transaction(() => { for (let i = 0; i < length; i++) this.applyOp(ops[i]); }); return this; } applyOp(op) { switch (op.op) { case 'add': this.add(op.path, op.value); break; case 'remove': this.remove(op.path); break; case 'replace': this.replace(op.path, op.value); break; case 'move': this.move(op.path, op.from); break; case 'copy': this.copy(op.path, op.from); break; case 'test': this.test(op.path, op.value); break; case 'str_ins': this.strIns(op.path, op.pos, op.str); break; case 'str_del': this.strDel(op.path, op.pos, op.len ?? 0, op.str); break; default: throw new Error('UNKNOWN_OP'); } this.model.api.apply(); return this; } builder() { return this.model.api.builder; } toPath(path) { return this.pfx.concat((0, util_1.toPath)(path)); } add(path, value) { const builder = this.builder(); const steps = this.toPath(path); if (!steps.length) this.setRoot(value); else { const objSteps = steps.slice(0, steps.length - 1); const node = this.model.api.find(objSteps); const key = steps[steps.length - 1]; if (node instanceof nodes_1.ObjNode) { builder.insObj(node.id, [[String(key), builder.json(value)]]); // TODO: see if "con" nodes can be used here in some cases. } else if (node instanceof nodes_1.ArrNode) { const builderValue = builder.json(value); if (key === '-') { const length = node.length(); const after = node.find(length - 1) || node.id; builder.insArr(node.id, after, [builderValue]); } else { const index = ~~key; if ('' + index !== key) throw new Error('INVALID_INDEX'); if (!index) builder.insArr(node.id, node.id, [builderValue]); else { const after = node.find(index - 1); if (!after) throw new Error('NOT_FOUND'); builder.insArr(node.id, after, [builderValue]); } } } else throw new Error('NOT_FOUND'); } } remove(path) { const builder = this.builder(); const steps = this.toPath(path); if (!steps.length) this.setRoot(null); else { const objSteps = steps.slice(0, steps.length - 1); const node = this.model.api.find(objSteps); const key = steps[steps.length - 1]; if (node instanceof nodes_1.ObjNode) { const stringKey = String(key); const valueNode = node.get(stringKey); if (valueNode === undefined) throw new Error('NOT_FOUND'); if (valueNode instanceof nodes_1.ConNode && valueNode.val === undefined) throw new Error('NOT_FOUND'); builder.insObj(node.id, [[stringKey, builder.const(undefined)]]); } else if (node instanceof nodes_1.ArrNode) { const key = steps[steps.length - 1]; const index = ~~key; if (typeof key === 'string' && '' + index !== key) throw new Error('INVALID_INDEX'); const id = node.find(index); if (!id) throw new Error('NOT_FOUND'); builder.del(node.id, [(0, clock_1.interval)(id, 0, 1)]); } else throw new Error('NOT_FOUND'); } } replace(path, value) { this.remove(path); this.add(path, value); } move(path, from) { path = (0, util_1.toPath)(path); from = (0, util_1.toPath)(from); if ((0, util_1.isChild)(from, path)) throw new Error('INVALID_CHILD'); const json = this.json(this.toPath(from)); this.remove(from); this.add(path, json); } copy(path, from) { path = (0, util_1.toPath)(path); const json = this.json(this.toPath(from)); this.add(path, json); } test(path, value) { path = this.toPath(path); const json = this.json(path); if (!(0, deepEqual_1.deepEqual)(json, value)) throw new Error('TEST'); } strIns(path, pos, str) { path = this.toPath(path); const { node } = this.model.api.str(path); const length = node.length(); const after = pos ? node.find(length < pos ? length - 1 : pos - 1) : node.id; if (!after) throw new Error('OUT_OF_BOUNDS'); this.builder().insStr(node.id, after, str); } strDel(path, pos, len, str = '') { path = this.toPath(path); const { node } = this.model.api.str(path); const length = node.length(); if (length <= pos) return; const deletionLength = Math.min(len ?? str.length, length - pos); const range = node.findInterval(pos, deletionLength); if (!range) throw new Error('OUT_OF_BOUNDS'); this.builder().del(node.id, range); } get(path) { return this._get(this.toPath(path)); } _get(steps) { const model = this.model; if (!steps.length) return model.view(); else { try { const objSteps = steps.slice(0, steps.length - 1); const node = model.api.find(objSteps); const key = steps[steps.length - 1]; if (node instanceof nodes_1.ObjNode) { return node.get(String(key))?.view(); } else if (node instanceof nodes_1.ArrNode) { const index = ~~key; if ('' + index !== key) throw new Error('INVALID_INDEX'); const arrNode = node.getNode(index); if (!arrNode) throw new Error('NOT_FOUND'); return arrNode.view(); } } catch { return; } } return undefined; } json(steps) { const json = this._get(steps); if (json === undefined) throw new Error('NOT_FOUND'); return json; } setRoot(json) { const builder = this.builder(); builder.root(builder.json(json)); } } exports.JsonPatch = JsonPatch;