From 608a309f2c99662f5c5bcc5040657e2f93d85576 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Tue, 30 May 2023 09:05:06 +0200 Subject: [PATCH] more work --- src/structs/ContentLink.js | 108 +++++++++++++++++++++++++++++++------ src/structs/Item.js | 19 +++++-- src/types/AbstractType.js | 9 +++- src/types/WeakLink.js | 89 +++++++++++++++++++++++++++--- src/types/YArray.js | 11 +++- src/types/YMap.js | 5 +- tests/index.js | 3 +- tests/weakLinks.tests.js | 25 +++++++-- 8 files changed, 231 insertions(+), 38 deletions(-) diff --git a/src/structs/ContentLink.js b/src/structs/ContentLink.js index 6ec2170e..78668a80 100644 --- a/src/structs/ContentLink.js +++ b/src/structs/ContentLink.js @@ -1,30 +1,33 @@ +import { decoding, encoding, error } from 'lib0' import { - UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line + UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore, // eslint-disable-line + WeakLink, + findRootTypeKey, + ID, + find, + ContentType } from '../internals.js' export class ContentLink { /** - * @param {Item} item + * @param {WeakLink|{parent:string|ID,item:string|ID}} link */ - constructor (item) { - /** - * @type {Item} - */ - this.item = item + constructor (link) { + this.link = link } /** * @return {number} */ getLength () { - return this.item.length + return 1 } /** * @return {Array} */ getContent () { - throw new Error('not implemented') + return [this.link] } /** @@ -38,7 +41,7 @@ import { * @return {ContentLink} */ copy () { - return new ContentLink(this.item) + return new ContentLink(this.link) } /** @@ -46,7 +49,7 @@ import { * @return {ContentLink} */ splice (offset) { - throw new Error('not implemented') + throw error.methodUnimplemented() } /** @@ -54,19 +57,57 @@ import { * @return {boolean} */ mergeWith (right) { - throw new Error('not implemented') + return false } /** * @param {Transaction} transaction * @param {Item} item */ - integrate (transaction, item) {} + integrate (transaction, item) { + if (this.link.constructor !== WeakLink) { + let { parent, item } = /** @type {any} */ (this.link) + let key = null + if (parent.constructor === ID) { + const parentItem = find(transaction.doc.store, parent) + if (parentItem.constructor === Item) { + parent = /** @type {ContentType} */ (parentItem.content).type + } else { + parent = null + } + } else { + parent = transaction.doc.share.get(parent) + } + + if (item.constructor === ID) { + item = find(transaction.doc.store, item) + } else { + key = item + item = parent._map.get(key) + } + this.link = new WeakLink(parent, item, key) + } + + const link = /** @type {WeakLink} */ (this.link) + if (link.item.constructor === Item) { + if (link.item.linkedBy === null) { + link.item.linkedBy = new Set() + } + link.item.linkedBy.add(link) + } + } /** * @param {Transaction} transaction */ - delete (transaction) {} + delete (transaction) { + const link = /** @type {WeakLink} */ (this.link) + if (link.item.constructor === Item) { + if (link.item.linkedBy !== null) { + link.item.linkedBy.delete(link) + } + } + } /** * @param {StructStore} store @@ -78,14 +119,34 @@ import { * @param {number} offset */ write (encoder, offset) { - throw new Error('not implemented') + const link = /** @type {WeakLink} */ (this.link) + let flags = 0 + const parentItem = link.source._item + if (parentItem) { + flags |= 1 + } + if (link.key) { + flags |= 2 + } + encoding.writeVarUint(encoder.restEncoder, flags) + if (parentItem) { + encoder.writeLeftID(parentItem.id) + } else { + const ykey = findRootTypeKey(link.source) + encoder.writeString(ykey) + } + if (link.key !== null) { + encoder.writeString(link.key) + } else { + encoder.writeLeftID(link.item.id) + } } /** * @return {number} */ getRef () { - return 10 + return 11 } } @@ -94,6 +155,19 @@ import { * @return {ContentLink} */ export const readContentWeakLink = decoder => { - throw new Error('not implemented') + const flags = decoding.readVarUint(decoder.restDecoder) + let parent + let item + if ((flags & 1) !== 0) { + parent = decoder.readLeftID() + } else { + parent = decoder.readString() + } + if ((flags & 2) !== 0) { + item = decoder.readString() + } else { + item = decoder.readLeftID() + } + return new ContentLink({parent, item}) } \ No newline at end of file diff --git a/src/structs/Item.js b/src/structs/Item.js index e696e753..d8589dbf 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -24,7 +24,9 @@ import { readContentType, addChangedTypeToTransaction, isDeleted, - StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line + StackItem, DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction, // eslint-disable-line + WeakLink, + ContentLink } from '../internals.js' import * as error from 'lib0/error' @@ -296,6 +298,13 @@ export class Item extends AbstractStruct { * @type {ID | null} */ this.redone = null + /** + * If this item was referenced by other weak links, here we keep the references + * to these weak refs. + * + * @type {Set> | null} + */ + this.linkedBy = null /** * @type {AbstractContent} */ @@ -511,6 +520,7 @@ export class Item extends AbstractStruct { /** @type {AbstractType} */ (this.parent)._map.set(this.parentSub, this) if (this.left !== null) { // this is the current attribute value of parent. delete right + this.linkedBy = this.left.linkedBy this.left.delete(transaction) } } @@ -579,6 +589,8 @@ export class Item extends AbstractStruct { this.deleted === right.deleted && this.redone === null && right.redone === null && + this.linkedBy === null && + right.linkedBy === null && this.content.constructor === right.content.constructor && this.content.mergeWith(right.content) ) { @@ -624,6 +636,7 @@ export class Item extends AbstractStruct { addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length) addChangedTypeToTransaction(transaction, parent, this.parentSub) this.content.delete(transaction) + this.linkedBy = null } } @@ -720,8 +733,8 @@ export const contentRefs = [ readContentType, // 7 readContentAny, // 8 readContentDoc, // 9 - readContentWeakLink, // 10 - () => { error.unexpectedCase() } // 10 - Skip is not ItemContent + () => { error.unexpectedCase() }, // 10 - Skip is not ItemContent + readContentWeakLink // 11 ] /** diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 144cfc3c..4f9830c8 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -11,7 +11,7 @@ import { ContentAny, ContentBinary, getItemCleanStart, - ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line + ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, WeakLink, ContentLink, // eslint-disable-line } from '../internals.js' import * as map from 'lib0/map' @@ -669,6 +669,10 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c))) left.integrate(transaction, 0) break + case WeakLink: + left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentLink(/** @type {WeakLink} */ (c))) + left.integrate(transaction, 0) + break default: if (c instanceof AbstractType) { left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)) @@ -851,6 +855,9 @@ export const typeMapSet = (transaction, parent, key, value) => { case Doc: content = new ContentDoc(/** @type {Doc} */ (value)) break + case WeakLink: + content = new ContentLink(/** @type {WeakLink} */ (value)) + break; default: if (value instanceof AbstractType) { content = new ContentType(value) diff --git a/src/types/WeakLink.js b/src/types/WeakLink.js index c6ef6e60..f51d950e 100644 --- a/src/types/WeakLink.js +++ b/src/types/WeakLink.js @@ -1,3 +1,7 @@ +import { AbstractType, GC, Item, createID } from "yjs" +import { findMarker, typeMapGet } from "./AbstractType.js" +import { error } from "lib0" +import { Transaction, getItemCleanEnd, getItemCleanStart } from "src/internals.js" /** * @template T @@ -5,13 +9,82 @@ * Weak link to another value stored somewhere in the document. */ export class WeakLink { - - /** - * Returns a reference to an underlying value existing somewhere on in the document. - * - * @return {T|undefined} - */ - deref() { - throw new Error('not implemented') + /** + * @param {AbstractType} source + * @param {Item|GC} item + * @param {string|null} key + */ + constructor(source, item, key) { + this.source = source + this.item = item + this.key = key + } + + /** + * Returns a reference to an underlying value existing somewhere on in the document. + * + * @return {T|undefined} + */ + deref() { + if (this.key) { + return /** @type {T|undefined} */ (typeMapGet(this.source, this.key)) + } else { + if (this.item.constructor === Item) { + return this.item.content.getContent()[0] + } else { + return undefined + } } + } +} + +const lengthExceeded = error.create('Length exceeded!') + +/** + * Returns a {WeakLink} to an YArray element at given index. + * + * @param {Transaction} transaction + * @param {AbstractType} parent + * @param {number} index + * @return {WeakLink} + */ +export const arrayWeakLink = (transaction, parent, index) => { + const marker = findMarker(parent, index) + let n = parent._start + if (marker !== null) { + n = marker.p + index -= marker.index + } + for (; n !== null; n = n.right) { + if (!n.deleted && n.countable) { + if (index < n.length) { + if (index > 0) { + n = getItemCleanStart(transaction, createID(n.id.clock, n.id.clock + index)) + } + if (n.length > 1) { + n = getItemCleanEnd(transaction, transaction.doc.store, createID(n.id.clock, n.id.clock + 1)) + } + return new WeakLink(parent, n, null) + } + index -= n.length + } + } + + throw lengthExceeded +} + +/** + * Returns a {WeakLink} to an YMap element at given key. + * + * @param {AbstractType} parent + * @param {string} key + * @return {WeakLink|undefined} + */ +export const mapWeakLink = (parent, key) => { + const item = parent._map.get(key) + if (item !== undefined) { + return new WeakLink(parent, item, key) + } else { + return undefined + } } \ No newline at end of file diff --git a/src/types/YArray.js b/src/types/YArray.js index 59bf0901..cfd5dac7 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -17,7 +17,8 @@ import { callTypeObservers, transact, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line - WeakLink + WeakLink, + arrayWeakLink } from '../internals.js' import { typeListSlice } from './AbstractType.js' @@ -209,7 +210,13 @@ export class YArray extends AbstractType { * @return {WeakLink} */ link(index) { - throw new Error('Method not implemented.') + if (this.doc !== null) { + return transact(this.doc, transaction => { + return arrayWeakLink(transaction, this, index) + }) + } else { + throw new Error('todo') + } } /** diff --git a/src/types/YMap.js b/src/types/YMap.js index b4895792..c86e8b3d 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -15,7 +15,8 @@ import { callTypeObservers, transact, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line - WeakLink + WeakLink, + mapWeakLink } from '../internals.js' import * as iterator from 'lib0/iterator' @@ -241,7 +242,7 @@ export class YMap extends AbstractType { * @return {WeakLink|undefined} */ link(key) { - throw new Error('Method not implemented.') + return mapWeakLink(this, key) } /** diff --git a/tests/index.js b/tests/index.js index 945452fe..d002290f 100644 --- a/tests/index.js +++ b/tests/index.js @@ -21,7 +21,8 @@ if (isBrowser) { log.createVConsole(document.body) } runTests({ - doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions, weakLinks + //doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions, + weakLinks }).then(success => { /* istanbul ignore next */ if (isNode) { diff --git a/tests/weakLinks.tests.js b/tests/weakLinks.tests.js index cb412dc2..dd711fab 100644 --- a/tests/weakLinks.tests.js +++ b/tests/weakLinks.tests.js @@ -5,18 +5,35 @@ import { init, compare } from './testHelper.js' /** * @param {t.TestCase} tc */ -export const testBasic = tc => { +export const testBasicMap = tc => { const doc = new Y.Doc() const map = doc.getMap('map') const nested = new Y.Map() nested.set('a1', 'hello') map.set('a', nested) - const link = nested.link('a') + const link = map.link('a') map.set('b', link) - const nested2 = map.get('b') - t.compare(nested2.toJSON(), nested.toJSON()) + const link2 = /** @type {Y.WeakLink} */ (map.get('b')) + const expected = nested.toJSON() + const actual = link2.deref().toJSON() + t.compare(actual, expected) +} + +/** + * @param {t.TestCase} tc + */ +export const testBasicArray = tc => { + const doc = new Y.Doc() + const array = doc.getArray('array') + array.insert(0, [1,2,3]) + array.insert(3, [array.link(1)]) + + t.compare(array.get(0), 1) + t.compare(array.get(1), 2) + t.compare(array.get(2), 3) + t.compare(array.get(3).deref(), 2) } /**