283 lines
7.4 KiB
JavaScript
283 lines
7.4 KiB
JavaScript
/**
|
|
* @module Y
|
|
*/
|
|
|
|
import {
|
|
StructStore,
|
|
AbstractType,
|
|
YArray,
|
|
YText,
|
|
YMap,
|
|
YXmlFragment,
|
|
transact,
|
|
ContentDoc, Item, Transaction, YEvent // eslint-disable-line
|
|
} from '../internals.js'
|
|
|
|
import { Observable } from 'lib0/observable.js'
|
|
import * as random from 'lib0/random.js'
|
|
import * as map from 'lib0/map.js'
|
|
import * as array from 'lib0/array.js'
|
|
|
|
export const generateNewClientId = random.uint32
|
|
|
|
/**
|
|
* @typedef {Object} DocOpts
|
|
* @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true)
|
|
* @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
|
|
* @property {string} [DocOpts.guid] Define a globally unique identifier for this document
|
|
* @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well.
|
|
* @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically.
|
|
*/
|
|
|
|
/**
|
|
* A Yjs instance handles the state of shared data.
|
|
* @extends Observable<string>
|
|
*/
|
|
export class Doc extends Observable {
|
|
/**
|
|
* @param {DocOpts} [opts] configuration
|
|
*/
|
|
constructor ({ guid = random.uuidv4(), gc = true, gcFilter = () => true, meta = null, autoLoad = false } = {}) {
|
|
super()
|
|
this.gc = gc
|
|
this.gcFilter = gcFilter
|
|
this.clientID = generateNewClientId()
|
|
this.guid = guid
|
|
/**
|
|
* @type {Map<string, AbstractType<YEvent>>}
|
|
*/
|
|
this.share = new Map()
|
|
this.store = new StructStore()
|
|
/**
|
|
* @type {Transaction | null}
|
|
*/
|
|
this._transaction = null
|
|
/**
|
|
* @type {Array<Transaction>}
|
|
*/
|
|
this._transactionCleanups = []
|
|
/**
|
|
* @type {Set<Doc>}
|
|
*/
|
|
this.subdocs = new Set()
|
|
/**
|
|
* If this document is a subdocument - a document integrated into another document - then _item is defined.
|
|
* @type {Item?}
|
|
*/
|
|
this._item = null
|
|
this.shouldLoad = autoLoad
|
|
this.autoLoad = autoLoad
|
|
this.meta = meta
|
|
}
|
|
|
|
/**
|
|
* Notify the parent document that you request to load data into this subdocument (if it is a subdocument).
|
|
*
|
|
* `load()` might be used in the future to request any provider to load the most current data.
|
|
*
|
|
* It is safe to call `load()` multiple times.
|
|
*/
|
|
load () {
|
|
const item = this._item
|
|
if (item !== null && !this.shouldLoad) {
|
|
transact(/** @type {any} */ (item.parent).doc, transaction => {
|
|
transaction.subdocsLoaded.add(this)
|
|
}, null, true)
|
|
}
|
|
this.shouldLoad = true
|
|
}
|
|
|
|
getSubdocs () {
|
|
return this.subdocs
|
|
}
|
|
|
|
getSubdocGuids () {
|
|
return new Set(Array.from(this.subdocs).map(doc => doc.guid))
|
|
}
|
|
|
|
/**
|
|
* Changes that happen inside of a transaction are bundled. This means that
|
|
* the observer fires _after_ the transaction is finished and that all changes
|
|
* that happened inside of the transaction are sent as one message to the
|
|
* other peers.
|
|
*
|
|
* @param {function(Transaction):void} f The function that should be executed as a transaction
|
|
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
|
|
*
|
|
* @public
|
|
*/
|
|
transact (f, origin = null) {
|
|
transact(this, f, origin)
|
|
}
|
|
|
|
/**
|
|
* Define a shared data type.
|
|
*
|
|
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
|
|
* and do not overwrite each other. I.e.
|
|
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
|
|
*
|
|
* After this method is called, the type is also available on `y.share.get(name)`.
|
|
*
|
|
* *Best Practices:*
|
|
* Define all types right after the Yjs instance is created and store them in a separate object.
|
|
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
|
|
*
|
|
* @example
|
|
* const y = new Y(..)
|
|
* const appState = {
|
|
* document: y.getText('document')
|
|
* comments: y.getArray('comments')
|
|
* }
|
|
*
|
|
* @param {string} name
|
|
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
|
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
|
*
|
|
* @public
|
|
*/
|
|
get (name, TypeConstructor = AbstractType) {
|
|
const type = map.setIfUndefined(this.share, name, () => {
|
|
// @ts-ignore
|
|
const t = new TypeConstructor()
|
|
t._integrate(this, null)
|
|
return t
|
|
})
|
|
const Constr = type.constructor
|
|
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
|
|
if (Constr === AbstractType) {
|
|
// @ts-ignore
|
|
const t = new TypeConstructor()
|
|
t._map = type._map
|
|
type._map.forEach(/** @param {Item?} n */ n => {
|
|
for (; n !== null; n = n.left) {
|
|
// @ts-ignore
|
|
n.parent = t
|
|
}
|
|
})
|
|
t._start = type._start
|
|
for (let n = t._start; n !== null; n = n.right) {
|
|
n.parent = t
|
|
}
|
|
t._length = type._length
|
|
this.share.set(name, t)
|
|
t._integrate(this, null)
|
|
return t
|
|
} else {
|
|
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
|
|
}
|
|
}
|
|
return type
|
|
}
|
|
|
|
/**
|
|
* @template T
|
|
* @param {string} [name]
|
|
* @return {YArray<T>}
|
|
*
|
|
* @public
|
|
*/
|
|
getArray (name = '') {
|
|
// @ts-ignore
|
|
return this.get(name, YArray)
|
|
}
|
|
|
|
/**
|
|
* @param {string} [name]
|
|
* @return {YText}
|
|
*
|
|
* @public
|
|
*/
|
|
getText (name = '') {
|
|
// @ts-ignore
|
|
return this.get(name, YText)
|
|
}
|
|
|
|
/**
|
|
* @param {string} [name]
|
|
* @return {YMap<any>}
|
|
*
|
|
* @public
|
|
*/
|
|
getMap (name = '') {
|
|
// @ts-ignore
|
|
return this.get(name, YMap)
|
|
}
|
|
|
|
/**
|
|
* @param {string} [name]
|
|
* @return {YXmlFragment}
|
|
*
|
|
* @public
|
|
*/
|
|
getXmlFragment (name = '') {
|
|
// @ts-ignore
|
|
return this.get(name, YXmlFragment)
|
|
}
|
|
|
|
/**
|
|
* Converts the entire document into a js object, recursively traversing each yjs type
|
|
* Doesn't log types that have not been defined (using ydoc.getType(..)).
|
|
*
|
|
* @deprecated Do not use this method and rather call toJSON directly on the shared types.
|
|
*
|
|
* @return {Object<string, any>}
|
|
*/
|
|
toJSON () {
|
|
/**
|
|
* @type {Object<string, any>}
|
|
*/
|
|
const doc = {}
|
|
|
|
this.share.forEach((value, key) => {
|
|
doc[key] = value.toJSON()
|
|
})
|
|
|
|
return doc
|
|
}
|
|
|
|
/**
|
|
* Emit `destroy` event and unregister all event handlers.
|
|
*/
|
|
destroy () {
|
|
array.from(this.subdocs).forEach(subdoc => subdoc.destroy())
|
|
const item = this._item
|
|
if (item !== null) {
|
|
this._item = null
|
|
const content = /** @type {ContentDoc} */ (item.content)
|
|
if (item.deleted) {
|
|
// @ts-ignore
|
|
content.doc = null
|
|
} else {
|
|
content.doc = new Doc({ guid: this.guid, ...content.opts })
|
|
content.doc._item = item
|
|
}
|
|
transact(/** @type {any} */ (item).parent.doc, transaction => {
|
|
if (!item.deleted) {
|
|
transaction.subdocsAdded.add(content.doc)
|
|
}
|
|
transaction.subdocsRemoved.add(this)
|
|
}, null, true)
|
|
}
|
|
this.emit('destroyed', [true])
|
|
this.emit('destroy', [this])
|
|
super.destroy()
|
|
}
|
|
|
|
/**
|
|
* @param {string} eventName
|
|
* @param {function(...any):any} f
|
|
*/
|
|
on (eventName, f) {
|
|
super.on(eventName, f)
|
|
}
|
|
|
|
/**
|
|
* @param {string} eventName
|
|
* @param {function} f
|
|
*/
|
|
off (eventName, f) {
|
|
super.off(eventName, f)
|
|
}
|
|
}
|