UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

727 lines (726 loc) 26.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Overlay = void 0; const printTree_1 = require("tree-dump/lib/printTree"); const printBinary_1 = require("tree-dump/lib/printBinary"); const util_1 = require("sonic-forest/lib/util"); const util2_1 = require("sonic-forest/lib/util2"); const util_2 = require("sonic-forest/lib/splay/util"); const constants_1 = require("../rga/constants"); const OverlayPoint_1 = require("./OverlayPoint"); const MarkerOverlayPoint_1 = require("./MarkerOverlayPoint"); const refs_1 = require("./refs"); const clock_1 = require("../../../json-crdt-patch/clock"); const hash_1 = require("../../../json-hash/hash"); const MarkerSlice_1 = require("../slice/MarkerSlice"); const iterator_1 = require("../../../util/iterator"); const constants_2 = require("../slice/constants"); const spatialComparator = (a, b) => a.cmpSpatial(b); /** * Overlay is a tree structure that represents all the intersections of slices * in the text. It is used to quickly find all the slices that overlap a * given point in the text. The overlay is a read-only structure, its state * is changed only by calling the `refresh` method, which updates the overlay * based on the current state of the text and slices. */ class Overlay { constructor(txt) { this.txt = txt; this.root = undefined; this.root2 = undefined; // ----------------------------------------------------------------- Stateful this.hash = 0; this.slices = new Map(); this.leadingTextHash = 0; const id = txt.str.id; this.START = this.point(id, constants_1.Anchor.After); this.END = this.point(id, constants_1.Anchor.Before); } point(id, anchor) { return new OverlayPoint_1.OverlayPoint(this.txt.str, id, anchor); } mPoint(marker, anchor) { return new MarkerOverlayPoint_1.MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker); } first() { return this.root ? (0, util_1.first)(this.root) : undefined; } last() { return this.root ? (0, util_1.last)(this.root) : undefined; } firstMarker() { return this.root2 ? (0, util2_1.first2)(this.root2) : undefined; } lastMarker() { return this.root2 ? (0, util2_1.last2)(this.root2) : undefined; } /** * Retrieve overlay point or the previous one, measured in spacial dimension. */ getOrNextLower(point) { if (point.isAbsStart()) { const first = this.first(); if (!first) return; return first.isAbsStart() ? first : void 0; } else if (point.isAbsEnd()) return this.last(); let curr = this.root; let result = undefined; while (curr) { const cmp = curr.cmpSpatial(point); if (cmp === 0) return curr; if (cmp > 0) curr = curr.l; else { const next = curr.r; result = curr; if (!next) return result; curr = next; } } return result; } /** * Retrieve overlay point or the next one, measured in spacial dimension. */ getOrNextHigher(point) { if (point.isAbsEnd()) { const last = this.last(); if (!last) return; return last.isAbsEnd() ? last : void 0; } else if (point.isAbsStart()) return this.first(); let curr = this.root; let result = undefined; while (curr) { const cmp = curr.cmpSpatial(point); if (cmp === 0) return curr; if (cmp < 0) curr = curr.r; else { const next = curr.l; result = curr; if (!next) return result; curr = next; } } return result; } /** * Retrieve a {@link MarkerOverlayPoint} at the specified point or the * previous one, measured in spacial dimension. */ getOrNextLowerMarker(point) { if (point.isAbsStart()) { const first = this.firstMarker(); if (!first) return; return first.isAbsStart() ? first : void 0; } else if (point.isAbsEnd()) return this.lastMarker(); let curr = this.root2; let result = undefined; while (curr) { const cmp = curr.cmpSpatial(point); if (cmp === 0) return curr; if (cmp > 0) curr = curr.l2; else { const next = curr.r2; result = curr; if (!next) return result; curr = next; } } return result; } /** @todo Rename to `chunks()`. */ chunkSlices0(chunk, p1, p2, callback) { const rga = this.txt.str; const strId = rga.id; let checkFirstAnchor = p1.anchor === constants_1.Anchor.After; const adjustForLastAnchor = p2.anchor === constants_1.Anchor.Before; let id1 = p1.id; const id1IsStr = !(0, clock_1.compare)(id1, strId); if (id1IsStr) { const first = rga.first(); if (!first) return; id1 = first.id; checkFirstAnchor = false; } const id2 = p2.id; if (!checkFirstAnchor && !adjustForLastAnchor) { return rga.range0(chunk, id1, id2, callback); } const sid1 = id1.sid; const time1 = id1.time; const sid2 = id2.sid; const time2 = id2.time; return rga.range0(undefined, id1, id2, (chunk, off, len) => { if (checkFirstAnchor) { checkFirstAnchor = false; const chunkId = chunk.id; if (chunkId.sid === sid1 && chunkId.time + off === time1) { if (len <= 1) return; off += 1; len -= 1; } } if (adjustForLastAnchor) { const chunkId = chunk.id; if (chunkId.sid === sid2 && chunkId.time + off + len - 1 === time2) { if (len <= 1) return; len -= 1; } } if (callback(chunk, off, len)) return true; }); } points0(after, inclusive) { let curr = after ? (inclusive ? after : (0, util_1.next)(after)) : this.first(); return () => { const ret = curr; if (curr) curr = (0, util_1.next)(curr); return ret; }; } points(after, inclusive) { return new iterator_1.UndefEndIter(this.points0(after, inclusive)); } /** * Returns all {@link MarkerOverlayPoint} instances in the overlay, starting * from the given marker point, not including the marker point itself. * * If the `after` parameter is not provided, the iteration starts from the * first marker point in the overlay. * * @param after The marker point after which to start the iteration. * @returns All marker points in the overlay, starting from the given marker * point. */ markers0(after) { let curr = after ? (0, util2_1.next2)(after) : (0, util2_1.first2)(this.root2); return () => { const ret = curr; if (curr) curr = (0, util2_1.next2)(curr); return ret; }; } markers(after) { return new iterator_1.UndefEndIter(this.markers0(after)); } /** * Returns all {@link MarkerOverlayPoint} instances in the overlay, starting * from a give {@link Point}, including any marker overlay points that are * at the same position as the given point. * * @param point Point (inclusive) from which to return all markers. * @returns All marker points in the overlay, starting from the given marker * point. */ markersFrom0(point) { if (point.isAbsStart()) return this.markers0(undefined); let after = this.getOrNextLowerMarker(point); if (after && after.cmp(point) === 0) after = (0, util2_1.prev2)(after); return this.markers0(after); } /** * Returns a pair of overlay marker points for each pair of adjacent marker * points in the overlay, starting from a given point (which may not be a * marker). The very first point in the first pair might be `undefined`, if * the given point is not a marker. Similarly, the very last point in the last * pair might be `undefined`, if the iteration end point is not a marker. * * @param start Start point of the iteration, inclusive. * @param end End point of the iteration. If not provided, the iteration * continues until the end of the overlay. * @returns Iterator that returns pairs of overlay points. */ markerPairs0(start, end) { const i = this.markersFrom0(start); let closed = false; let p1; let p2 = i(); if (p2) { if (p2.isAbsStart() || !p2.cmp(start)) { p1 = p2; p2 = i(); } if (end && p2) { const cmp = end.cmpSpatial(p2); if (cmp <= 0) return () => (closed ? void 0 : ((closed = true), [p1, cmp ? void 0 : p2])); } } return () => { if (closed) return; if (!p2 || p2.isAbsEnd()) return (closed = true), [p1, p2]; else if (p2 && end) { const cmp = end.cmpSpatial(p2); if (cmp <= 0) { closed = true; return [p1, cmp ? void 0 : p2]; } } const result = [p1, p2]; p1 = p2; p2 = i(); return result; }; } pairs0(after) { const isEmpty = !this.root; if (isEmpty) { const u = undefined; let closed = false; return () => (closed ? u : ((closed = true), [u, u])); } let p1; let p2 = after; const iterator = this.points0(after); return () => { const next = iterator(); const isEnd = !next; if (isEnd) { if (!p2 || p2.isAbsEnd()) return; p1 = p2; p2 = undefined; return [p1, p2]; } p1 = p2; p2 = next; if (!p1) { if (p2 && p2.isAbsStart()) { p1 = p2; p2 = iterator(); } } return p1 || p2 ? [p1, p2] : undefined; }; } pairs(after) { return new iterator_1.UndefEndIter(this.pairs0(after)); } tuples0(after) { const iterator = this.pairs0(after); return () => { const pair = iterator(); if (!pair) return; pair[0] ?? (pair[0] = this.START); pair[1] ?? (pair[1] = this.END); return pair; }; } tuples(after) { return new iterator_1.UndefEndIter(this.tuples0(after)); } /** * Finds the first point that satisfies the given predicate function. * * @param predicate Predicate function to find the point, returns true if the * point is found. * @returns The first point that satisfies the predicate, or undefined if no * point is found. */ find(predicate) { let point = this.first(); while (point) { if (predicate(point)) return point; point = (0, util_1.next)(point); } return; } /** * Finds all slices that are contained within the given range. A slice is * considered contained if its start and end points are within the range, * inclusive (uses {@link Range#contains} method to check containment). * * @param range The range to search for contained slices. * @returns A set of slices that are contained within the given range. */ findContained(range) { const result = new Set(); let point = this.getOrNextLower(range.start) ?? this.first(); if (!point) return result; do { if (!range.containsPoint(point)) continue; const slices = point.layers; const length = slices.length; for (let i = 0; i < length; i++) { const slice = slices[i]; if (!result.has(slice) && range.contains(slice)) result.add(slice); } if (point instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) { const marker = point.marker; if (marker && !result.has(marker) && range.contains(marker)) result.add(marker); } } while (point && (point = (0, util_1.next)(point)) && range.containsPoint(point)); return result; } /** * Finds all slices that overlap with the given range. A slice is considered * overlapping if its start or end point is within the range, inclusive * (uses {@link Range#containsPoint} method to check overlap). * * @param range The range to search for overlapping slices. * @returns A set of slices that overlap with the given range. */ findOverlapping(range) { const result = new Set(); let point = this.getOrNextLower(range.start) ?? this.first(); if (!point) return result; do { const slices = point.layers; const length = slices.length; for (let i = 0; i < length; i++) result.add(slices[i]); if (point instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) { const marker = point.marker; if (marker) result.add(marker); } } while (point && (point = (0, util_1.next)(point)) && range.containsPoint(point)); return result; } /** * Returns a summary of how different slice types overlap with the given range. * * @param range Range over which to search for slices. * @param endOnMarker If set to a positive number, the search will stop after * the given number of marker points have been observed. * @returns Summary of the slices in this range. `complete` contains all * "Overwrite" slice types, which overlay the full range, which have not * been removed by "Erase" slice type. `partial` contains all "Overwrite" * slice types, which mark a part of the range, and have not been removed * by "Erase" slice type. */ stat(range, endOnMarker = 10) { const { start, end: end_ } = range; let end = end_; const isSamePoint = start.cmp(end_) === 0; if (isSamePoint) { end = end.clone(); end.halfstep(1); } const after = this.getOrNextLower(start); const hasLeadingPoint = !!after; const iterator = this.points0(after, true); let complete = new Set(); let partial = new Set(); let isFirst = true; let markerCount = 0; OVERLAY: for (let point = iterator(); point && point.cmpSpatial(end) < 0; point = iterator()) { if (point instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) { markerCount++; if (markerCount >= endOnMarker) break; continue OVERLAY; } const current = new Set(); const layers = point.layers; const length = layers.length; LAYERS: for (let i = 0; i < length; i++) { const slice = layers[i]; const type = slice.type; if (typeof type === 'object') continue LAYERS; const stacking = slice.stacking; STACKING: switch (stacking) { case constants_2.SliceStacking.One: current.add(type); break STACKING; case constants_2.SliceStacking.Erase: current.delete(type); break STACKING; } } if (isFirst) { isFirst = false; if (hasLeadingPoint) complete = current; else partial = current; continue OVERLAY; } for (const type of complete) if (!current.has(type)) { complete.delete(type); partial.add(type); } for (const type of current) if (!complete.has(type)) partial.add(type); } return [complete, partial, markerCount]; } /** * Returns `true` if the current character is a marker sentinel. * * @param id ID of the point to check. * @returns Whether the point is a marker point. */ isMarker(id) { const p = this.txt.point(id, constants_1.Anchor.Before); const op = this.getOrNextLower(p); return op instanceof MarkerOverlayPoint_1.MarkerOverlayPoint && op.id.time === id.time && op.id.sid === id.sid; } skipMarkers(point, direction) { while (true) { const isMarker = this.isMarker(point.id); if (!isMarker) return true; const end = point.step(direction); if (end) break; } return false; } refresh(slicesOnly = false) { const txt = this.txt; let hash = hash_1.CONST.START_STATE; hash = this.refreshSlices(hash, txt.savedSlices); hash = this.refreshSlices(hash, txt.extraSlices); hash = this.refreshSlices(hash, txt.localSlices); // TODO: Move test hash calculation out of the overlay. if (!slicesOnly) { // hash = updateRga(hash, txt.str); hash = this.refreshTextSlices(hash); } return (this.hash = hash); } refreshSlices(state, slices) { const oldSlicesHash = slices.hash; const changed = oldSlicesHash !== slices.refresh(); const sliceSet = this.slices; state = (0, hash_1.updateNum)(state, slices.hash); if (changed) { // biome-ignore lint: slices is not iterable slices.forEach((slice) => { let tuple = sliceSet.get(slice); if (tuple) { if (slice.isDel && slice.isDel()) { this.delSlice(slice, tuple); return; } const positionMoved = tuple[0].cmp(slice.start) !== 0 || tuple[1].cmp(slice.end) !== 0; if (positionMoved) this.delSlice(slice, tuple); else return; } tuple = slice instanceof MarkerSlice_1.MarkerSlice ? this.insMarker(slice) : this.insSlice(slice); this.slices.set(slice, tuple); }); if (slices.size() < sliceSet.size) { sliceSet.forEach((tuple, slice) => { const mutSlice = slice; if (mutSlice.isDel) { if (!mutSlice.isDel()) return; this.delSlice(slice, tuple); } }); } } return state; } insSlice(slice) { const x0 = slice.start; const x1 = slice.end; const [start, isStartNew] = this.upsertPoint(x0); const [end, isEndNew] = this.upsertPoint(x1); const isCollapsed = x0.cmp(x1) === 0; start.refs.push(new refs_1.OverlayRefSliceStart(slice)); end.refs.push(new refs_1.OverlayRefSliceEnd(slice)); if (isStartNew) { const beforeStartPoint = (0, util_1.prev)(start); if (beforeStartPoint) start.layers.push(...beforeStartPoint.layers); } if (!isCollapsed) { if (isEndNew) { const beforeEndPoint = (0, util_1.prev)(end); if (beforeEndPoint) end.layers.push(...beforeEndPoint.layers); } let curr = start; do curr.addLayer(slice); while ((curr = (0, util_1.next)(curr)) && curr !== end); } else start.addMarker(slice); return [start, end]; } insMarker(slice) { const point = this.mPoint(slice, constants_1.Anchor.Before); const pivot = this.insPoint(point); if (!pivot) { point.refs.push(slice); const prevPoint = (0, util_1.prev)(point); if (prevPoint) point.layers.push(...prevPoint.layers); } return [point, point]; } delSlice(slice, [start, end]) { this.slices.delete(slice); let curr = start; do { curr.removeLayer(slice); curr.removeMarker(slice); curr = (0, util_1.next)(curr); } while (curr && curr !== end); start.removeRef(slice); end.removeRef(slice); if (!start.refs.length) this.delPoint(start); if (!end.refs.length && start !== end) this.delPoint(end); } /** * Retrieve an existing {@link OverlayPoint} or create a new one, inserted * in the tree, sorted by spatial dimension. */ upsertPoint(point) { const newPoint = this.point(point.id, point.anchor); const pivot = this.insPoint(newPoint); if (pivot) return [pivot, false]; return [newPoint, true]; } /** * Inserts a point into the tree, sorted by spatial dimension. * @param point Point to insert. * @returns Returns the existing point if it was already in the tree. */ insPoint(point) { if (point instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) { this.root2 = (0, util2_1.insert2)(this.root2, point, spatialComparator); // if (this.root2 !== point) this.root2 = splay2(this.root2!, point, 10); } let pivot = this.getOrNextLower(point); if (!pivot) pivot = (0, util_1.first)(this.root); if (!pivot) { this.root = point; return; } else { if (pivot.cmp(point) === 0) return pivot; const cmp = pivot.cmpSpatial(point); if (cmp < 0) (0, util_1.insertRight)(point, pivot); else (0, util_1.insertLeft)(point, pivot); } if (this.root !== point) this.root = (0, util_2.splay)(this.root, point, 10); return; } delPoint(point) { if (point instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) this.root2 = (0, util2_1.remove2)(this.root2, point); this.root = (0, util_1.remove)(this.root, point); } refreshTextSlices(stateTotal) { const txt = this.txt; const str = txt.str; const firstChunk = str.first(); if (!firstChunk) return stateTotal; let chunk = firstChunk; let marker = undefined; const i = this.tuples0(undefined); let state = hash_1.CONST.START_STATE; for (let pair = i(); pair; pair = i()) { const [p1, p2] = pair; const id1 = p1.id; state = (state << 5) + state + (id1.sid >>> 0) + id1.time; let overlayPointHash = hash_1.CONST.START_STATE; chunk = this.chunkSlices0(chunk || firstChunk, p1, p2, (chunk, off, len) => { const id = chunk.id; overlayPointHash = (overlayPointHash << 5) + overlayPointHash + ((((id.sid >>> 0) + id.time) << 8) + (off << 4) + len); }); state = (0, hash_1.updateNum)(state, overlayPointHash); for (const slice of p1.layers) state = (0, hash_1.updateNum)(state, slice.hash); for (const slice of p1.markers) state = (0, hash_1.updateNum)(state, slice.hash); p1.hash = overlayPointHash; stateTotal = (0, hash_1.updateNum)(stateTotal, overlayPointHash); if (p2 instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) { if (marker) { marker.textHash = state; } else { this.leadingTextHash = state; } stateTotal = (0, hash_1.updateNum)(stateTotal, state); state = hash_1.CONST.START_STATE; marker = p2; } } if (marker instanceof MarkerOverlayPoint_1.MarkerOverlayPoint) { marker.textHash = state; } else { this.leadingTextHash = state; } return stateTotal; } // ---------------------------------------------------------------- Printable toString(tab = '') { const printPoint = (tab, point) => { return (point.toString(tab) + (0, printBinary_1.printBinary)(tab, [ !point.l ? null : (tab) => printPoint(tab, point.l), !point.r ? null : (tab) => printPoint(tab, point.r), ])); }; const printMarkerPoint = (tab, point) => { return (point.toString(tab) + (0, printBinary_1.printBinary)(tab, [ !point.l2 ? null : (tab) => printMarkerPoint(tab, point.l2), !point.r2 ? null : (tab) => printMarkerPoint(tab, point.r2), ])); }; return (`Overlay #${this.hash.toString(36)}` + (0, printTree_1.printTree)(tab, [ !this.root ? null : (tab) => printPoint(tab, this.root), !this.root2 ? null : (tab) => printMarkerPoint(tab, this.root2), ])); } } exports.Overlay = Overlay;