/** * @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 */ 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>} */ this.share = new Map() this.store = new StructStore() /** * @type {Transaction | null} */ this._transaction = null /** * @type {Array} */ this._transactionCleanups = [] /** * @type {Set} */ 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} 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} * * @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} * * @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} */ toJSON () { /** * @type {Object} */ 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) } }