Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

YWeakLink #581

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ export {
YXmlHook as XmlHook,
YXmlElement as XmlElement,
YXmlFragment as XmlFragment,
YWeakLink as WeakLink,
YWeakLinkEvent,
YXmlEvent,
YMapEvent,
YArrayEvent,
YTextEvent,
YEvent,
YRange as Range,
Item,
AbstractStruct,
GC,
Expand Down
2 changes: 2 additions & 0 deletions src/internals.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -27,6 +28,7 @@ export * from './types/YXmlElement.js'
export * from './types/YXmlEvent.js'
export * from './types/YXmlHook.js'
export * from './types/YXmlText.js'
export * from './types/YWeakLink.js'

export * from './structs/AbstractStruct.js'
export * from './structs/GC.js'
Expand Down
25 changes: 23 additions & 2 deletions src/structs/ContentType.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
readYXmlFragment,
readYXmlHook,
readYXmlText,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
readYWeakLink,
unlinkFrom,
YWeakLink,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType, ID, // eslint-disable-line
} from '../internals.js'

import * as error from 'lib0/error'
Expand All @@ -23,7 +26,8 @@ export const typeRefs = [
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText
readYXmlText,
readYWeakLink
]

export const YArrayRefID = 0
Expand All @@ -33,6 +37,7 @@ export const YXmlElementRefID = 3
export const YXmlFragmentRefID = 4
export const YXmlHookRefID = 5
export const YXmlTextRefID = 6
export const YWeakLinkRefID = 7

/**
* @private
Expand Down Expand Up @@ -104,6 +109,22 @@ export class ContentType {
* @param {Transaction} transaction
*/
delete (transaction) {
if (this.type.constructor === YWeakLink) {
// when removing weak links, remove references to them
// from type they're pointing to
const type = /** @type {YWeakLink<any>} */ (this.type)
const end = /** @type {ID} */ (type._quoteEnd.item)
for (let item = type._firstItem; item !== null; item = item.right) {
if (item.linked) {
unlinkFrom(transaction, item, type)
}
const lastId = item.lastId
if (lastId.client === end.client && lastId.clock === end.clock) {
break
}
}
type._firstItem = null
}
let item = this.type._start
while (item !== null) {
if (!item.deleted) {
Expand Down
88 changes: 84 additions & 4 deletions src/structs/Item.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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
YWeakLink,
joinLinkedRange
} from '../internals.js'

import * as error from 'lib0/error'
Expand Down Expand Up @@ -105,6 +107,14 @@ export const splitItem = (transaction, leftItem, diff) => {
if (leftItem.redone !== null) {
rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff)
}
if (leftItem.linked) {
rightItem.linked = true
const allLinks = transaction.doc.store.linkedBy
const linkedBy = allLinks.get(leftItem)
if (linkedBy !== undefined) {
allLinks.set(rightItem, new Set(linkedBy))
}
}
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
leftItem.right = rightItem
// update right
Expand Down Expand Up @@ -304,11 +314,28 @@ export class Item extends AbstractStruct {
* bit2: countable
* bit3: deleted
* bit4: mark - mark node as fast-search-marker
* bit9: linked - this item is linked by Weak Link references
* @type {number} byte
*/
this.info = this.content.isCountable() ? binary.BIT2 : 0
}

/**
* This is used to mark the item as linked by weak link references.
* Reference dependencies are being kept in StructStore.
*
* @type {boolean}
*/
set linked (isLinked) {
if (((this.info & binary.BIT9) > 0) !== isLinked) {
this.info ^= binary.BIT9
}
}

get linked () {
return (this.info & binary.BIT9) > 0
}

/**
* This is used to mark the item as an indexed fast-search marker
*
Expand Down Expand Up @@ -377,6 +404,20 @@ export class Item extends AbstractStruct {
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)
const link = /** @type {YWeakLink<any>} */ (content.type)
const start = link._quoteStart.item
if (start !== null && start.clock >= getState(store, start.client)) {
return start.client
}
const end = link._quoteEnd.item
if (end !== null && end.clock >= getState(store, end.client)) {
return end.client
}
}

// We have all missing ids, now find the items

if (this.origin) {
Expand Down Expand Up @@ -508,18 +549,43 @@ export class Item extends AbstractStruct {
// set as current parent value if right === null and this is parentSub
/** @type {AbstractType<any>} */ (this.parent)._map.set(this.parentSub, this)
if (this.left !== null) {
// move links from block we're overriding
this.linked = this.left.linked
this.left.linked = false
const allLinks = transaction.doc.store.linkedBy
const links = allLinks.get(this.left)
if (links !== undefined) {
allLinks.set(this, links)
// since left is being deleted, it will remove
// its links from store.linkedBy anyway
}
// this is the current attribute value of parent. delete right
this.left.delete(transaction)
}
}
// adjust length of parent
if (this.parentSub === null && this.countable && !this.deleted) {
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
if (this.parentSub === null && !this.deleted) {
if (this.countable) {
// adjust length of parent
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
}
if ((this.left && this.left.linked) || (this.right && this.right.linked)) {
// this item may exists within a quoted range
joinLinkedRange(transaction, this)
}
}
addStruct(transaction.doc.store, this)
this.content.integrate(transaction, this)
// add parent to transaction.changed
addChangedTypeToTransaction(transaction, /** @type {AbstractType<any>} */ (this.parent), this.parentSub)
if (this.linked) {
// notify links about changes
const linkedBy = transaction.doc.store.linkedBy.get(this)
if (linkedBy !== undefined) {
for (const link of linkedBy) {
addChangedTypeToTransaction(transaction, link, this.parentSub)
}
}
}
if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
Expand Down Expand Up @@ -577,6 +643,7 @@ export class Item extends AbstractStruct {
this.deleted === right.deleted &&
this.redone === null &&
right.redone === null &&
!this.linked && !right.linked && // linked items cannot be merged
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {
Expand Down Expand Up @@ -622,6 +689,19 @@ 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)

if (this.linked) {
// notify links that current element has been removed
const allLinks = transaction.doc.store.linkedBy
const linkedBy = allLinks.get(this)
if (linkedBy !== undefined) {
for (const link of linkedBy) {
addChangedTypeToTransaction(transaction, link, this.parentSub)
}
allLinks.delete(this)
}
this.linked = false
}
}
}

Expand Down
17 changes: 15 additions & 2 deletions src/types/AbstractType.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, YWeakLink, // eslint-disable-line
} from '../internals.js'

import * as map from 'lib0/map'
Expand Down Expand Up @@ -233,15 +233,28 @@ export const getTypeChildren = t => {
* @param {AbstractType<EventType>} type
* @param {Transaction} transaction
* @param {EventType} event
* @param {Set<YWeakLink<any>>|null} visitedLinks
*/
export const callTypeObservers = (type, transaction, event) => {
export const callTypeObservers = (type, transaction, event, visitedLinks = null) => {
const changedType = type
const changedParentTypes = transaction.changedParentTypes
while (true) {
// @ts-ignore
map.setIfUndefined(changedParentTypes, type, () => []).push(event)
if (type._item === null) {
break
} else if (type._item.linked) {
const linkedBy = transaction.doc.store.linkedBy.get(type._item)
if (linkedBy !== undefined) {
for (const link of linkedBy) {
if (visitedLinks === null || !visitedLinks.has(link)) {
visitedLinks = visitedLinks !== null ? visitedLinks : new Set()
visitedLinks.add(link)
// recursive call
callTypeObservers(link, transaction, /** @type {any} */ (event), visitedLinks)
}
}
}
}
type = /** @type {AbstractType<any>} */ (type._item.parent)
}
Expand Down
20 changes: 19 additions & 1 deletion src/types/YArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
YArrayRefID,
callTypeObservers,
transact,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // 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'

Expand Down Expand Up @@ -201,6 +202,23 @@ export class YArray extends AbstractType {
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 {YRange} range quoted range
* @return {YWeakLink<T>}
*/
quote (range) {
if (this.doc !== null) {
return transact(this.doc, transaction => {
return quoteRange(transaction, this, range)
})
}
throw new Error('cannot quote an YArray that has not been integrated into YDoc')
}

/**
* Transforms this YArray to a JavaScript Array.
*
Expand Down
13 changes: 12 additions & 1 deletion src/types/YMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
YMapRefID,
callTypeObservers,
transact,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
mapWeakLink,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YWeakLink, // eslint-disable-line
} from '../internals.js'

import * as iterator from 'lib0/iterator'
Expand Down Expand Up @@ -233,6 +234,16 @@ export class YMap extends AbstractType {
return /** @type {any} */ (typeMapGet(this, key))
}

/**
* Returns a weak reference link to another element stored in the same document.
*
* @param {string} key
* @return {YWeakLink<MapType>|undefined}
*/
link (key) {
return mapWeakLink(this, key)
}

/**
* Returns a boolean indicating whether the specified key exists or not.
*
Expand Down