sync-playground/sync/ordt/plain-text.ts

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;
} */
}