From a01113812c9a5e885825dc5221010bae892cb281 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Thu, 26 Oct 2023 09:56:31 +0900 Subject: [PATCH] introduced Y.Range for inclusive and exclusive range definitions --- src/index.js | 1 + src/internals.js | 1 + src/structs/ContentType.js | 2 +- src/structs/Item.js | 6 +- src/types/AbstractType.js | 2 +- src/types/YArray.js | 16 ++- src/types/YMap.js | 2 +- src/types/YText.js | 20 ++-- src/types/YWeakLink.js | 207 ++++++++++++++++------------------- src/utils/YRange.js | 214 +++++++++++++++++++++++++++++++++++++ tests/y-weak-link.tests.js | 127 +++++++++++++++++++--- 11 files changed, 441 insertions(+), 157 deletions(-) create mode 100644 src/utils/YRange.js diff --git a/src/index.js b/src/index.js index d46e48e0..973f1cb9 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ export { YArrayEvent, YTextEvent, YEvent, + YRange as Range, Item, AbstractStruct, GC, diff --git a/src/internals.js b/src/internals.js index c1229bfc..1103000f 100644 --- a/src/internals.js +++ b/src/internals.js @@ -17,6 +17,7 @@ export * from './utils/Transaction.js' export * from './utils/UndoManager.js' export * from './utils/updates.js' export * from './utils/YEvent.js' +export * from './utils/YRange.js' export * from './types/AbstractType.js' export * from './types/YArray.js' diff --git a/src/structs/ContentType.js b/src/structs/ContentType.js index 9c3d8b2c..afca9b06 100644 --- a/src/structs/ContentType.js +++ b/src/structs/ContentType.js @@ -6,7 +6,7 @@ import { readYXmlElement, readYXmlFragment, readYXmlHook, - readYXmlText, + readYXmlText, readYWeakLink, unlinkFrom, YWeakLink, diff --git a/src/structs/Item.js b/src/structs/Item.js index 372f8254..5878a2a5 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -403,7 +403,7 @@ export class Item extends AbstractStruct { if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) { return this.parent.client } - + if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) { // make sure that linked content is integrated first const content = /** @type {ContentType} */ (this.content) @@ -568,8 +568,8 @@ export class Item extends AbstractStruct { // adjust length of parent /** @type {AbstractType} */ (this.parent)._length += this.length } - if (this.left && this.left.linked && this.right && this.right.linked) { - // this item exists within a quoted range + if ((this.left && this.left.linked) || (this.right && this.right.linked)) { + // this item may exists within a quoted range joinLinkedRange(transaction, this) } } diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 2de1708b..744c4830 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -246,7 +246,7 @@ export const callTypeObservers = (type, transaction, event, visitedLinks = null) } else if (type._item.linked) { const linkedBy = transaction.doc.store.linkedBy.get(type._item) if (linkedBy !== undefined) { - for (let link of linkedBy) { + for (const link of linkedBy) { if (visitedLinks === null || !visitedLinks.has(link)) { visitedLinks = visitedLinks !== null ? visitedLinks : new Set() visitedLinks.add(link) diff --git a/src/types/YArray.js b/src/types/YArray.js index 0ea2cb7e..e45b4dce 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -16,8 +16,8 @@ import { YArrayRefID, callTypeObservers, transact, - arrayWeakLink, - ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, // eslint-disable-line + quoteRange, + ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, YRange, // eslint-disable-line } from '../internals.js' import { typeListSlice } from './AbstractType.js' @@ -201,24 +201,22 @@ export class YArray extends AbstractType { get (index) { return typeListGet(this, index) } - + /** * Returns the weak link that allows to refer and observe live changes of contents of an YArray. * It points at a consecutive range of elements, starting at give `index` and spanning over provided * length of elements. * - * @param {number} index The index of the element to return from the YArray - * @param {number} length The number of elements to include in returned weak link reference. + * @param {YRange} range quoted range * @return {YWeakLink} */ - quote (index, length = 1) { + quote (range) { if (this.doc !== null) { return transact(this.doc, transaction => { - return arrayWeakLink(transaction, this, index, length) + return quoteRange(transaction, this, range) }) - } else { - throw new Error('cannot quote an YArray that has not been integrated into YDoc') } + throw new Error('cannot quote an YArray that has not been integrated into YDoc') } /** diff --git a/src/types/YMap.js b/src/types/YMap.js index c97c07b3..1fc75ad9 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -233,7 +233,7 @@ export class YMap extends AbstractType { get (key) { return /** @type {any} */ (typeMapGet(this, key)) } - + /** * Returns a weak reference link to another element stored in the same document. * diff --git a/src/types/YText.js b/src/types/YText.js index caf3d8b1..2c01a091 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -26,9 +26,9 @@ import { typeMapGet, typeMapGetAll, updateMarkerChanges, + quoteRange, ContentType, - ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, YWeakLink, // eslint-disable-line - quoteText + ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, YWeakLink, YRange, // eslint-disable-line } from '../internals.js' import * as object from 'lib0/object' @@ -1064,21 +1064,19 @@ export class YText extends AbstractType { * `hello world` could result in `he"llo wo"rld` * where `"llo wo"` represents quoted range. * - * @param {number} index The index where quoted range should start - * @param {number} length Number of quoted elements + * @param {YRange} range * @return {YWeakLink} * * @public */ - quote (index, length) { - const y = this.doc - if (y !== null) { - return transact(y, transaction => { - const pos = findPosition(transaction, this, index) - return quoteText(transaction, this, pos, length) + quote (range) { + if (this.doc !== null) { + return transact(this.doc, transaction => { + return quoteRange(transaction, this, range) }) } - throw new Error('Quoted text was not integrated into Doc') + + throw new Error('cannot quote an YText that has not been integrated into YDoc') } /** diff --git a/src/types/YWeakLink.js b/src/types/YWeakLink.js index 292ef418..cc406153 100644 --- a/src/types/YWeakLink.js +++ b/src/types/YWeakLink.js @@ -1,11 +1,10 @@ -import { decoding, encoding, error } from 'lib0' +import { decoding, encoding } from 'lib0' import * as map from 'lib0/map' import * as set from 'lib0/set' import { YEvent, AbstractType, transact, getItemCleanEnd, - createID, getItemCleanStart, callTypeObservers, YWeakLinkRefID, @@ -17,7 +16,7 @@ import { formatXmlString, YText, YXmlText, - Transaction, Item, Doc, ID, Snapshot, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ItemTextListPosition // eslint-disable-line + Transaction, Item, Doc, ID, Snapshot, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, YRange, rangeToRelative, // eslint-disable-line } from '../internals.js' /** @@ -54,12 +53,13 @@ export class YWeakLink extends AbstractType { this._quoteStart = start /** @type {RelativePosition} */ this._quoteEnd = end + /** @type {Item|null} */ this._firstItem = firstItem } /** * Position descriptor of the start of a quoted range. - * + * * @returns {RelativePosition} */ get quoteStart () { @@ -68,7 +68,7 @@ export class YWeakLink extends AbstractType { /** * Position descriptor of the end of a quoted range. - * + * * @returns {RelativePosition} */ get quoteEnd () { @@ -99,8 +99,8 @@ export class YWeakLink extends AbstractType { // we don't support quotations over maps this._firstItem = item } - if (!this._firstItem.deleted) { - return this._firstItem.content.getContent()[0] + if (!item.deleted) { + return item.content.getContent()[0] } } @@ -114,18 +114,27 @@ export class YWeakLink extends AbstractType { */ unquote () { let result = /** @type {Array} */ ([]) - let item = this._firstItem + let n = this._firstItem + if (n !== null && this._quoteStart.assoc >= 0) { + // if assoc >= we exclude start from range + n = n.right + } const end = /** @type {ID} */ (this._quoteEnd.item) + const endAssoc = this._quoteEnd.assoc // TODO: moved elements - while (item !== null) { - if (!item.deleted) { - result = result.concat(item.content.getContent()) - } - const lastId = item.lastId - if (lastId.client === end.client && lastId.clock === end.clock) { + while (n !== null) { + if (endAssoc < 0 && n.id.client === end.client && n.id.clock === end.clock) { + // right side is open (last item excluded) break } - item = item.right + if (!n.deleted) { + result = result.concat(n.content.getContent()) + } + const lastId = n.lastId + if (endAssoc >= 0 && lastId.client === end.client && lastId.clock === end.clock) { + break + } + n = n.right } return result } @@ -147,8 +156,7 @@ export class YWeakLink extends AbstractType { // link may refer to a single element in multi-element block // in such case we need to cut of the linked element into a // separate block - let firstItem = this._firstItem !== null ? this._firstItem : getItemCleanStart(transaction, /** @type {ID} */ (this._quoteStart.item)) - getItemCleanEnd(transaction, y.store, /** @type {ID} */(this._quoteEnd.item)) + let [firstItem, lastItem] = sliceBlocksByRange(transaction, this._quoteStart, this.quoteEnd) if (firstItem.parentSub !== null) { // for maps, advance to most recent item while (firstItem.right !== null) { @@ -159,11 +167,9 @@ export class YWeakLink extends AbstractType { /** @type {Item|null} */ let item = firstItem - const end = /** @type {ID} */ (this._quoteEnd.item) for (;item !== null; item = item.right) { createLink(transaction, item, this) - const lastId = item.lastId - if (lastId.client === end.client && lastId.clock === end.clock) { + if (item === lastItem) { break } } @@ -216,22 +222,31 @@ export class YWeakLink extends AbstractType { * @public */ toString () { - if (this._firstItem !== null) { - switch (/** @type {AbstractType} */ (this._firstItem.parent).constructor) { + let n = this._firstItem + if (n !== null && this._quoteStart.assoc >= 0) { + // if assoc >= we exclude start from range + n = n.right + } + if (n !== null) { + switch (/** @type {AbstractType} */ (n.parent).constructor) { case YText: { let str = '' - /** - * @type {Item|null} - */ - let n = this._firstItem const end = /** @type {ID} */ (this._quoteEnd.item) + const endAssoc = this._quoteEnd.assoc while (n !== null) { + if (endAssoc < 0 && n.id.client === end.client && n.id.clock === end.clock) { + // right side is open (last item excluded) + break + } if (!n.deleted && n.countable && n.content.constructor === ContentString) { str += /** @type {ContentString} */ (n.content).str } - const lastId = n.lastId - if (lastId.client === end.client && lastId.clock === end.clock) { - break + if (endAssoc >= 0) { + const lastId = n.lastId + if (lastId.client === end.client && lastId.clock === end.clock) { + // right side is closed (last item included) + break + } } n = n.right } @@ -278,107 +293,54 @@ export const readYWeakLink = decoder => { return new YWeakLink(start, end, null) } -const invalidQuotedRange = error.create('Invalid quoted range length.') - /** * Returns a {WeakLink} to an YArray element at given index. * - * @param {Transaction} transaction * @param {AbstractType} parent - * @param {number} index + * @param {Transaction} transaction + * @param {YRange} range * @return {YWeakLink} */ -export const arrayWeakLink = (transaction, parent, index, length = 1) => { - if (length <= 0) { - throw invalidQuotedRange - } - let startItem = parent._start - for (;startItem !== null; startItem = startItem.right) { - if (!startItem.deleted && startItem.countable) { - if (index < startItem.length) { - if (index > 0) { - startItem = getItemCleanStart(transaction, createID(startItem.id.client, startItem.id.clock + index)) +export const quoteRange = (transaction, parent, range) => { + const [start, end] = rangeToRelative(parent, range) + const [startItem, endItem] = sliceBlocksByRange(transaction, start, end) + const link = new YWeakLink(start, end, startItem) + if (parent.doc !== null) { + transact(parent.doc, (transaction) => { + for (let item = link._firstItem; item !== null; item = item = item.right) { + createLink(transaction, item, link) + if (item === endItem) { + break } - break } - index -= startItem.length - } + }) } - let endItem = startItem - let remaining = length - for (;endItem !== null; endItem = endItem.right) { - if (!endItem.deleted && endItem.countable) { - if (remaining > endItem.length) { - remaining -= endItem.length - } else { - endItem = getItemCleanEnd(transaction, transaction.doc.store, createID(endItem.id.client, endItem.id.clock + remaining - 1)) - break - } - } - } - if (startItem !== null && endItem !== null) { - const start = new RelativePosition(null, null, startItem.id, 0) - const end = new RelativePosition(null, null, endItem.lastId, -1) - const link = new YWeakLink(start, end, startItem) - if (parent.doc !== null) { - transact(parent.doc, (transaction) => { - const end = /** @type {ID} */ (link._quoteEnd.item) - for (let item = link._firstItem; item !== null; item = item = item.right) { - createLink(transaction, item, link) - const lastId = item.lastId - if (lastId.client === end.client && lastId.clock === end.clock) { - break - } - } - }) - } - return link - } - throw invalidQuotedRange + return link } /** - * Returns a {WeakLink} to an YMap element at given key. + * Checks relative position markers and slices the corresponding struct store items + * across their positions. * * @param {Transaction} transaction - * @param {AbstractType} parent - * @param {ItemTextListPosition} pos - * @param {number} length - * @return {YWeakLink} + * @param {RelativePosition} start + * @param {RelativePosition} end + * @returns {Array} first and last item that belongs to a sliced range */ -export const quoteText = (transaction, parent, pos, length) => { - if (pos.right !== null) { - const startItem = pos.right - const endIndex = pos.index + length - while (pos.index < endIndex) { - pos.forward() - } - if (pos.left !== null) { - let endItem = pos.left - if (pos.index > endIndex) { - const overflow = pos.index - endIndex - endItem = getItemCleanEnd(transaction, transaction.doc.store, createID(endItem.id.client, endItem.id.clock + endItem.length - overflow - 1)) - } - const start = new RelativePosition(null, null, startItem.id, 0) - const end = new RelativePosition(null, null, endItem.lastId, -1) - const link = new YWeakLink(start, end, startItem) - if (parent.doc !== null) { - transact(parent.doc, (transaction) => { - const end = /** @type {ID} */ (link._quoteEnd.item) - for (let item = link._firstItem; item !== null; item = item = item.right) { - createLink(transaction, item, link) - const lastId = item.lastId - if (lastId.client === end.client && lastId.clock === end.clock) { - break - } - } - }) - } - return link - } +const sliceBlocksByRange = (transaction, start, end) => { + if (start.item === null || end.item === null) { + throw new Error('this operation requires range to be bounded on both sides') } - - throw invalidQuotedRange + const first = getItemCleanStart(transaction, start.item) + /** @type {Item} */ + let last + if (end.assoc >= 0) { + last = getItemCleanEnd(transaction, transaction.doc.store, end.item) + } else { + const item = getItemCleanStart(transaction, end.item) + last = /** @type {Item} */ (item.left) + } + return [first, last] } /** @@ -459,6 +421,21 @@ export const joinLinkedRange = (transaction, item) => { const common = new Set() for (const link of leftLinks) { if (rightLinks.has(link)) { + // new item existing in a quoted range in between two elements + common.add(link) + } else if (link._quoteEnd.assoc < 0) { + // We're at the right edge of quoted range - right neighbor is not included + // but the left one is. Since quotation is open on the right side, we need to + // include current item. + common.add(link) + } + } + for (const link of rightLinks) { + if (!leftLinks.has(link) && link._firstItem === item.left && link._quoteStart.assoc >= 0) { + // We're at the right edge of quoted range - right neighbor is not included + // but the left one is. Since quotation is open on the right side, we need to + // include current item. + link._firstItem = item // this item is the new most left-wise common.add(link) } } @@ -466,4 +443,4 @@ export const joinLinkedRange = (transaction, item) => { allLinks.set(item, common) } } -} \ No newline at end of file +} diff --git a/src/utils/YRange.js b/src/utils/YRange.js new file mode 100644 index 00000000..4902a7e7 --- /dev/null +++ b/src/utils/YRange.js @@ -0,0 +1,214 @@ +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} 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} 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] +} diff --git a/tests/y-weak-link.tests.js b/tests/y-weak-link.tests.js index da8450cd..cc8ac7d8 100644 --- a/tests/y-weak-link.tests.js +++ b/tests/y-weak-link.tests.js @@ -27,7 +27,7 @@ export const testBasicMap = tc => { export const testBasicArray = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, [1, 2, 3]) - array0.insert(3, [array0.quote(1)]) + array0.insert(3, [array0.quote(Y.Range.only(1))]) t.compare(array0.get(0), 1) t.compare(array0.get(1), 2) @@ -49,7 +49,7 @@ export const testArrayQuoteMultipleElements = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) const nested = new Y.Map([['key', 'value']]) array0.insert(0, [1, 2, nested, 3]) - array0.insert(0, [array0.quote(1, 3)]) + array0.insert(0, [array0.quote(Y.Range.bound(1, 3))]) const link0 = array0.get(0) t.compare(link0.unquote(), [2, nested, 3]) @@ -89,7 +89,7 @@ export const testArrayQuoteMultipleElements = tc => { export const testSelfQuotation = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, [1, 2, 3, 4]) - const link0 = array0.quote(0, 3) + const link0 = array0.quote(Y.Range.bound(0, 3, false, true)) array0.insert(1, [link0]) // link is inserted into its own range t.compare(link0.unquote(), [1, link0, 2, 3]) @@ -259,7 +259,7 @@ export const testObserveMapDelete = tc => { export const testObserveArray = tc => { const { testConnector, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, ['A', 'B', 'C']) - const link0 = /** @type {Y.WeakLink} */ (array0.quote(1, 2)) + const link0 = /** @type {Y.WeakLink} */ (array0.quote(Y.Range.bound(1, 2))) array0.insert(0, [link0]) /** * @type {any} @@ -405,7 +405,7 @@ export const testDeepObserveMap = tc => { const nested = new Y.Map() array.insert(0, [nested]) - const link = array.quote(0) + const link = array.quote(Y.Range.only(0)) map.set('link', link) // update entry in linked map @@ -501,7 +501,7 @@ export const testDeepObserveNewElementWithinQuotedRange = tc => { const m1 = new Y.Map() const m3 = new Y.Map() array0.insert(0, [1, m1, m3, 2]) - const link0 = array0.quote(1, 2) + const link0 = array0.quote(Y.Range.bound(1, 2)) array0.insert(0, [link0]) testConnector.flushAllMessages() @@ -635,9 +635,9 @@ export const testDeepObserveRecursive = tc => { root.insert(1, [m1]) root.insert(2, [m2]) - const l0 = root.quote(0) - const l1 = root.quote(1) - const l2 = root.quote(2) + const l0 = root.quote(Y.Range.only(0)) + const l1 = root.quote(Y.Range.only(1)) + const l2 = root.quote(Y.Range.only(2)) // create cyclic reference between links m0.set('k1', l1) @@ -649,7 +649,7 @@ export const testDeepObserveRecursive = tc => { */ let events = [] m0.observeDeep((es) => { - events = es.map((e) => { + events = es.map((e) => { return { target: e.target, keys: e.keys } }) }) @@ -709,7 +709,7 @@ export const testTextBasic = tc => { const { testConnector, text0, text1 } = init(tc, { users: 2 }) text0.insert(0, 'abcd') // 'abcd' - const link0 = text0.quote(1, 2) // quote: [bc] + const link0 = text0.quote(Y.Range.bound(1, 2)) // quote: [bc] t.compare(link0.toString(), 'bc') text0.insert(2, 'ef') // 'abefcd', quote: [befc] t.compare(link0.toString(), 'befc') @@ -733,7 +733,7 @@ export const testXmlTextBasic = tc => { xml0.insert(0, [text0]) text0.insert(0, 'abcd') // 'abcd' - const link0 = text0.quote(1, 2) // quote: [bc] + const link0 = text0.quote(Y.Range.bound(1, 2)) // quote: [bc] t.compare(link0.toString(), 'bc') text0.insert(2, 'ef') // 'abefcd', quote: [befc] t.compare(link0.toString(), 'befc') @@ -747,6 +747,7 @@ export const testXmlTextBasic = tc => { const { insert } = delta[1] // YWeakLink t.compare(insert.toString(), 'be') } + /** * @param {t.TestCase} tc */ @@ -758,11 +759,11 @@ export const testQuoteFormattedText = tc => { text.insert(0, 'abcde') text.format(0, 1, { b: true }) text.format(1, 3, { i: true }) // 'abcde' - const l1 = text.quote(0, 2) + const l1 = text.quote(Y.Range.bound(0, 1)) t.compare(l1.toString(), 'ab') - const l2 = text.quote(2, 1) // 'c' + const l2 = text.quote(Y.Range.only(2)) // 'c' t.compare(l2.toString(), 'c') - const l3 = text.quote(3, 2) // 'de' + const l3 = text.quote(Y.Range.bound(3, 4)) // 'de' t.compare(l3.toString(), 'de') text2.insertEmbed(0, l1) @@ -775,4 +776,98 @@ export const testQuoteFormattedText = tc => { { insert: l2 }, { insert: l3 } ]) -} \ No newline at end of file +} + +/** + * @param {t.TestCase} tc + */ +export const testTextLowerBoundary = tc => { + const { testConnector, text0, text1, array0 } = init(tc, { users: 2 }) + text0.insert(0, 'abcdef') + + testConnector.flushAllMessages() + + const linkInclusive = text0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] + const linkExclusive = text0.quote(Y.Range.bound(0, 4, true, false)) // (0..4] + array0.insert(0, [linkInclusive, linkExclusive]) + t.compare(linkInclusive.toString(), 'bcde') + t.compare(linkExclusive.toString(), 'bcde') + + text1.insert(1, 'xyz') + + testConnector.flushAllMessages() + + t.compare(linkInclusive.toString(), 'bcde') + t.compare(linkExclusive.toString(), 'xyzbcde') +} + +/** + * @param {t.TestCase} tc + */ +export const testTextUpperBoundary = tc => { + const { testConnector, text0, text1, array0 } = init(tc, { users: 2 }) + text0.insert(0, 'abcdef') + + testConnector.flushAllMessages() + + const linkInclusive = text0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] + const linkExclusive = text0.quote(Y.Range.bound(1, 5, false, true)) // [1..5) + array0.insert(0, [linkInclusive, linkExclusive]) + t.compare(linkInclusive.toString(), 'bcde') + t.compare(linkExclusive.toString(), 'bcde') + + text1.insert(5, 'xyz') + + testConnector.flushAllMessages() + + t.compare(linkInclusive.toString(), 'bcde') + t.compare(linkExclusive.toString(), 'bcdexyz') +} + +/** + * @param {t.TestCase} tc + */ +export const testArrayLowerBoundary = tc => { + const { testConnector, array0, array1, map0 } = init(tc, { users: 2 }) + array0.insert(0, ['a', 'b', 'c', 'd', 'e', 'f']) + + testConnector.flushAllMessages() + + const linkInclusive = array0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] + const linkExclusive = array0.quote(Y.Range.bound(0, 4, true, false)) // (0..4] + map0.set('inclusive', linkInclusive) + map0.set('exclusive', linkExclusive) + t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) + t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e']) + + array1.insert(1, ['x', 'y', 'z']) + + testConnector.flushAllMessages() + + t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) + t.compare(linkExclusive.unquote(), ['x', 'y', 'z', 'b', 'c', 'd', 'e']) +} + +/** + * @param {t.TestCase} tc + */ +export const testArrayUpperBoundary = tc => { + const { testConnector, array0, array1, map0 } = init(tc, { users: 2 }) + array0.insert(0, ['a', 'b', 'c', 'd', 'e', 'f']) + + testConnector.flushAllMessages() + + const linkInclusive = array0.quote(Y.Range.bound(1, 4, false, false)) // [1..4] + const linkExclusive = array0.quote(Y.Range.bound(1, 5, false, true)) // [1..5) + map0.set('inclusive', linkInclusive) + map0.set('exclusive', linkExclusive) + t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) + t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e']) + + array1.insert(5, ['x', 'y', 'z']) + + testConnector.flushAllMessages() + + t.compare(linkInclusive.unquote(), ['b', 'c', 'd', 'e']) + t.compare(linkExclusive.unquote(), ['b', 'c', 'd', 'e', 'x', 'y', 'z']) +}