UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

189 lines 6.97 kB
import { AvlMap } from 'sonic-forest/lib/avl/AvlMap'; import { printTree } from 'tree-dump/lib/printTree'; import { Slice } from './Slice'; 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 { VecNode } from '../../../json-crdt/nodes'; import { Anchor } from '../rga/constants'; import { UndEndIterator } from '../../../util/iterator'; import * as schema from './schema'; 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 = Slice) { const slicesModel = this.set.doc; const arr = this.set; const api = slicesModel.api; const builder = api.builder; const stepId = 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.con(header); const x1Id = builder.con(start.id); const x2Id = builder.con(compare(start.id, end.id) === 0 ? 0 : end.id); const typeId = schema.type(type).build(builder); const tupleKeysUpdate = [ [SliceTupleIndex.Header, headerId], [SliceTupleIndex.X1, x1Id], // TODO: Make `x2Id` undefined, when `start.id` and `end.id` are equal. [SliceTupleIndex.X2, x2Id], [SliceTupleIndex.Type, typeId], ]; if (data !== undefined) tupleKeysUpdate.push([SliceTupleIndex.Data, builder.json(data)]); builder.insVec(stepId, tupleKeysUpdate); const chunkId = builder.insArr(arr.id, arr.id, [stepId]); // TODO: Consider using `s` schema here. api.apply(); const tuple = slicesModel.index.get(stepId); const chunk = arr.findById(chunkId); // TODO: Need to check if split slice text was deleted const slice = new Klass(slicesModel, this.txt, arr, chunk, tuple, stacking, 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) { // 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, '\n'); 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(arr, 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 = Slice.deserialize(model, txt, arr, chunk, tuple); if (slice.isMarker()) slice = new Slice(model, txt, arr, chunk, tuple, slice.stacking, 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 Slice) { 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; } /** * @todo Rename to `each0`. */ iterator0() { const iterator = this.list.iterator0(); return () => iterator()?.v; } each() { return new UndEndIterator(this.iterator0()); } forEach(callback) { 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(this.set, chunk)); } } } let hash = topologyHash; 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)))); } } //# sourceMappingURL=Slices.js.map