UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

185 lines (184 loc) 7.42 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Slices = void 0; const AvlMap_1 = require("sonic-forest/lib/avl/AvlMap"); const printTree_1 = require("tree-dump/lib/printTree"); const PersistedSlice_1 = require("./PersistedSlice"); const clock_1 = require("../../../json-crdt-patch/clock"); const hash_1 = require("../../../json-crdt/hash"); const hash_2 = require("../../../json-hash/hash"); const constants_1 = require("./constants"); const MarkerSlice_1 = require("./MarkerSlice"); const nodes_1 = require("../../../json-crdt/nodes"); const constants_2 = require("../constants"); const constants_3 = require("../rga/constants"); class Slices { constructor( /** The text RGA. */ txt, /** The `arr` node, used as a set, where slices are stored. */ set) { this.txt = txt; this.set = set; this.list = new AvlMap_1.AvlMap(clock_1.compare); // ----------------------------------------------------------------- Stateful this._topologyHash = 0; this.hash = 0; this.rga = txt.str; } ins(range, stacking, type, data, Klass = stacking === constants_1.SliceStacking.Marker ? MarkerSlice_1.MarkerSlice : PersistedSlice_1.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 << constants_1.SliceHeaderShift.Stacking) + ((start.anchor & 0b1) << constants_1.SliceHeaderShift.X1Anchor) + ((end.anchor & 0b1) << constants_1.SliceHeaderShift.X2Anchor); const headerId = builder.const(header); const x1Id = builder.const(start.id); const x2Id = builder.const((0, clock_1.compare)(start.id, end.id) === 0 ? 0 : end.id); const subtypeId = builder.const(type); const tupleKeysUpdate = [ [constants_1.SliceTupleIndex.Header, headerId], [constants_1.SliceTupleIndex.X1, x1Id], [constants_1.SliceTupleIndex.X2, x2Id], [constants_1.SliceTupleIndex.Type, subtypeId], ]; if (data !== undefined) tupleKeysUpdate.push([constants_1.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, constants_1.SliceStacking.Marker, type, data); } insMarkerAfter(after, type, data, separator = constants_2.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, constants_3.Anchor.Before); const range = txt.range(point, point.clone()); return this.insMarker(range, type, data); } insStack(range, type, data) { return this.ins(range, constants_1.SliceStacking.Many, type, data); } insOne(range, type, data) { return this.ins(range, constants_1.SliceStacking.One, type, data); } insErase(range, type, data) { return this.ins(range, constants_1.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 nodes_1.VecNode)) throw new Error('NOT_TUPLE'); let slice = PersistedSlice_1.PersistedSlice.deserialize(model, txt, chunk, tuple); if (slice.isSplit()) slice = new MarkerSlice_1.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, [(0, clock_1.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_1.PersistedSlice) { const id = slice.id; if (!set.findById(id)) continue; spans.push(new clock_1.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)); } refresh() { const topologyHash = (0, hash_1.updateRga)(hash_2.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 = (0, hash_2.updateNum)(hash, item.hash); }); return (this.hash = hash); } // ---------------------------------------------------------------- Printable toStringName() { return 'Slices'; } toString(tab = '') { return (this.toStringName() + (0, printTree_1.printTree)(tab, [...this.list.entries()].map(({ v }) => (tab) => v.toString(tab)))); } } exports.Slices = Slices;