'use strict'; /** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ /** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const _entryWidth = (e) => (e && e.width) || 0; const _getNodeAtPoint = (point) => point.nodes[0].downPtrs[0]; /** * The skip-list contains "entries", JavaScript objects that each must have a unique "key" * property that is a string. */ class SkipList { constructor() { // if there are N elements in the skiplist, "start" is element -1 and "end" is element N this._start = { key: null, levels: 1, upPtrs: [null], downPtrs: [null], downSkips: [1], downSkipWidths: [0], }; this._end = { key: null, levels: 1, upPtrs: [null], downPtrs: [null], downSkips: [null], downSkipWidths: [null], }; this._numNodes = 0; this._totalWidth = 0; this._keyToNodeMap = {}; this._start.downPtrs[0] = this._end; this._end.upPtrs[0] = this._start; } // a "point" object at location x allows modifications immediately after the first // x elements of the skiplist, such as multiple inserts or deletes. // After an insert or delete using point P, the point is still valid and points // to the same index in the skiplist. Other operations with other points invalidate // this point. _getPoint(targetLoc) { const numLevels = this._start.levels; let lvl = numLevels - 1; let i = -1; let ws = 0; const nodes = new Array(numLevels); const idxs = new Array(numLevels); const widthSkips = new Array(numLevels); nodes[lvl] = this._start; idxs[lvl] = -1; widthSkips[lvl] = 0; while (lvl >= 0) { let n = nodes[lvl]; while (n.downPtrs[lvl] && (i + n.downSkips[lvl] < targetLoc)) { i += n.downSkips[lvl]; ws += n.downSkipWidths[lvl]; n = n.downPtrs[lvl]; } nodes[lvl] = n; idxs[lvl] = i; widthSkips[lvl] = ws; lvl--; if (lvl >= 0) { nodes[lvl] = n; } } return { nodes, idxs, loc: targetLoc, widthSkips, toString: () => `getPoint(${targetLoc})`, }; } _getNodeAtOffset(targetOffset) { let i = 0; let n = this._start; let lvl = this._start.levels - 1; while (lvl >= 0 && n.downPtrs[lvl]) { while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) { i += n.downSkipWidths[lvl]; n = n.downPtrs[lvl]; } lvl--; } if (n === this._start) return (this._start.downPtrs[0] || null); if (n === this._end) { return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null; } return n; } _insertKeyAtPoint(point, entry) { const newNode = { key: entry.key, entry, levels: 0, upPtrs: [], downPtrs: [], downSkips: [], downSkipWidths: [], }; const pNodes = point.nodes; const pIdxs = point.idxs; const pLoc = point.loc; const widthLoc = point.widthSkips[0] + point.nodes[0].downSkipWidths[0]; const newWidth = _entryWidth(entry); // The new node will have at least level 1 // With a proability of 0.01^(n-1) the nodes level will be >= n while (newNode.levels === 0 || Math.random() < 0.01) { const lvl = newNode.levels; newNode.levels++; if (lvl === pNodes.length) { // assume we have just passed the end of point.nodes, and reached one level greater // than the skiplist currently supports pNodes[lvl] = this._start; pIdxs[lvl] = -1; this._start.levels++; this._end.levels++; this._start.downPtrs[lvl] = this._end; this._end.upPtrs[lvl] = this._start; this._start.downSkips[lvl] = this._numNodes + 1; this._start.downSkipWidths[lvl] = this._totalWidth; point.widthSkips[lvl] = 0; } const me = newNode; const up = pNodes[lvl]; const down = up.downPtrs[lvl]; const skip1 = pLoc - pIdxs[lvl]; const skip2 = up.downSkips[lvl] + 1 - skip1; up.downSkips[lvl] = skip1; up.downPtrs[lvl] = me; me.downSkips[lvl] = skip2; me.upPtrs[lvl] = up; me.downPtrs[lvl] = down; down.upPtrs[lvl] = me; const widthSkip1 = widthLoc - point.widthSkips[lvl]; const widthSkip2 = up.downSkipWidths[lvl] + newWidth - widthSkip1; up.downSkipWidths[lvl] = widthSkip1; me.downSkipWidths[lvl] = widthSkip2; } for (let lvl = newNode.levels; lvl < pNodes.length; lvl++) { const up = pNodes[lvl]; up.downSkips[lvl]++; up.downSkipWidths[lvl] += newWidth; } this._keyToNodeMap[`$KEY$${newNode.key}`] = newNode; this._numNodes++; this._totalWidth += newWidth; } _deleteKeyAtPoint(point) { const elem = point.nodes[0].downPtrs[0]; const elemWidth = _entryWidth(elem.entry); for (let i = 0; i < point.nodes.length; i++) { if (i < elem.levels) { const up = elem.upPtrs[i]; const down = elem.downPtrs[i]; const totalSkip = up.downSkips[i] + elem.downSkips[i] - 1; up.downPtrs[i] = down; down.upPtrs[i] = up; up.downSkips[i] = totalSkip; const totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth; up.downSkipWidths[i] = totalWidthSkip; } else { const up = point.nodes[i]; up.downSkips[i]--; up.downSkipWidths[i] -= elemWidth; } } delete this._keyToNodeMap[`$KEY$${elem.key}`]; this._numNodes--; this._totalWidth -= elemWidth; } _propagateWidthChange(node) { const oldWidth = node.downSkipWidths[0]; const newWidth = _entryWidth(node.entry); const widthChange = newWidth - oldWidth; let n = node; let lvl = 0; while (lvl < n.levels) { n.downSkipWidths[lvl] += widthChange; lvl++; while (lvl >= n.levels && n.upPtrs[lvl - 1]) { n = n.upPtrs[lvl - 1]; } } this._totalWidth += widthChange; } _getNodeIndex(node, byWidth) { let dist = (byWidth ? 0 : -1); let n = node; while (n !== this._start) { const lvl = n.levels - 1; n = n.upPtrs[lvl]; if (byWidth) dist += n.downSkipWidths[lvl]; else dist += n.downSkips[lvl]; } return dist; } _getNodeByKey(key) { return this._keyToNodeMap[`$KEY$${key}`]; } // Returns index of first entry such that entryFunc(entry) is truthy, // or length() if no such entry. Assumes all falsy entries come before // all truthy entries. search(entryFunc) { let low = this._start; let lvl = this._start.levels - 1; let lowIndex = -1; const f = (node) => { if (node === this._start) return false; else if (node === this._end) return true; else return entryFunc(node.entry); }; while (lvl >= 0) { let nextLow = low.downPtrs[lvl]; while (!f(nextLow)) { lowIndex += low.downSkips[lvl]; low = nextLow; nextLow = low.downPtrs[lvl]; } lvl--; } return lowIndex + 1; } length() { return this._numNodes; } atIndex(i) { if (i < 0) console.warn(`atIndex(${i})`); if (i >= this._numNodes) console.warn(`atIndex(${i}>=${this._numNodes})`); return _getNodeAtPoint(this._getPoint(i)).entry; } // differs from Array.splice() in that new elements are in an array, not varargs splice(start, deleteCount, newEntryArray) { if (start < 0) console.warn(`splice(${start}, ...)`); if (start + deleteCount > this._numNodes) { console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._numNodes}`); console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._numNodes); console.trace(); } if (!newEntryArray) newEntryArray = []; const pt = this._getPoint(start); for (let i = 0; i < deleteCount; i++) { this._deleteKeyAtPoint(pt); } for (let i = (newEntryArray.length - 1); i >= 0; i--) { const entry = newEntryArray[i]; this._insertKeyAtPoint(pt, entry); } } next(entry) { return this._getNodeByKey(entry.key).downPtrs[0].entry || null; } prev(entry) { return this._getNodeByKey(entry.key).upPtrs[0].entry || null; } push(entry) { this.splice(this._numNodes, 0, [entry]); } slice(start, end) { // act like Array.slice() if (start === undefined) start = 0; else if (start < 0) start += this._numNodes; if (end === undefined) end = this._numNodes; else if (end < 0) end += this._numNodes; if (start < 0) start = 0; if (start > this._numNodes) start = this._numNodes; if (end < 0) end = 0; if (end > this._numNodes) end = this._numNodes; window.dmesg(String([start, end, this._numNodes])); if (end <= start) return []; let n = this.atIndex(start); const array = [n]; for (let i = 1; i < (end - start); i++) { n = this.next(n); array.push(n); } return array; } atKey(key) { return this._getNodeByKey(key).entry; } indexOfKey(key) { return this._getNodeIndex(this._getNodeByKey(key)); } indexOfEntry(entry) { return this.indexOfKey(entry.key); } containsKey(key) { return !!this._getNodeByKey(key); } // gets the last entry starting at or before the offset atOffset(offset) { return this._getNodeAtOffset(offset).entry; } keyAtOffset(offset) { return this.atOffset(offset).key; } offsetOfKey(key) { return this._getNodeIndex(this._getNodeByKey(key), true); } offsetOfEntry(entry) { return this.offsetOfKey(entry.key); } setEntryWidth(entry, width) { entry.width = width; this._propagateWidthChange(this._getNodeByKey(entry.key)); } totalWidth() { return this._totalWidth; } offsetOfIndex(i) { if (i < 0) return 0; if (i >= this._numNodes) return this._totalWidth; return this.offsetOfEntry(this.atIndex(i)); } indexOfOffset(offset) { if (offset <= 0) return 0; if (offset >= this._totalWidth) return this._numNodes; return this.indexOfEntry(this.atOffset(offset)); } } module.exports = SkipList;