UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

184 lines (183 loc) 6.96 kB
import { AvlMap } from 'sonic-forest/lib/avl/AvlMap'; import { printTree } from 'tree-dump/lib/printTree'; import { PersistedSlice } from './PersistedSlice'; import { Timespan, compare, tss } from '../../../json-crdt-patch/clock'; import { updateRga } from '../../../json-crdt/hash'; import { CONST, updateNum } from '../../../json-hash/hash'; import { SliceHeaderShift, SliceStacking, SliceTupleIndex } from './constants'; import { MarkerSlice } from './MarkerSlice'; import { VecNode } from '../../../json-crdt/nodes'; import { Chars } from '../constants'; import { Anchor } from '../rga/constants'; export class Slices { txt; set; list = new AvlMap(compare); rga; constructor( /** The text RGA. */ txt, /** The `arr` node, used as a set, where slices are stored. */ set) { this.txt = txt; this.set = set; this.rga = txt.str; } ins(range, stacking, type, data, Klass = stacking === SliceStacking.Marker ? MarkerSlice : PersistedSlice) { const slicesModel = this.set.doc; const set = this.set; const api = slicesModel.api; const builder = api.builder; const tupleId = builder.vec(); const start = range.start.clone(); const end = range.end.clone(); stacking = stacking & 0b111; const header = (stacking << SliceHeaderShift.Stacking) + ((start.anchor & 0b1) << SliceHeaderShift.X1Anchor) + ((end.anchor & 0b1) << 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 = [ [SliceTupleIndex.Header, headerId], [SliceTupleIndex.X1, x1Id], [SliceTupleIndex.X2, x2Id], [SliceTupleIndex.Type, subtypeId], ]; if (data !== undefined) tupleKeysUpdate.push([SliceTupleIndex.Data, builder.json(data)]); builder.insVec(tupleId, tupleKeysUpdate); const chunkId = builder.insArr(set.id, set.id, [tupleId]); // TODO: Consider using `s` schema here. api.apply(); const tuple = slicesModel.index.get(tupleId); const chunk = set.findById(chunkId); // TODO: Need to check if split slice text was deleted const slice = new Klass(slicesModel, this.txt, chunk, tuple, stacking, type, start, end); this.list.set(chunk.id, slice); return slice; } insMarker(range, type, data) { return this.ins(range, SliceStacking.Marker, type, data); } insMarkerAfter(after, type, data, separator = Chars.BlockSplitSentinel) { // TODO: test condition when cursors is at absolute or relative starts const txt = this.txt; const api = txt.model.api; const builder = api.builder; /** * We skip one clock cycle to prevent Block-wise RGA from merging adjacent * characters. We want the marker chunk to always be its own distinct chunk. */ builder.nop(1); // TODO: Handle case when marker is inserted at the abs start, prevent abs start/end inserts. const textId = builder.insStr(txt.str.id, after, separator); api.apply(); const point = txt.point(textId, Anchor.Before); const range = txt.range(point, point.clone()); return this.insMarker(range, type, data); } insStack(range, type, data) { return this.ins(range, SliceStacking.Many, type, data); } insOne(range, type, data) { return this.ins(range, SliceStacking.One, type, data); } insErase(range, type, data) { return this.ins(range, SliceStacking.Erase, type, data); } unpack(chunk) { const txt = this.txt; const model = this.set.doc; const tupleId = chunk.data ? chunk.data[0] : undefined; if (!tupleId) throw new Error('SLICE_NOT_FOUND'); const tuple = model.index.get(tupleId); if (!(tuple instanceof VecNode)) throw new Error('NOT_TUPLE'); let slice = PersistedSlice.deserialize(model, txt, chunk, tuple); if (slice.isSplit()) slice = new MarkerSlice(model, txt, chunk, tuple, slice.stacking, slice.type, slice.start, slice.end); return slice; } get(id) { return this.list.get(id); } del(id) { this.list.del(id); const set = this.set; const api = set.doc.api; if (!set.findById(id)) return; // TODO: Is it worth checking if the slice is already deleted? api.builder.del(set.id, [tss(id.sid, id.time, 1)]); api.apply(); } delSlices(slices) { const set = this.set; const doc = set.doc; const api = doc.api; const spans = []; for (const slice of slices) { if (slice instanceof PersistedSlice) { const id = slice.id; if (!set.findById(id)) continue; spans.push(new Timespan(id.sid, id.time, 1)); } } if (!spans.length) return false; api.builder.del(this.set.id, spans); api.apply(); return true; } size() { return this.list._size; } iterator0() { const iterator = this.list.iterator0(); return () => iterator()?.v; } forEach(callback) { // biome-ignore lint: list is not iterable this.list.forEach((node) => callback(node.v)); } // ----------------------------------------------------------------- Stateful _topologyHash = 0; hash = 0; refresh() { const topologyHash = updateRga(CONST.START_STATE, this.set); if (topologyHash !== this._topologyHash) { this._topologyHash = topologyHash; let chunk; for (const iterator = this.set.iterator(); (chunk = iterator());) { const item = this.list.get(chunk.id); if (chunk.del) { if (item) this.list.del(chunk.id); } else { if (!item) this.list.set(chunk.id, this.unpack(chunk)); } } } let hash = topologyHash; // biome-ignore lint: slices is not iterable this.list.forEach(({ v: item }) => { item.refresh(); hash = updateNum(hash, item.hash); }); return (this.hash = hash); } // ---------------------------------------------------------------- Printable toStringName() { return 'Slices'; } toString(tab = '') { return (this.toStringName() + printTree(tab, [...this.list.entries()].map(({ v }) => (tab) => v.toString(tab)))); } }