UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

241 lines (240 loc) 9.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PersistedSlice = void 0; const hasOwnProperty_1 = require("@jsonjoy.com/util/lib/hasOwnProperty"); const Point_1 = require("../rga/Point"); const Range_1 = require("../rga/Range"); const hash_1 = require("../../../json-crdt/hash"); const printTree_1 = require("tree-dump/lib/printTree"); const constants_1 = require("./constants"); const hash_2 = require("../../../json-hash/hash"); const clock_1 = require("../../../json-crdt-patch/clock"); const json_pretty_1 = require("../../../json-pretty"); const util_1 = require("./util"); const json_crdt_patch_1 = require("../../../json-crdt-patch"); const JsonCrdtDiff_1 = require("../../../json-crdt-diff/JsonCrdtDiff"); const model_1 = require("../../../json-crdt/model"); const NestedType_1 = require("./NestedType"); /** * 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". */ class PersistedSlice extends Range_1.Range { 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); if (typeof header !== 'number') throw new Error('INVALID_HEADER'); if (!(id1 instanceof clock_1.Timestamp)) throw new Error('INVALID_ID'); if (!(id2 instanceof clock_1.Timestamp)) throw new Error('INVALID_ID'); const anchor1 = (header & constants_1.SliceHeaderMask.X1Anchor) >>> constants_1.SliceHeaderShift.X1Anchor; const anchor2 = (header & constants_1.SliceHeaderMask.X2Anchor) >>> constants_1.SliceHeaderShift.X2Anchor; const stacking = (header & constants_1.SliceHeaderMask.Stacking) >>> constants_1.SliceHeaderShift.Stacking; const rga = txt.str; const p1 = new Point_1.Point(rga, id1, anchor1); const p2 = new Point_1.Point(rga, id2, anchor2); const slice = new PersistedSlice(model, txt, chunk, tuple, stacking, p1, p2); (0, util_1.validateType)(slice.type()); return slice; } 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, 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; /** ------------------------------------------------------ {@link Stateful} */ this.hash = 0; this.rga = txt.str; this.id = chunk.id; this.stacking = stacking; } isSplit() { return this.stacking === constants_1.SliceStacking.Marker; } tupleApi() { return this.model.api.wrap(this.tuple); } // protected setType(schema: NodeBuilder): void { // this.tupleApi().set([ // [SliceTupleIndex.Type, schema.type(type as SliceTypeSteps)], // ]); // } // ---------------------------------------------------------------- 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 }); } 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([constants_1.SliceTupleIndex.X1, json_crdt_patch_1.s.con(range.start.id)], [constants_1.SliceTupleIndex.X2, json_crdt_patch_1.s.con(range.end.id)]); this.start = range.start; this.end = range.start === range.end ? range.end.clone() : range.end; } if (params.type !== undefined) { changes.push([constants_1.SliceTupleIndex.Type, params.type instanceof json_crdt_patch_1.NodeBuilder ? params.type : json_crdt_patch_1.s.jsonCon(params.type)]); } if ((0, hasOwnProperty_1.hasOwnProperty)(params, 'data')) changes.push([constants_1.SliceTupleIndex.Data, params.data]); if (updateHeader) { const header = (this.stacking << constants_1.SliceHeaderShift.Stacking) + (this.start.anchor << constants_1.SliceHeaderShift.X1Anchor) + (this.end.anchor << constants_1.SliceHeaderShift.X2Anchor); changes.push([constants_1.SliceTupleIndex.Header, json_crdt_patch_1.s.con(header)]); } this.tupleApi().set(changes); } 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; } // -------------------------------------------------- slice type manipulation typeNode() { return this.tuple.get(constants_1.SliceTupleIndex.Type); } typeApi() { const node = this.typeNode(); if (!node) return; return this.model.api.wrap(node); } nestedType() { return new NestedType_1.NestedType(this); } type() { return this.typeNode()?.view(); } typeSteps() { const type = this.type() ?? 0 /* SliceTypeCon.p */; return Array.isArray(type) ? type : [type]; } // -------------------------------------------------- slice data manipulation data() { return this.tuple.get(constants_1.SliceTupleIndex.Data)?.view(); } dataNode() { const node = this.tuple.get(constants_1.SliceTupleIndex.Data); return node && this.model.api.wrap(node); } dataAsObj() { const node = this.dataNode(); if (!(node instanceof model_1.ObjApi)) { this.tupleApi().set([[constants_1.SliceTupleIndex.Data, json_crdt_patch_1.s.obj({})]]); } return this.dataNode(); } /** * Overwrites the data of this slice with the given data. * * @param data Data to set for this slice. The data can be any JSON value, but * it is recommended to use an object. */ setData(data) { this.tupleApi().set([[constants_1.SliceTupleIndex.Data, json_crdt_patch_1.s.jsonCon(data)]]); } /** * Merges object data into the slice's data using JSON CRDT diffing. * * @param data Data to merge into the slice. If the data is an object, it will be * merged with the existing data of the slice using JSON CRDT diffing. */ mergeData(data) { const { model } = this; const diff = new JsonCrdtDiff_1.JsonCrdtDiff(model); if (this.dataNode() instanceof model_1.ObjApi && !!data && typeof data === 'object' && !Array.isArray(data)) { const dataNode = this.dataAsObj(); const patch = diff.diff(dataNode.node, data); model.applyPatch(patch); } else this.setData(data); } refresh() { let state = hash_2.CONST.START_STATE; state = (0, hash_1.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.start = slice.start; this.end = slice.end; } return this.hash; } /** ----------------------------------------------------- {@link Printable} */ toStringName() { const type = this.type(); if (typeof type === 'number' && Math.abs(type) <= 64 && constants_1.SliceTypeName[type]) { return `slice [${constants_1.SliceStackingName[this.stacking]}] <${constants_1.SliceTypeName[type]}>`; } return `slice [${constants_1.SliceStackingName[this.stacking]}] ${JSON.stringify(type)}`; } toStringHeaderName() { const data = this.data(); const dataFormatted = data ? (0, json_pretty_1.prettyOneLine)(data) : '∅'; const dataLengthBreakpoint = 32; const header = `${this.toStringName()} ${super.toString('', true)}, ${constants_1.SliceStackingName[this.stacking]}, ${JSON.stringify(this.type())}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`; return header; } toStringHeader(tab = '') { const data = this.data(); const dataFormatted = data ? (0, json_pretty_1.prettyOneLine)(data) : ''; const dataLengthBreakpoint = 32; return (this.toStringHeaderName() + (0, printTree_1.printTree)(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted])); } } exports.PersistedSlice = PersistedSlice;