139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
import { Timestamp, timestampCompare } from "../common.ts";
|
|
import { AnyCausalTreeOp, CausalTree, CausalTreeOp } from "./causal-tree.ts";
|
|
|
|
export type PlainTextOperation =
|
|
| {
|
|
type: "insert";
|
|
sequence: string;
|
|
}
|
|
| { type: "delete" };
|
|
|
|
export class PlainTextORDT extends CausalTree<PlainTextOperation> {
|
|
onevent?: (op: CausalTreeOp<PlainTextOperation>) => void;
|
|
|
|
// caches for insert op <=> text transformation,
|
|
// all these arrays should be the same length (SoA)
|
|
opIndexCache: number[] = [];
|
|
sequences: string[] = [];
|
|
timestamps: Timestamp[] = [];
|
|
deleted: boolean[] = [];
|
|
|
|
override apply(op: AnyCausalTreeOp<PlainTextOperation>): number {
|
|
const opIdx = super.apply(op);
|
|
op = this.operations[opIdx];
|
|
this.#applyCacheUpdate(op, opIdx);
|
|
this.onevent?.(op);
|
|
return opIdx;
|
|
}
|
|
|
|
#applyCacheUpdate(op: CausalTreeOp<PlainTextOperation>, opIdx: number) {
|
|
for (let i = 0; i < this.opIndexCache.length; i++) {
|
|
if (this.opIndexCache[i] >= opIdx) {
|
|
this.opIndexCache[i] += 1;
|
|
}
|
|
}
|
|
|
|
const parentTimestamp = op.parent?.at;
|
|
const parentCacheIdx = this.timestamps.findIndex(it => it === parentTimestamp);
|
|
|
|
if (op.type === "insert") {
|
|
let idx = parentCacheIdx + 1;
|
|
for (; idx < this.timestamps.length; idx++) {
|
|
const curr = this.timestamps[idx];
|
|
if (timestampCompare(curr, op.at) < 0) break;
|
|
}
|
|
|
|
this.sequences.splice(idx, 0, op.sequence);
|
|
this.timestamps.splice(idx, 0, op.at);
|
|
this.deleted.splice(idx, 0, false);
|
|
this.opIndexCache.splice(idx, 0, opIdx);
|
|
}
|
|
if (op.type === "delete" && parentCacheIdx !== -1) {
|
|
this.deleted[parentCacheIdx] = true;
|
|
}
|
|
}
|
|
|
|
findOpAtTextIndex(textIndex: number): number {
|
|
let start = 0;
|
|
for (let idx = 0; idx < this.sequences.length; idx++) {
|
|
if (this.deleted[idx]) continue;
|
|
|
|
const sequence = this.sequences[idx];
|
|
if (start < textIndex && textIndex <= start + sequence.length)
|
|
return this.opIndexCache[idx];
|
|
|
|
start += this.sequences[idx].length;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
render() {
|
|
let s = "";
|
|
let start = 0;
|
|
const metadata = [];
|
|
for (let idx = 0; idx < this.sequences.length; idx++) {
|
|
if (this.deleted[idx]) continue;
|
|
const sequence = this.sequences[idx];
|
|
s += sequence;
|
|
metadata.push({
|
|
start,
|
|
end: start + sequence.length,
|
|
op: this.operations[this.opIndexCache[idx]],
|
|
});
|
|
start += sequence.length;
|
|
}
|
|
return [s, metadata] as const;
|
|
}
|
|
|
|
/* very slow, uncached:
|
|
findOpAtTextIndex(textIndex: number): number {
|
|
const [_string, metadata] = this.render();
|
|
|
|
for (const meta of metadata) {
|
|
if (meta.start < textIndex && textIndex <= meta.end)
|
|
return this.operations.findIndex(it => it === meta.op);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
render() {
|
|
const s: string[] = [];
|
|
const metadata = [];
|
|
let length = 0;
|
|
for (const op of this.operations) {
|
|
if (op.type === "insert") {
|
|
s.push(op.sequence);
|
|
metadata.push({
|
|
op,
|
|
start: length,
|
|
end: length + op.sequence.length,
|
|
});
|
|
length += op.sequence.length;
|
|
}
|
|
if (op.type === "delete") {
|
|
if (!op.parent) continue;
|
|
if (op.parent.type !== "insert") continue;
|
|
|
|
const len = op.parent.sequence.length;
|
|
length -= len;
|
|
|
|
// since we expect to be tacked on close to the referenced op,
|
|
// we can quickly seek back from current position
|
|
// (it should only take a few iterations - usually 1)
|
|
for (let idx = metadata.length - 1; idx >= 0; idx--) {
|
|
const m = metadata[idx];
|
|
m.start -= len;
|
|
m.end -= len;
|
|
if (m.op === op.parent) {
|
|
s.splice(idx, 1);
|
|
metadata.splice(idx, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return [s.join(""), metadata] as const;
|
|
} */
|
|
}
|