215 lines
6.3 KiB
JavaScript
215 lines
6.3 KiB
JavaScript
import {
|
|
createID,
|
|
findMarker,
|
|
createRelativePosition,
|
|
AbstractType, RelativePosition, Item // eslint-disable-line
|
|
} from '../internals.js'
|
|
|
|
/**
|
|
* Object which describes bounded range of elements, together with inclusivity/exclusivity rules
|
|
* operating over that range.
|
|
*
|
|
* These inclusivity rules bear extra meaning when it comes to concurrent inserts, that may
|
|
* eventually happen ie. range `[1..2]` (both side inclusive) means that if a concurrent insert
|
|
* would happen at the boundary between 2nd and 3rd index, it should **NOT** be a part of that
|
|
* range, while range definition `[1..3)` (right side is open) while still describing similar
|
|
* range in linear collection, would also span the range over the elements inserted concurrently
|
|
* between 2nd and 3rd indexes.
|
|
*/
|
|
export class YRange {
|
|
// API mirrored after: https://www.w3.org/TR/IndexedDB/#idbkeyrange
|
|
|
|
/**
|
|
*
|
|
* @param {number|null} lower a lower bound of a range (cannot be higher than upper)
|
|
* @param {number|null} upper an upper bound of a range (cannot be less than lower)
|
|
* @param {boolean} lowerOpen if `true` lower is NOT included in the range
|
|
* @param {boolean} upperOpen if `true` upper is NOT included in the range
|
|
*/
|
|
constructor (lower, upper, lowerOpen = false, upperOpen = false) {
|
|
if (lower !== null && upper !== null && lower > upper) {
|
|
throw new Error('Invalid range: lower bound is higher than upper bound')
|
|
}
|
|
/**
|
|
* A lower bound of a range (cannot be higher than upper). Null if unbounded.
|
|
* @type {number|null}
|
|
*/
|
|
this.lower = lower
|
|
/**
|
|
* An upper bound of a range (cannot be less than lower). Null if unbounded.
|
|
* @type {number|null}
|
|
*/
|
|
this.upper = upper
|
|
/**
|
|
* If `true` lower is NOT included in the range.
|
|
* @type {boolean}
|
|
*/
|
|
this.lowerOpen = lowerOpen
|
|
/**
|
|
* If `true` upper is NOT included in the range.
|
|
* @type {boolean}
|
|
*/
|
|
this.upperOpen = upperOpen
|
|
}
|
|
|
|
/**
|
|
* Creates a range that only spans over a single element.
|
|
*
|
|
* @param {number} index
|
|
* @returns {YRange}
|
|
*/
|
|
static only (index) {
|
|
return new YRange(index, index)
|
|
}
|
|
|
|
/**
|
|
* Returns a range instance, that's bounded on the lower side and
|
|
* unbounded on the upper side.
|
|
*
|
|
* @param {number} lower a lower bound of a range
|
|
* @param {boolean} lowerOpen if `true` lower is NOT included in the range
|
|
* @returns {YRange}
|
|
*/
|
|
static lowerBound (lower, lowerOpen = false) {
|
|
return new YRange(lower, null, lowerOpen, false)
|
|
}
|
|
|
|
/**
|
|
* Returns a range instance, that's unbounded on the lower side and
|
|
* bounded on the upper side.
|
|
*
|
|
* @param {number} upper an upper bound of a range
|
|
* @param {boolean} upperOpen if `true` upper is NOT included in the range
|
|
* @returns {YRange}
|
|
*/
|
|
static upperBound (upper, upperOpen = false) {
|
|
return new YRange(null, upper, false, upperOpen)
|
|
}
|
|
|
|
/**
|
|
* Creates a new range instance, bounded on both ends.
|
|
*
|
|
* @param {number} lower a lower bound of a range (cannot be higher than upper)
|
|
* @param {number} upper an upper bound of a range (cannot be less than lower)
|
|
* @param {boolean} lowerOpen if `true` lower is NOT included in the range
|
|
* @param {boolean} upperOpen if `true` upper is NOT included in the range
|
|
*/
|
|
static bound (lower, upper, lowerOpen = false, upperOpen = false) {
|
|
return new YRange(lower, upper, lowerOpen, upperOpen)
|
|
}
|
|
|
|
/**
|
|
* Checks if a provided index is included in current range.
|
|
*
|
|
* @param {number} index
|
|
* @returns {boolean}
|
|
*/
|
|
includes (index) {
|
|
if (this.lower !== null && index < this.lower) {
|
|
return false
|
|
}
|
|
if (this.upper !== null && index > this.upper) {
|
|
return false
|
|
}
|
|
if (index === this.lower) {
|
|
return !this.lowerOpen
|
|
}
|
|
if (index === this.upper) {
|
|
return !this.upperOpen
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
const indexOutOfBounds = new Error('index out of bounds')
|
|
|
|
/**
|
|
*
|
|
* @param {AbstractType<any>} type
|
|
* @param {number} index
|
|
* @returns {{item: Item,index:number}|null}
|
|
*/
|
|
const findPosition = (type, index) => {
|
|
if (type._searchMarker !== null) {
|
|
const marker = findMarker(type, index)
|
|
if (marker !== null) {
|
|
return { item: marker.p, index: marker.index }
|
|
} else {
|
|
return null
|
|
}
|
|
} else {
|
|
let remaining = index
|
|
let item = type._start
|
|
for (; item !== null && remaining > 0; item = item.right) {
|
|
if (!item.deleted && item.countable) {
|
|
if (remaining < item.length) {
|
|
break
|
|
}
|
|
remaining -= item.length
|
|
}
|
|
}
|
|
if (item === null) {
|
|
return null
|
|
} else {
|
|
return { item, index: index - remaining }
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a pair of values representing relative IDs of a range.
|
|
*
|
|
* @param {AbstractType<any>} type collection that range relates to
|
|
* @param {YRange} range
|
|
* @returns {RelativePosition[]}
|
|
* @throws Will throw an error, if range indexes are out of an type's bounds.
|
|
*/
|
|
export const rangeToRelative = (type, range) => {
|
|
/** @type {RelativePosition} */
|
|
let start
|
|
/** @type {RelativePosition} */
|
|
let end
|
|
let item = type._start
|
|
let remaining = 0
|
|
if (range.lower !== null) {
|
|
remaining = range.lower
|
|
if (remaining === 0 && item !== null) {
|
|
start = createRelativePosition(type, item.id, range.lowerOpen ? 0 : -1)
|
|
} else {
|
|
const pos = findPosition(type, remaining)
|
|
if (pos !== null) {
|
|
item = pos.item
|
|
remaining -= pos.index
|
|
start = createRelativePosition(type, createID(pos.item.id.client, pos.item.id.clock + remaining), range.lowerOpen ? 0 : -1)
|
|
} else {
|
|
throw indexOutOfBounds
|
|
}
|
|
}
|
|
} else {
|
|
// left-side unbounded
|
|
start = createRelativePosition(type, null, -1)
|
|
}
|
|
|
|
if (range.upper !== null) {
|
|
remaining = range.upper - (range.lower ?? 0) + remaining
|
|
while (item !== null) {
|
|
if (!item.deleted && item.countable) {
|
|
if (item.length > remaining) {
|
|
break
|
|
}
|
|
remaining -= item.length
|
|
}
|
|
item = item.right
|
|
}
|
|
if (item === null) {
|
|
throw indexOutOfBounds
|
|
} else {
|
|
end = createRelativePosition(type, createID(item.id.client, item.id.clock + remaining), range.upperOpen ? -1 : 0)
|
|
}
|
|
} else {
|
|
// right-side unbounded
|
|
end = createRelativePosition(type, null, 0)
|
|
}
|
|
return [start, end]
|
|
}
|