UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

403 lines (402 loc) 14.8 kB
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}`; } }