json-joy
Version:
Collection of libraries for building collaborative editing apps.
821 lines • 31.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Overlay = exports.insert = 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 clock_1 = require("../../../json-crdt-patch/clock");
const hash_1 = require("../../../json-hash/hash");
const Slice_1 = require("../slice/Slice");
const iterator_1 = require("../../../util/iterator");
const constants_2 = require("../slice/constants");
const find = (root, comparison, comparator) => {
let curr = root;
while (curr) {
const cmp = comparator(comparison, curr);
if (cmp === 0)
return curr;
curr = cmp < 0 ? curr.l : curr.r;
}
return curr;
};
const find2 = (root, comparison, comparator) => {
let curr = root;
while (curr) {
const cmp = comparator(comparison, curr);
if (cmp === 0)
return curr;
curr = cmp < 0 ? curr.l2 : curr.r2;
}
return curr;
};
const insert = (root, node, comparator) => {
if (!root)
return node;
let curr = root;
while (curr) {
const cmp = comparator(node, curr);
const next = cmp < 0 ? curr.l : curr.r;
if (!next) {
if (cmp < 0)
(0, util_1.insertLeft)(node, curr);
else
(0, util_1.insertRight)(node, curr);
break;
}
else
curr = next;
}
return root;
};
exports.insert = insert;
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;
/**
* @todo Make it an AVL tree.
*/
this.root = undefined;
this.root2 = undefined;
/** ------------------------------------------------------ {@link 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);
}
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;
}
get(point) {
return find(this.root, point, spatialComparator);
}
getMarker(point) {
return find2(this.root2, point, spatialComparator);
}
/**
* 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 OverlayPoint} 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.UndEndIterator(this.points0(after, inclusive));
}
/**
* Returns all {@link OverlayPoint} 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.UndEndIterator(this.markers0(after));
}
/**
* Returns all {@link OverlayPoint} 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, inclusive) {
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, inclusive);
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, inclusive) {
return new iterator_1.UndEndIterator(this.pairs0(after, inclusive));
}
tuples0(after, inclusive) {
const iterator = this.pairs0(after, inclusive);
return () => {
const pair = iterator();
if (!pair)
return;
pair[0] ?? (pair[0] = this.START);
pair[1] ?? (pair[1] = this.END);
return pair;
};
}
tuples(after, inclusive) {
return new iterator_1.UndEndIterator(this.tuples0(after, inclusive));
}
/**
* 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 OverlayPoint_1.OverlayPoint && point.isMarker()) {
const marker = point.markers[0];
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 OverlayPoint_1.OverlayPoint && point.isMarker()) {
const marker = point.markers[0];
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 OverlayPoint_1.OverlayPoint && point.isMarker()) {
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 OverlayPoint_1.OverlayPoint && op.isMarker() && 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;
}
clear() {
this.root = void 0;
this.root2 = void 0;
this.slices.clear();
}
refresh(slicesOnly = false) {
const { txt, slices } = this;
let hash = hash_1.CONST.START_STATE;
{
const { savedSlices, extraSlices, localSlices } = txt;
const savedSlicesOldHash = savedSlices.hash;
const savedSlicesHash = savedSlices.refresh();
const savedSlicesChanged = savedSlicesOldHash !== savedSlicesHash;
hash = (0, hash_1.updateNum)(hash, savedSlicesHash);
const extraSlicesOldHash = extraSlices.hash;
const extraSlicesHash = extraSlices.refresh();
const extraSlicesChanged = extraSlicesOldHash !== extraSlicesHash;
hash = (0, hash_1.updateNum)(hash, extraSlicesHash);
const localSlicesOldHash = localSlices.hash;
const localSlicesHash = localSlices.refresh();
const localSlicesChanged = localSlicesOldHash !== localSlicesHash;
hash = (0, hash_1.updateNum)(hash, localSlicesHash);
if (savedSlicesChanged || extraSlicesChanged || localSlicesChanged) {
// this.clear();
// savedSlices.forEach((slice) => {
// if (slice.isSplit()) this.insMarker(slice);
// else this.insSlice(slice);
// });
// extraSlices.forEach((slice) => {
// if (slice.isSplit()) this.insMarker(slice);
// else this.insSlice(slice);
// });
// localSlices.forEach((slice) => {
// if (slice.isSplit()) this.insMarker(slice);
// else this.insSlice(slice);
// });
if (savedSlicesChanged || extraSlicesChanged) {
this.clear();
// TODO: Implement complete rebuild routine from scratch. It should
// be efficient and fast: (1) retrieve all slices from all
// sources, (2) sort them by start point, (3) insert them into
// the overlay and update hashes in one go.
}
else if (localSlicesChanged) {
slices.forEach((tuple, slice) => {
const mutSlice = slice;
if (mutSlice.isDel?.())
this.delSlice(slice, tuple);
});
}
if (savedSlicesChanged || extraSlicesChanged) {
savedSlices.forEach((slice) => {
if (slice.isMarker())
this.upsertSlice(slice);
else
this.upsertSlice(slice);
});
extraSlices.forEach((slice) => {
if (slice.isMarker())
this.upsertSlice(slice);
else
this.upsertSlice(slice);
});
}
if (localSlicesChanged || savedSlicesChanged || extraSlicesChanged) {
const sliceSet = this.slices;
localSlices.forEach((slice) => {
const tuple = sliceSet.get(slice);
if (tuple) {
const positionMoved = tuple[0].cmp(slice.start) !== 0 || tuple[1].cmp(slice.end) !== 0;
if (positionMoved)
this.delSlice(slice, tuple);
}
});
localSlices.forEach((slice) => {
const tuple = slice.isMarker() ? this.upsertSlice(slice) : this.upsertSlice(slice);
this.slices.set(slice, tuple);
});
}
}
}
// TODO: Move text hash calculation out of the overlay.
if (!slicesOnly) {
// hash = updateRga(hash, txt.str);
hash = this.refreshTextSlices(hash);
}
return (this.hash = hash);
}
upsertSlice(slice) {
if (slice.isMarker()) {
const start = slice.start;
const overlayPointOrLower = this.getOrNextLower(start);
if (overlayPointOrLower) {
const isStart = !overlayPointOrLower.cmp(start);
if (isStart) {
overlayPointOrLower.addMarkerRef(slice);
const markerOverlayPoint = this.getMarker(start);
if (!markerOverlayPoint)
this.root2 = (0, util2_1.insert2)(this.root2, overlayPointOrLower, spatialComparator);
return [overlayPointOrLower, overlayPointOrLower];
}
}
const markerOverlayPointOrLower = this.getOrNextLowerMarker(start);
if (markerOverlayPointOrLower) {
const isStart = !markerOverlayPointOrLower.cmp(start);
if (isStart) {
markerOverlayPointOrLower.addMarkerRef(slice);
this.root = (0, exports.insert)(this.root, markerOverlayPointOrLower, spatialComparator);
return [markerOverlayPointOrLower, markerOverlayPointOrLower];
}
}
const point = new OverlayPoint_1.OverlayPoint(this.txt.str, start.id, constants_1.Anchor.Before);
point.addMarkerRef(slice);
this.root = (0, exports.insert)(this.root, point, spatialComparator);
this.root2 = (0, util2_1.insert2)(this.root2, point, spatialComparator);
const prevPoint = (0, util_1.prev)(point);
if (prevPoint)
point.layers.push(...prevPoint.layers);
return [point, point];
}
const x0 = slice.start;
const x1 = slice.end;
const [start, isStartNew] = this.upsertPoint(slice.start);
const [end, isEndNew] = this.upsertPoint(slice.end);
const isCollapsed = x0.cmp(x1) === 0;
start.upsertStartRef(slice);
end.upsertEndRef(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];
}
delSlice(slice, [start, end]) {
if (slice instanceof Slice_1.Slice && slice.isMarker()) {
this.slices.delete(slice);
this.root2 = (0, util2_1.remove2)(this.root2, start);
this.root = (0, util_1.remove)(this.root, start);
}
else {
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);
}
}
/**
* Inserts a point into the tree, sorted by spatial dimension.
* Retrieve an existing {@link OverlayPoint} or create a new one, inserted
* in the tree, sorted by spatial dimension.
*
* @param point Point to insert.
* @returns Returns the existing point if it was already in the tree.
*/
upsertPoint(point) {
let overlayPointOrLower = this.getOrNextLower(point);
overlayPointOrLower ?? (overlayPointOrLower = (0, util_1.first)(this.root));
if (!overlayPointOrLower)
return [(this.root = this.point(point.id, point.anchor)), true];
if (overlayPointOrLower.cmp(point) === 0)
return [overlayPointOrLower, false];
const cmp = overlayPointOrLower.cmpSpatial(point);
const overlayPoint = this.getMarker(point) ?? this.point(point.id, point.anchor);
if (cmp < 0)
(0, util_1.insertRight)(overlayPoint, overlayPointOrLower);
else
(0, util_1.insertLeft)(overlayPoint, overlayPointOrLower);
if (this.root !== overlayPoint)
this.root = (0, util_2.splay)(this.root, overlayPoint, 10);
return [overlayPoint, true];
}
delPoint(point) {
if (point.p2)
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 && p2.isMarker()) {
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 && marker.isMarker()) {
marker.textHash = state;
}
else {
this.leadingTextHash = state;
}
return stateTotal;
}
/** ----------------------------------------------------- {@link 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;
//# sourceMappingURL=Overlay.js.map