json-joy
Version:
Collection of libraries for building collaborative editing apps.
198 lines (197 loc) • 7.36 kB
JavaScript
import { hasOwnProperty as hasOwnProp } from '@jsonjoy.com/util/lib/hasOwnProperty';
import { Point } from '../rga/Point';
import { Range } from '../rga/Range';
import { updateNode } from '../../../json-crdt/hash';
import { printTree } from 'tree-dump/lib/printTree';
import { SliceHeaderMask, SliceHeaderShift, SliceStacking, SliceTupleIndex, SliceStackingName, SliceTypeName, } from './constants';
import { CONST } from '../../../json-hash/hash';
import { Timestamp } from '../../../json-crdt-patch/clock';
import { prettyOneLine } from '../../../json-pretty';
import { validateType } from './util';
import { s } from '../../../json-crdt-patch';
/**
* A persisted slice is a slice that is stored in a {@link Model}. It is used for
* rich-text formatting and annotations.
*
* @todo Maybe rename to "saved", "stored", "mutable".
*/
export class PersistedSlice extends Range {
model;
txt;
chunk;
tuple;
start;
end;
static deserialize(model, txt, chunk, tuple) {
const header = +tuple.get(0).view();
const id1 = tuple.get(1).view();
const id2 = (tuple.get(2).view() || id1);
const type = tuple.get(3).view();
if (typeof header !== 'number')
throw new Error('INVALID_HEADER');
if (!(id1 instanceof Timestamp))
throw new Error('INVALID_ID');
if (!(id2 instanceof Timestamp))
throw new Error('INVALID_ID');
validateType(type);
const anchor1 = (header & SliceHeaderMask.X1Anchor) >>> SliceHeaderShift.X1Anchor;
const anchor2 = (header & SliceHeaderMask.X2Anchor) >>> SliceHeaderShift.X2Anchor;
const stacking = (header & SliceHeaderMask.Stacking) >>> SliceHeaderShift.Stacking;
const rga = txt.str;
const p1 = new Point(rga, id1, anchor1);
const p2 = new Point(rga, id2, anchor2);
const slice = new PersistedSlice(model, txt, chunk, tuple, stacking, type, p1, p2);
return slice;
}
/** @todo Use API node here. */
rga;
constructor(
/** The `Model` where the slice is stored. */
model,
/** The Peritext context. */
txt,
/** The `arr` chunk of `arr` where the slice is stored. */
chunk,
/** The `vec` node which stores the serialized contents of this slice. */
tuple, stacking, type, start, end) {
super(txt.str, start, end);
this.model = model;
this.txt = txt;
this.chunk = chunk;
this.tuple = tuple;
this.start = start;
this.end = end;
this.rga = txt.str;
this.id = chunk.id;
this.stacking = stacking;
this.type = type;
}
isSplit() {
return this.stacking === SliceStacking.Marker;
}
tupleApi() {
return this.model.api.wrap(this.tuple);
}
// ---------------------------------------------------------------- mutations
set(start, end = start) {
super.set(start, end);
this.update({ range: this });
}
/**
* Expand range left and right to contain all invisible space: (1) tombstones,
* (2) anchors of non-deleted adjacent chunks.
*/
expand() {
super.expand();
this.update({ range: this });
}
// ------------------------------------------------------------- MutableSlice
id;
stacking;
type;
tag() {
const type = this.type;
return Array.isArray(type) ? type[type.length - 1] : type;
}
typeSteps() {
const type = this.type ?? 0 /* SliceTypeCon.p */;
return Array.isArray(type) ? type : [type];
}
update(params) {
let updateHeader = false;
const changes = [];
const stacking = params.stacking;
if (stacking !== undefined) {
this.stacking = stacking;
updateHeader = true;
}
const range = params.range;
if (range) {
updateHeader = true;
changes.push([SliceTupleIndex.X1, s.con(range.start.id)], [SliceTupleIndex.X2, s.con(range.end.id)]);
this.start = range.start;
this.end = range.start === range.end ? range.end.clone() : range.end;
}
if (params.type !== undefined) {
this.type = params.type;
changes.push([SliceTupleIndex.Type, s.con(this.type)]);
}
if (hasOwnProp(params, 'data'))
changes.push([SliceTupleIndex.Data, params.data]);
if (updateHeader) {
const header = (this.stacking << SliceHeaderShift.Stacking) +
(this.start.anchor << SliceHeaderShift.X1Anchor) +
(this.end.anchor << SliceHeaderShift.X2Anchor);
changes.push([SliceTupleIndex.Header, s.con(header)]);
}
this.tupleApi().set(changes);
}
data() {
return this.tuple.get(SliceTupleIndex.Data)?.view();
}
dataNode() {
const node = this.tuple.get(SliceTupleIndex.Data);
return node && this.model.api.wrap(node);
}
getStore() {
const txt = this.txt;
const sid = this.id.sid;
let store = txt.savedSlices;
if (sid === store.set.doc.clock.sid)
return store;
store = txt.localSlices;
if (sid === store.set.doc.clock.sid)
return store;
store = txt.extraSlices;
if (sid === store.set.doc.clock.sid)
return store;
return;
}
del() {
const store = this.getStore();
if (!store)
return;
store.del(this.id);
}
isDel() {
return this.chunk.del;
}
// ----------------------------------------------------------------- Stateful
hash = 0;
refresh() {
let state = CONST.START_STATE;
state = updateNode(state, this.tuple);
const changed = state !== this.hash;
this.hash = state;
if (changed) {
const tuple = this.tuple;
const slice = PersistedSlice.deserialize(this.model, this.txt, this.chunk, tuple);
this.stacking = slice.stacking;
this.type = slice.type;
this.start = slice.start;
this.end = slice.end;
}
return this.hash;
}
// ---------------------------------------------------------------- Printable
toStringName() {
if (typeof this.type === 'number' && Math.abs(this.type) <= 64 && SliceTypeName[this.type]) {
return `slice [${SliceStackingName[this.stacking]}] <${SliceTypeName[this.type]}>`;
}
return `slice [${SliceStackingName[this.stacking]}] ${JSON.stringify(this.type)}`;
}
toStringHeaderName() {
const data = this.data();
const dataFormatted = data ? prettyOneLine(data) : '∅';
const dataLengthBreakpoint = 32;
const header = `${this.toStringName()} ${super.toString('', true)}, ${SliceStackingName[this.stacking]}, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`;
return header;
}
toStringHeader(tab = '') {
const data = this.data();
const dataFormatted = data ? prettyOneLine(data) : '';
const dataLengthBreakpoint = 32;
return (this.toStringHeaderName() +
printTree(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]));
}
}