json-joy
Version:
Collection of libraries for building collaborative editing apps.
403 lines (402 loc) • 14.8 kB
JavaScript
const toStringNode = (self, tab = '') => {
let children = '';
const last = self.children.size - 1;
let i = 0;
for (const [index, node] of self.children) {
const isLast = i === last;
children += `\n${tab}${isLast ? ' └─ ' : ' ├─ '}${node.toString(tab + (isLast ? ' ' : ' │ '))}`;
i++;
}
const indexFormatted = typeof self.index === 'number' ? `[${self.index}]` : `"${self.index}"`;
const registerFormatted = self.regId === undefined
? ''
: self.regId === -1
? ''
: ` [${self.regId}]`;
return `${indexFormatted} ${self.constructor.name}${registerFormatted}${children}`;
};
export class PickNode {
regId;
index;
path;
pathLength;
parent;
children;
constructor(index, path, pathLength) {
this.regId = -1;
this.index = index;
this.path = path;
this.pathLength = pathLength;
this.parent = null;
this.children = new Map();
}
toString(tab = '') {
return toStringNode(this, tab);
}
}
export class DropNode {
regId;
index;
path;
pathLength;
parent;
// edits: unknown[] = [];
children;
constructor(index, path, pathLength) {
this.regId = -1;
this.index = index;
this.path = path;
this.pathLength = pathLength;
this.parent = null;
this.children = new Map();
}
toString(tab = '') {
return toStringNode(this, tab);
}
clone() {
const clone = new DropNode(this.index, this.path, this.pathLength);
clone.regId = this.regId;
for (const [index, node] of this.children) {
const child = node.clone();
child.parent = clone;
clone.children.set(index, child);
}
return clone;
}
}
class PickRoot extends PickNode {
constructor() {
super(0, [], 0);
}
}
class DropRoot extends DropNode {
constructor() {
super(0, [], 0);
}
}
export class Register {
id;
pick = null;
data = undefined;
drops = [];
constructor(id) {
this.id = id;
}
addDrop(drop) {
this.drops.push(drop);
}
removeDrop(drop) {
const index = this.drops.findIndex((v) => v === drop);
if (index > -1)
this.drops.splice(index, 1);
}
toString() {
let drops = '';
if (this.drops.length)
drops = this.drops.map((drop) => '/' + drop.path.slice(0, drop.pathLength).join('/')).join(', ');
const src = this.pick
? '/' + this.pick.path.slice(0, this.pick.pathLength).join('/')
: `{ ${JSON.stringify(this.data)} }`;
const dst = drops ? `{ ${drops} }` : '∅';
return `${this.id}: Register ${src} ┈┈┈→ ${dst}`;
}
}
export class OpTree {
static from(op) {
const [test, pick = [], data = [], drop = [], edit = []] = op;
const tree = new OpTree();
if (test.length)
tree.test.push(...test);
for (let i = 0; i < pick.length; i++) {
const [registerId, what] = pick[i];
tree.addPickNode(registerId, what);
}
for (let i = 0; i < data.length; i++) {
const [registerId, value] = data[i];
tree.addData(registerId, value);
}
for (let i = 0; i < drop.length; i++) {
const [registerId, where] = drop[i];
tree.addDropNode(registerId, where);
}
return tree;
}
maxRegId = -1;
test = [];
pick = new PickRoot();
drop = new DropRoot();
register = new Map();
findPick(path, pathLength) {
let parent = this.pick;
for (let i = 0; i < pathLength; i++) {
const index = path[i];
if (!parent.children.has(index))
return undefined;
parent = parent.children.get(index);
}
return parent;
}
findDrop(path, pathLength) {
let parent = this.drop;
for (let i = 0; i < pathLength; i++) {
const index = path[i];
if (!parent.children.has(index))
return undefined;
parent = parent.children.get(index);
}
return parent;
}
setRegister(register) {
const regId = register.id;
if (regId > this.maxRegId)
this.maxRegId = regId;
this.register.set(regId, register);
}
addPickNode(registerId, what, length = what.length) {
let parent = this.pick;
if (!length) {
parent.regId = registerId;
const register = new Register(registerId);
register.pick = parent;
this.setRegister(register);
}
else {
for (let i = 0; i < length; i++) {
const index = what[i];
const childExists = parent.children.has(index);
if (!childExists) {
const child = new PickNode(index, what, i + 1);
parent.children.set(index, child);
child.parent = parent;
}
const child = parent.children.get(index);
const isLast = i === length - 1;
if (isLast) {
if (child.regId < 0) {
child.regId = registerId;
const register = new Register(registerId);
register.pick = child;
this.setRegister(register);
}
}
parent = child;
}
}
return this.register.get(parent.regId);
}
addData(registerId, data) {
const register = new Register(registerId);
register.data = data;
this.setRegister(register);
}
addDropNode(registerId, where) {
let parent = this.drop;
const length = where.length;
if (!length) {
const register = this.register.get(registerId);
if (register instanceof Register) {
parent.regId = register.id;
register.addDrop(parent);
}
}
else {
for (let i = 0; i < length; i++) {
const index = where[i];
const childExists = parent.children.has(index);
if (!childExists) {
const child = new DropNode(index, where, i + 1);
parent.children.set(index, child);
child.parent = parent;
}
const child = parent.children.get(index);
const isLast = i === length - 1;
if (isLast) {
child.regId = registerId;
const register = this.register.get(registerId);
register.addDrop(child);
}
parent = child;
}
}
}
/**
* Composes two operations into one combined operation. This object contains
* the result of the composition. During the composition, both operations
* are mutated in place, hence the `other` becomes unusable after the call.
*
* @param other another OpTree
*/
compose(other) {
this.test.push(...other.test);
// Compose deletes.
const d1 = this.drop;
const d2 = other.drop;
// biome-ignore lint: using .forEach() is the fastest way to iterate over a Map
other.register.forEach((register2) => {
// Update pick path.
if (register2.pick) {
let path = register2.pick.path;
let pathLength = register2.pick.pathLength;
const deepestDropNodeInPath = this.findDeepestDropInPath(register2.pick.path, register2.pick.pathLength);
if (deepestDropNodeInPath) {
if (deepestDropNodeInPath) {
const dropRegister = this.register.get(deepestDropNodeInPath.regId);
if (dropRegister.pick) {
path = [
...dropRegister.pick.path.slice(0, dropRegister.pick.pathLength),
...register2.pick.path.slice(0, register2.pick.pathLength).slice(deepestDropNodeInPath.pathLength),
];
pathLength = path.length;
register2.pick.path = path;
register2.pick.pathLength = pathLength;
}
}
}
for (let i = 0; i < pathLength; i++) {
const comp = path[i];
if (typeof comp === 'number') {
const pick = this.findPick(path, i);
if (pick) {
let numberOfPickWithLowerIndex = 0;
pick.children.forEach((child, index) => {
if (+index <= comp)
numberOfPickWithLowerIndex++;
});
path[i] += numberOfPickWithLowerIndex;
}
}
}
const isDelete = !register2.drops.length;
if (isDelete) {
const op1Pick = this.findPick(register2.pick.path, register2.pick.pathLength);
if (op1Pick && op1Pick.regId) {
const register = this.register.get(op1Pick.regId);
const alreadyDeletedInOp1 = register && !register.drops.length;
if (alreadyDeletedInOp1)
return;
}
this.addPickNode(this.maxRegId + 1, path, pathLength);
const drop = this.findDrop(register2.pick.path, register2.pick.pathLength);
if (drop) {
if (drop.parent) {
drop.parent.children.delete(drop.index);
drop.parent = null;
const register1 = this.register.get(drop.regId);
if (register1 instanceof Register) {
register1.removeDrop(drop);
if (!register1.drops.length && !register1.pick)
this.register.delete(drop.regId);
}
}
else {
this.drop.regId = -1;
}
}
}
}
});
this.composeDrops(d1, d2, other);
}
findDeepestDropInPath(path, pathLength = path.length) {
let longest = null;
let curr = this.drop;
for (let i = 0; i < pathLength; i++) {
const comp = path[i];
const child = curr.children.get(comp);
if (!child)
break;
curr = child;
if (curr.regId >= 0)
longest = curr;
}
return longest;
}
removeDrop(drop) {
if (drop.regId >= 0) {
const register = this.register.get(drop.regId);
register.removeDrop(drop);
if (!register.drops.length && !register.pick)
this.register.delete(drop.regId);
}
}
composeDrops(d1, d2, tree2) {
const isDrop = d2.regId >= 0;
if (isDrop) {
const isRoot = !d2.parent;
const clone = !isRoot ? d2.clone() : this.drop;
const register2 = tree2.register.get(d2.regId);
const isDataDrop = register2.data !== undefined;
if (isDataDrop) {
const newRegister = new Register(this.maxRegId + 1);
newRegister.data = register2.data;
newRegister.addDrop(clone);
this.setRegister(newRegister);
clone.regId = newRegister.id;
}
else {
const samePickInOp1Exists = this.findPick(register2.pick.path, register2.pick.pathLength);
if (samePickInOp1Exists) {
clone.regId = samePickInOp1Exists.regId;
}
else {
const reg = this.addPickNode(this.maxRegId + 1, register2.pick.path, register2.pick.pathLength);
reg.addDrop(clone);
clone.regId = reg.id;
}
}
if (!!d1.parent && !!d2.parent) {
const child = d1.parent.children.get(d1.index);
if (child)
this.removeDrop(child);
d1.parent.children.set(d2.index, clone);
}
}
for (const [index, child2] of d2.children) {
if (!d1.children.has(index)) {
const child1 = new DropNode(child2.index, child2.path, child2.pathLength);
child1.parent = d1;
d1.children.set(child1.index, child1);
}
const child1 = d1.children.get(index);
this.composeDrops(child1, child2, tree2);
}
}
toJson() {
const pick = [];
const data = [];
const drop = [];
for (const [index, register] of this.register) {
if (register.data !== undefined) {
data.push([index, register.data]);
}
else {
const pickPath = register.pick.path;
const pickPathLength = register.pick.pathLength;
pick.push([index, pickPath.slice(0, pickPathLength)]);
}
}
this.pushDropNode(drop, this.drop);
return [this.test, pick, data, drop, []];
}
pushDropNode(drop, node) {
if (node.regId >= 0)
drop.push([node.regId, node.path.slice(0, node.pathLength)]);
// biome-ignore lint: using .forEach() is the fastest way to iterate over a Map
node.children.forEach((child) => {
this.pushDropNode(drop, child);
});
}
toString(tab = '') {
const picks = this.pick ? this.pick.toString(tab + '│ ') : ' ∅';
let registers = 'Registers';
const lastRegister = this.register.size - 1;
let i = 0;
for (const [id, register] of this.register) {
const isLast = i === lastRegister;
registers += `\n${tab}${isLast ? '│ └─' : '│ ├─'} ${register}`;
i++;
}
const drops = this.drop ? this.drop.toString(tab + ' ') : ' ∅';
return `OpTree\n${tab}├─ ${picks}\n${tab}│\n${tab}├─ ${registers}\n${tab}│\n${tab}└─ ${drops}`;
}
}