gc more efficiently

This commit is contained in:
Kevin Jahns 2019-04-23 20:51:32 +02:00
parent bb6f6cd141
commit 45237571b7
15 changed files with 1402 additions and 1335 deletions

View File

@ -12,8 +12,8 @@ import { exampleSetup } from 'prosemirror-example-setup'
// import { noteHistoryPlugin } from './prosemirror-history.js' // import { noteHistoryPlugin } from './prosemirror-history.js'
const provider = new WebsocketProvider(conf.serverAddress) const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror', { gc: false }) const ydocument = provider.get('prosemirror' /*, { gc: false } */)
const type = ydocument.define('prosemirror', Y.XmlFragment) const type = ydocument.get('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), { const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({ state: EditorState.create({

1975
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"version": "13.0.0-79", "version": "13.0.0-79",
"description": "A ", "description": "A ",
"main": "./dist/yjs.js", "main": "./dist/yjs.js",
"module": "./dist/yjs.mjs'", "module": "./src/index.js",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production", "test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production",
@ -13,7 +13,8 @@
"lint": "standard && tsc", "lint": "standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/", "serve-docs": "npm run docs && serve ./docs/",
"postversion": "npm run lint && PRODUCTION=1 npm run dist && node ./dist/tests.js --repitition-time 1000", "preversion": "PRODUCTION=1 npm run dist && node ./dist/tests.js --repitition-time 10000",
"postversion": "git push && git push --tags",
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'", "debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js", "trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.js" "trace-opt": "clear && rollup -c && node --trace-opt dist/test.js"
@ -58,6 +59,7 @@
"devDependencies": { "devDependencies": {
"codemirror": "^5.42.0", "codemirror": "^5.42.0",
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"jsdoc": "^3.5.5",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"prosemirror-example-setup": "^1.0.1", "prosemirror-example-setup": "^1.0.1",
"prosemirror-schema-basic": "^1.0.0", "prosemirror-schema-basic": "^1.0.0",
@ -71,7 +73,6 @@
"rollup-plugin-node-resolve": "^4.0.0", "rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-terser": "^4.0.4", "rollup-plugin-terser": "^4.0.4",
"standard": "^11.0.1", "standard": "^11.0.1",
"jsdoc": "^3.5.5",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^3.3.3333" "typescript": "^3.3.3333"
} }

View File

@ -85,8 +85,8 @@ export default [{
}), }),
commonjs() commonjs()
] ]
}, /*{ }, {
input: ['./examples/codemirror.js', './examples/textarea.js'], // './examples/quill.js', './examples/dom.js', './examples/prosemirror.js' input: ['./examples/textarea.js', './examples/prosemirror.js'], // './examples/quill.js', './examples/dom.js', './examples/codemirror.js'
output: { output: {
dir: 'examples/build', dir: 'examples/build',
format: 'esm', format: 'esm',
@ -102,4 +102,4 @@ export default [{
commonjs(), commonjs(),
...minificationPlugins ...minificationPlugins
] ]
}*/] }]

View File

@ -9,6 +9,23 @@ export {
YXmlHook as XmlHook, YXmlHook as XmlHook,
YXmlElement as XmlElement, YXmlElement as XmlElement,
YXmlFragment as XmlFragment, YXmlFragment as XmlFragment,
YXmlEvent,
YMapEvent,
YArrayEvent,
YEvent,
AbstractItem,
AbstractStruct,
GC,
ItemBinary,
ItemDeleted,
ItemEmbed,
ItemFormat,
ItemJSON,
ItemString,
ItemType,
AbstractType,
compareCursors,
Cursor,
createCursorFromTypeOffset, createCursorFromTypeOffset,
createCursorFromJSON, createCursorFromJSON,
createAbsolutePositionFromCursor, createAbsolutePositionFromCursor,
@ -22,5 +39,10 @@ export {
readStatesAsMap, readStatesAsMap,
writeStates, writeStates,
writeModel, writeModel,
readModel readModel,
Snapshot,
findRootTypeKey,
typeArrayToArraySnapshot,
typeMapGetSnapshot,
iterateDeletedStructs
} from './internals.js' } from './internals.js'

View File

@ -14,6 +14,7 @@ export * from './types/AbstractType.js'
export * from './types/YArray.js' export * from './types/YArray.js'
export * from './types/YMap.js' export * from './types/YMap.js'
export * from './types/YText.js' export * from './types/YText.js'
export * from './types/YXmlFragment.js'
export * from './types/YXmlElement.js' export * from './types/YXmlElement.js'
export * from './types/YXmlEvent.js' export * from './types/YXmlEvent.js'
export * from './types/YXmlHook.js' export * from './types/YXmlHook.js'

View File

@ -103,7 +103,6 @@ export class ItemType extends AbstractItem {
super.delete(transaction) super.delete(transaction)
transaction.changed.delete(this.type) transaction.changed.delete(this.type)
transaction.changedParentTypes.delete(this.type) transaction.changedParentTypes.delete(this.type)
this.gcChildren(transaction, transaction.y.store)
} }
/** /**
@ -132,8 +131,8 @@ export class ItemType extends AbstractItem {
* @param {StructStore} store * @param {StructStore} store
*/ */
gc (transaction, store) { gc (transaction, store) {
super.gc(transaction, store)
this.gcChildren(transaction, store) this.gcChildren(transaction, store)
super.gc(transaction, store)
} }
} }

View File

@ -197,6 +197,29 @@ export const typeArrayToArray = type => {
return cs return cs
} }
/**
* @param {AbstractType<any>} type
* @param {Snapshot} snapshot
* @return {Array<any>}
*
* @private
* @function
*/
export const typeArrayToArraySnapshot = (type, snapshot) => {
const cs = []
let n = type._start
while (n !== null) {
if (n.countable && isVisible(n, snapshot)) {
const c = n.getContent()
for (let i = 0; i < c.length; i++) {
cs.push(c[i])
}
}
n = n.right
}
return cs
}
/** /**
* Executes a provided function on once on overy element of this YArray. * Executes a provided function on once on overy element of this YArray.
* *

View File

@ -147,7 +147,7 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val) left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val)
left.integrate(transaction) left.integrate(transaction)
} }
return {left, right} return { left, right }
} }
/** /**
@ -617,10 +617,11 @@ export class YText extends AbstractType {
constructor (string) { constructor (string) {
super() super()
/** /**
* @type {Array<string>?} * Array of pending operations on this type
* @type {Array<function():void>?}
* @private * @private
*/ */
this._prelimContent = string !== undefined ? [string] : [] this._pending = string !== undefined ? [() => this.insert(0, string)] : []
} }
get length () { get length () {
@ -635,9 +636,13 @@ export class YText extends AbstractType {
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore this._prelimContent is still defined try {
this.insert(0, this._prelimContent.join('')) // @ts-ignore this._prelimContent is still defined
this._prelimContent = null this._pending.forEach(f => f())
} catch (e) {
console.error(e)
}
this._pending = null
} }
/** /**
@ -737,6 +742,9 @@ export class YText extends AbstractType {
} }
} }
}) })
} else {
// @ts-ignore
this._pending.push(() => this.applyDelta(delta))
} }
} }
@ -836,9 +844,12 @@ export class YText extends AbstractType {
const y = this._y const y = this._y
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const {left, right, currentAttributes} = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, text, attributes) insertText(transaction, this, left, right, currentAttributes, text, attributes)
}) })
} else {
// @ts-ignore
this._pending.push(() => this.insert(index, text, attributes))
} }
} }
@ -862,6 +873,9 @@ export class YText extends AbstractType {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes) insertText(transaction, this, left, right, currentAttributes, embed, attributes)
}) })
} else {
// @ts-ignore
this._pending.push(() => this.insertEmbed(index, embed, attributes))
} }
} }
@ -883,6 +897,9 @@ export class YText extends AbstractType {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
deleteText(transaction, left, right, currentAttributes, length) deleteText(transaction, left, right, currentAttributes, length)
}) })
} else {
// @ts-ignore
this._pending.push(() => this.delete(index, length))
} }
} }
@ -906,6 +923,9 @@ export class YText extends AbstractType {
} }
formatText(transaction, this, left, right, currentAttributes, length, attributes) formatText(transaction, this, left, right, currentAttributes, length, attributes)
}) })
} else {
// @ts-ignore
this._pending.push(() => this.format(index, length, attributes))
} }
} }

View File

@ -1,244 +1,19 @@
/**
* @module YXml
*/
import { import {
YXmlEvent, YXmlFragment,
AbstractType, transact,
typeArrayMap, typeMapDelete,
typeArrayForEach, typeMapSet,
typeMapGet, typeMapGet,
typeMapGetAll, typeMapGetAll,
typeArrayInsertGenerics, typeArrayForEach,
typeArrayDelete,
typeMapSet,
typeMapDelete,
YXmlElementRefID, YXmlElementRefID,
callTypeObservers, Snapshot, Y, ItemType // eslint-disable-line
transact,
Y, Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {IterableIterator}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {ItemType | null}
*/
// @ts-ignore
this._currentNode = root._start
this._firstCall = true
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
let n = this._currentNode
if (n !== null && (!this._firstCall || n.deleted || !this._filter(n.type))) { // if first call, we check if we can use the first item
do {
if (!n.deleted && (n.type.constructor === YXmlElement || n.type.constructor === YXmlFragment) && n.type._start !== null) {
// walk down in the tree
// @ts-ignore
n = n.type._start
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
// @ts-ignore
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(n.type)))
}
this._firstCall = false
this._currentNode = n
if (n === null) {
// @ts-ignore return undefined if done=true (the expected result)
return { value: undefined, done: true }
}
// @ts-ignore
return { value: n.type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
*/
export class YXmlFragment extends AbstractType {
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
}
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
}
toString () {
return this.toDomString()
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toDomString () {
return typeArrayMap(this, xml => xml.toDomString()).join('')
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDom (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeArrayForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null)
})
return fragment
}
}
/** /**
* An YXmlElement imitates the behavior of a * An YXmlElement imitates the behavior of a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}. * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
@ -250,11 +25,6 @@ export class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') { constructor (nodeName = 'UNDEFINED') {
super() super()
this.nodeName = nodeName.toUpperCase() this.nodeName = nodeName.toUpperCase()
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
/** /**
* @type {Map<string, any>|null} * @type {Map<string, any>|null}
* @private * @private
@ -389,44 +159,6 @@ export class YXmlElement extends YXmlFragment {
return typeMapGetAll(this) return typeMapGetAll(this)
} }
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/** /**
* Creates a Dom Element that mirrors this YXmlElement. * Creates a Dom Element that mirrors this YXmlElement.
* *
@ -480,11 +212,3 @@ export class YXmlElement extends YXmlFragment {
* @function * @function
*/ */
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder)) export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))
/**
* @param {decoding.Decoder} decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()

313
src/types/YXmlFragment.js Normal file
View File

@ -0,0 +1,313 @@
/**
* @module YXml
*/
import {
YXmlEvent,
YXmlElement,
AbstractType,
typeArrayMap,
typeArrayForEach,
typeArrayInsertGenerics,
typeArrayDelete,
typeArrayToArray,
YXmlFragmentRefID,
callTypeObservers,
transact,
Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {IterableIterator}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {ItemType | null}
*/
// @ts-ignore
this._currentNode = root._start
this._firstCall = true
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
let n = this._currentNode
if (n !== null && (!this._firstCall || n.deleted || !this._filter(n.type))) { // if first call, we check if we can use the first item
do {
if (!n.deleted && (n.type.constructor === YXmlElement || n.type.constructor === YXmlFragment) && n.type._start !== null) {
// walk down in the tree
// @ts-ignore
n = n.type._start
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
// @ts-ignore
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(n.type)))
}
this._firstCall = false
this._currentNode = n
if (n === null) {
// @ts-ignore return undefined if done=true (the expected result)
return { value: undefined, done: true }
}
// @ts-ignore
return { value: n.type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
*/
export class YXmlFragment extends AbstractType {
constructor () {
super()
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
}
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
}
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
}
toString () {
return this.toDomString()
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toDomString () {
return typeArrayMap(this, xml => xml.toDomString()).join('')
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDom (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeArrayForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null)
})
return fragment
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
*/
toArray () {
return typeArrayToArray(this)
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_write (encoder) {
encoding.writeVarUint(encoder, YXmlFragmentRefID)
}
}
/**
* @param {decoding.Decoder} decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()

View File

@ -3,7 +3,7 @@ import {
findIndexSS, findIndexSS,
createID, createID,
getState, getState,
AbstractItem, StructStore, Transaction, ID // eslint-disable-line AbstractStruct, AbstractItem, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
@ -11,9 +11,6 @@ import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
class DeleteItem { class DeleteItem {
/** /**
* @param {number} clock * @param {number} clock
@ -37,8 +34,6 @@ class DeleteItem {
* - This DeleteSet is send to other clients * - This DeleteSet is send to other clients
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore * - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged. * - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
*
* @private
*/ */
export class DeleteSet { export class DeleteSet {
constructor () { constructor () {
@ -50,6 +45,33 @@ export class DeleteSet {
} }
} }
/**
* Iterate over all structs that were deleted.
*
* This function expects that the deletes structs are not deleted. Hence, you can
* probably only use it in type observes and `afterTransaction` events. But not
* in `afterTransactionCleanup`.
*
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(AbstractStruct):void} f
*
* @function
*/
export const iterateDeletedStructs = (ds, store, f) =>
ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(clientid))
for (let i = 0; i < deletes.length; i++) {
const del = deletes[i]
let index = findIndexSS(structs, del.clock)
let struct
do {
struct = structs[index++]
f(struct)
} while (index < structs.length && structs[index].id.clock < del.clock + del.len)
}
})
/** /**
* @param {Array<DeleteItem>} dis * @param {Array<DeleteItem>} dis
* @param {number} clock * @param {number} clock

View File

@ -1,8 +1,7 @@
import { import {
DeleteSet,
isDeleted, isDeleted,
AbstractItem // eslint-disable-line DeleteSet, AbstractItem // eslint-disable-line
} from '../internals.js' } from '../internals.js'
export class Snapshot { export class Snapshot {
@ -17,7 +16,7 @@ export class Snapshot {
* @type {DeleteSet} * @type {DeleteSet}
* @private * @private
*/ */
this.ds = new DeleteSet() this.ds = ds
/** /**
* State Map * State Map
* @type {Map<number,number>} * @type {Map<number,number>}

View File

@ -181,9 +181,14 @@ export const transact = (y, f) => {
if (deleteItem.clock + deleteItem.len <= struct.id.clock) { if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break break
} }
if (struct.deleted && struct instanceof AbstractItem && (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted))) { if (struct.deleted && struct instanceof AbstractItem) {
// check if we can GC if (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted)) {
struct.gc(transaction, store) // check if we can GC
struct.gc(transaction, store)
} else {
// otherwise only gc children (if there are any)
struct.gcChildren(transaction, store)
}
} }
} }
} }

View File

@ -12,6 +12,7 @@ import {
getState, getState,
findRootTypeKey, findRootTypeKey,
AbstractItem, AbstractItem,
ItemType,
ID, StructStore, Y, AbstractType // eslint-disable-line ID, StructStore, Y, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -240,7 +241,13 @@ export const createAbsolutePositionFromCursor = (cursor, y) => {
if (tname !== null) { if (tname !== null) {
type = y.get(tname) type = y.get(tname)
} else if (typeID !== null) { } else if (typeID !== null) {
type = getItemType(store, typeID).type const struct = getItemType(store, typeID)
if (struct instanceof ItemType) {
type = struct.type
} else {
// struct is garbage collected
return null
}
} else { } else {
throw error.unexpectedCase() throw error.unexpectedCase()
} }