json-joy
Version:
Collection of libraries for building collaborative editing apps.
185 lines (184 loc) • 7.42 kB
JavaScript
"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;