Skip to content

Commit

Permalink
feat(json-crdt-extensions): 🎸 add slices manager
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Apr 15, 2024
1 parent c85fc10 commit 2024b02
Show file tree
Hide file tree
Showing 6 changed files with 410 additions and 3 deletions.
43 changes: 41 additions & 2 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import {Anchor} from './constants';
import {Anchor, SliceBehavior} from './constants';
import {Point} from './point/Point';
import {Range} from './slice/Range';
import {Editor} from './editor/Editor';
import {printTree} from '../../util/print/printTree';
import {ArrNode, StrNode} from '../../json-crdt/nodes';
import {Slices} from './slice/Slices';
import {type ITimestampStruct} from '../../json-crdt-patch/clock';
import type {Model} from '../../json-crdt/model';
import type {Printable} from '../../util/print/types';
import type {SliceType} from './types';
import type {PersistedSlice} from './slice/PersistedSlice';
import {CONST} from '../../json-hash';

export class Peritext implements Printable {
public readonly slices: Slices;
public readonly editor: Editor;

constructor(
public readonly model: Model,
public readonly str: StrNode,
slices: ArrNode,
) {
this.slices = new Slices(this, slices);
this.editor = new Editor(this);
}

Expand Down Expand Up @@ -69,6 +75,22 @@ export class Peritext implements Printable {
return textId;
}

public insSlice(
range: Range,
behavior: SliceBehavior,
type: SliceType,
data?: unknown | ITimestampStruct,
): PersistedSlice {
// if (range.isCollapsed()) throw new Error('INVALID_RANGE');
// TODO: If range is not collapsed, check if there are any visible characters in the range.
const slice = this.slices.ins(range, behavior, type, data);
return slice;
}

public delSlice(sliceId: ITimestampStruct): void {
this.slices.del(sliceId);
}

/** Select a single character before a point. */
public findCharBefore(point: Point): Range | undefined {
if (point.anchor === Anchor.After) {
Expand All @@ -84,6 +106,23 @@ export class Peritext implements Printable {

public toString(tab: string = ''): string {
const nl = () => '';
return this.constructor.name + printTree(tab, [(tab) => this.str.toString(tab)]);
return (
this.constructor.name +
printTree(tab, [
(tab) => this.editor.cursor.toString(tab),
nl,
(tab) => this.str.toString(tab),
nl,
(tab) => this.slices.toString(tab),
])
);
}

// ----------------------------------------------------------------- Stateful

public hash: number = 0;

public refresh(): number {
return this.slices.refresh();
}
}
8 changes: 7 additions & 1 deletion src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {Cursor} from '../slice/Cursor';
import {Anchor} from '../constants';
import {Anchor, SliceBehavior} from '../constants';
import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock';
import {PersistedSlice} from '../slice/PersistedSlice';
import type {Range} from '../slice/Range';
import type {Peritext} from '../Peritext';
import type {Printable} from '../../../util/print/types';
import type {Point} from '../point/Point';
import type {SliceType} from '../types';

export class Editor implements Printable {
/**
Expand Down Expand Up @@ -116,4 +118,8 @@ export class Editor implements Printable {
const range = this.all();
if (range) this.cursor.setRange(range);
}

public insertSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
return this.txt.insSlice(this.cursor, SliceBehavior.Stack, type, data);
}
}
84 changes: 84 additions & 0 deletions src/json-crdt-extensions/peritext/slice/PersistedSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {Point} from '../point/Point';
import {Range} from './Range';
import {hashNode} from '../../../json-crdt/hash';
import {printTree} from '../../../util/print/printTree';
import {Anchor, SliceHeaderMask, SliceHeaderShift, SliceBehavior} from '../constants';
import {ArrChunk} from '../../../json-crdt/nodes';
import {type ITimestampStruct, Timestamp} from '../../../json-crdt-patch/clock';
import type {Slice} from './types';
import type {Peritext} from '../Peritext';
import type {SliceDto, SliceType, Stateful} from '../types';
import type {Printable} from '../../../util/print/types';
import type {JsonNode, VecNode} from '../../../json-crdt/nodes';

export class PersistedSlice extends Range implements Slice, Printable, Stateful {
public readonly id: ITimestampStruct;

constructor(
protected readonly txt: Peritext,
protected readonly chunk: ArrChunk,
public readonly tuple: VecNode,
public behavior: SliceBehavior,
/** @todo Rename to x1? */
public start: Point,
/** @todo Rename to x2? */
public end: Point,
public type: SliceType,
) {
super(txt, start, end);
this.id = this.chunk.id;
}

protected tagNode(): JsonNode | undefined {
// TODO: Normalize `.get()` and `.getNode()` methods across VecNode and ArrNode.
return this.tuple.get(3);
}

public data(): unknown | undefined {
return this.tuple.get(4)?.view();
}

public del(): boolean {
return this.chunk.del;
}

// ---------------------------------------------------------------- Printable

public toString(tab: string = ''): string {
const tagNode = this.tagNode();
const range = `${this.start.toString('', true)}${this.end.toString('', true)}`;
const header = `${this.constructor.name} ${range}`;
return header + printTree(tab, [!tagNode ? null : (tab) => tagNode.toString(tab)]);
}

// ----------------------------------------------------------------- Stateful

public hash: number = 0;

public refresh(): number {
const hash = hashNode(this.tuple);
const changed = hash !== this.hash;
this.hash = hash;
if (changed) {
const tuple = this.tuple;
const header = +(tuple.get(0)!.view() as SliceDto[0]);
const anchor1: Anchor = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor;
const anchor2: Anchor = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor;
const type: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior;
const id1 = tuple.get(1)!.view() as ITimestampStruct;
const id2 = (tuple.get(2)!.view() || id1) as ITimestampStruct;
if (!(id1 instanceof Timestamp)) throw new Error('INVALID_ID');
if (!(id2 instanceof Timestamp)) throw new Error('INVALID_ID');
const subtype = tuple.get(3)!.view() as SliceType;
this.behavior = type;
this.type = subtype;
const x1 = this.start;
const x2 = this.end;
x1.id = id1;
x1.anchor = anchor1;
x2.id = id2;
x2.anchor = anchor2;
}
return this.hash;
}
}
156 changes: 156 additions & 0 deletions src/json-crdt-extensions/peritext/slice/Slices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {PersistedSlice} from './PersistedSlice';
import {ITimespanStruct, ITimestampStruct, Timespan, Timestamp, compare, tss} from '../../../json-crdt-patch/clock';
import {Range} from './Range';
import {updateRga} from '../../../json-crdt/hash';
import {CONST, updateNum} from '../../../json-hash';
import {printTree} from '../../../util/print/printTree';
import {Anchor, SliceBehavior, SliceHeaderMask, SliceHeaderShift} from '../constants';
import {SplitSlice} from './SplitSlice';
import {Point} from '../point/Point';
import {Slice} from './types';
import {VecNode} from '../../../json-crdt/nodes';
import type {SliceDto, SliceType, Stateful} from '../types';
import type {Peritext} from '../Peritext';
import type {Printable} from '../../../util/print/types';
import type {ArrChunk, ArrNode} from '../../../json-crdt/nodes';

export class Slices implements Stateful, Printable {
private list = new Map<ArrChunk, PersistedSlice>();

constructor(public readonly txt: Peritext, public readonly set: ArrNode) {}

public ins(range: Range, behavior: SliceBehavior, type: SliceType, data?: unknown): PersistedSlice {
const peritext = this.txt;
const model = peritext.model;
const set = this.set;
const api = model.api;
const builder = api.builder;
const tupleId = builder.vec();
const start = range.start;
const end = range.end;
const header =
(behavior << SliceHeaderShift.Behavior) +
(start.anchor << SliceHeaderShift.X1Anchor) +
(end.anchor << SliceHeaderShift.X2Anchor);
const headerId = builder.const(header);
const x1Id = builder.const(start.id);
const x2Id = builder.const(compare(start.id, end.id) === 0 ? 0 : end.id);
const subtypeId = builder.const(type);
const tupleKeysUpdate: [key: number, value: ITimestampStruct][] = [
[0, headerId],
[1, x1Id],
[2, x2Id],
[3, subtypeId],
];
if (data !== undefined) tupleKeysUpdate.push([4, builder.json(data)]);
builder.insVec(tupleId, tupleKeysUpdate);
const chunkId = builder.insArr(set.id, set.id, [tupleId]);
api.apply();
const tuple = model.index.get(tupleId) as VecNode;
const chunk = set.findById(chunkId)!;
// TODO: Need to check if split slice text was deleted
const slice =
behavior === SliceBehavior.Split
? new SplitSlice(this.txt, chunk, tuple, behavior, start, end, type)
: new PersistedSlice(this.txt, chunk, tuple, behavior, start, end, type);
this.list.set(chunk, slice);
return slice;
}

protected unpack(chunk: ArrChunk): PersistedSlice {
const txt = this.txt;
const model = txt.model;
const tupleId = chunk.data ? chunk.data[0] : undefined;
if (!tupleId) throw new Error('MARKER_NOT_FOUND');
const tuple = model.index.get(tupleId);
if (!(tuple instanceof VecNode)) throw new Error('NOT_TUPLE');
const header = +(tuple.get(0)!.view() as SliceDto[0]);
const anchor1: Anchor = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor;
const anchor2: Anchor = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor;
const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior;
const id1 = tuple.get(1)!.view() as ITimestampStruct;
const id2 = (tuple.get(2)!.view() || id1) as ITimestampStruct;
if (!(id1 instanceof Timestamp)) throw new Error('INVALID_ID');
if (!(id2 instanceof Timestamp)) throw new Error('INVALID_ID');
const p1 = new Point(txt, id1, anchor1);
const p2 = new Point(txt, id2, anchor2);
const type = tuple.get(3)!.view() as SliceType;
const slice =
behavior === SliceBehavior.Split
? new SplitSlice(this.txt, chunk, tuple, behavior, p1, p2, type)
: new PersistedSlice(this.txt, chunk, tuple, behavior, p1, p2, type);
return slice;
}

public del(id: ITimestampStruct): void {
const api = this.txt.model.api;
api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]);
api.apply();
}

public delMany(slices: Slice[]): void {
const api = this.txt.model.api;
const spans: ITimespanStruct[] = [];
const length = slices.length;
for (let i = 0; i < length; i++) {
const slice = slices[i];
if (slice instanceof PersistedSlice) {
const id = slice.id;
spans.push(new Timespan(id.sid, id.time, 1));
}
}
api.builder.del(this.set.id, spans);
api.apply();
}

public size(): number {
return this.list.size;
}

public forEach(callback: (item: PersistedSlice) => void): void {
this.list.forEach(callback);
}

// ----------------------------------------------------------------- Stateful

private _topologyHash: number = 0;
public hash: number = 0;

public refresh(): number {
const topologyHash = updateRga(CONST.START_STATE, this.set);
if (topologyHash !== this._topologyHash) {
this._topologyHash = topologyHash;
let chunk: ArrChunk | undefined;
for (const iterator = this.set.iterator(); (chunk = iterator()); ) {
const item = this.list.get(chunk);
if (chunk.del) {
if (item) this.list.delete(chunk);
} else {
if (!item) this.list.set(chunk, this.unpack(chunk));
}
}
}
let hash: number = topologyHash;
this.list.forEach((item) => {
item.refresh();
hash = updateNum(hash, item.hash);
});
return (this.hash = hash);
}

// ---------------------------------------------------------------- Printable

public toString(tab: string = ''): string {
return (
this.constructor.name +
printTree(
tab,
[...this.list].map(
([, slice]) =>
(tab) =>
slice.toString(tab),
),
)
);
}
}
3 changes: 3 additions & 0 deletions src/json-crdt-extensions/peritext/slice/SplitSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {PersistedSlice} from './PersistedSlice';

export class SplitSlice extends PersistedSlice {}
Loading

0 comments on commit 2024b02

Please sign in to comment.