json-joy
Version:
Collection of libraries for building collaborative editing apps.
216 lines (215 loc) • 7.62 kB
JavaScript
"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;