UNPKG

@y/y

Version:

Shared Editing Library

253 lines (239 loc) • 9.13 kB
/** * @module Y */ import { ObservableV2 } from 'lib0/observable' import * as random from 'lib0/random' import * as map from 'lib0/map' import * as array from 'lib0/array' import * as promise from 'lib0/promise' import { StructStore } from './StructStore.js' import { transact, generateNewClientId } from './Transaction.js' import { YType } from '../ytype.js' import { $ydoc } from './schemas.js' /** * @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 {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection. * @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. * @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load() * @property {boolean} [DocOpts.isSuggestionDoc] Set to true if this document merely suggests * changes. If this flag is not set in a suggestion document, automatic formatting changes will be * displayed as suggestions, which might not be intended. */ /** * @typedef {Object} DocEvents * @property {function(Doc):void} DocEvents.destroy * @property {function(Doc):void} DocEvents.load * @property {function(boolean, Doc):void} DocEvents.sync * @property {function(Uint8Array<ArrayBuffer>, any, Doc, Transaction):void} DocEvents.update * @property {function(Uint8Array<ArrayBuffer>, any, Doc, Transaction):void} DocEvents.updateV2 * @property {function(Doc):void} DocEvents.beforeAllTransactions * @property {function(Transaction, Doc):void} DocEvents.beforeTransaction * @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls * @property {function(Transaction, Doc):void} DocEvents.afterTransaction * @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup * @property {function(Doc, Array<Transaction>):void} DocEvents.afterAllTransactions * @property {function({ loaded: Set<Doc>, added: Set<Doc>, removed: Set<Doc> }, Doc, Transaction):void} DocEvents.subdocs */ /** * A Yjs instance handles the state of shared data. * @extends ObservableV2<DocEvents> */ export class Doc extends ObservableV2 { /** * @param {DocOpts} opts configuration */ constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true, isSuggestionDoc = false } = {}) { super() this.gc = gc this.gcFilter = gcFilter this.clientID = generateNewClientId() this.guid = guid this.collectionid = collectionid this.isSuggestionDoc = isSuggestionDoc this.cleanupFormatting = !isSuggestionDoc /** * @type {Map<string, YType>} */ 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 = shouldLoad this.autoLoad = autoLoad this.meta = meta /** * This is set to true when the persistence provider loaded the document from the database or when the `sync` event fires. * Note that not all providers implement this feature. Provider authors are encouraged to fire the `load` event when the doc content is loaded from the database. * * @type {boolean} */ this.isLoaded = false /** * This is set to true when the connection provider has successfully synced with a backend. * Note that when using peer-to-peer providers this event may not provide very useful. * Also note that not all providers implement this feature. Provider authors are encouraged to fire * the `sync` event when the doc has been synced (with `true` as a parameter) or if connection is * lost (with false as a parameter). */ this.isSynced = false this.isDestroyed = false /** * Promise that resolves once the document has been loaded from a persistence provider. */ this.whenLoaded = promise.create(resolve => { this.on('load', () => { this.isLoaded = true resolve(this) }) }) const provideSyncedPromise = () => promise.create(resolve => { /** * @param {boolean} isSynced */ const eventHandler = (isSynced) => { if (isSynced === undefined || isSynced === true) { this.off('sync', eventHandler) resolve() } } this.on('sync', eventHandler) }) this.on('sync', isSynced => { if (isSynced === false && this.isSynced) { this.whenSynced = provideSyncedPromise() } this.isSynced = isSynced === undefined || isSynced === true if (this.isSynced && !this.isLoaded) { this.emit('load', [this]) } }) /** * Promise that resolves once the document has been synced with a backend. * This promise is recreated when the connection is lost. * Note the documentation about the `isSynced` property. */ this.whenSynced = provideSyncedPromise() } /** * 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. * * @template T * @param {function(Transaction):T} 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 * @param {boolean} [local] * @return T */ transact (f, origin = null, local = true) { return transact(this, f, origin, local) } /** * Define a shared data type. * * Multiple calls of `ydoc.get(name)` yield the same result * and do not overwrite each other. I.e. * `ydoc.get(name) === ydoc.get(name)` * * After this method is called, the type is also available on `ydoc.share.get(name)`. * * @param {string} key * @param {string?} name Type-name * * @return {YType} */ get (key = '', name = null) { return map.setIfUndefined(this.share, key, () => { const t = new YType(name) t._integrate(this, null) return t }) } /** * 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 () { this.isDestroyed = true array.from(this.subdocs).forEach(subdoc => subdoc.destroy()) const item = this._item if (item !== null) { this._item = null const content = /** @type {ContentDoc} */ (item.content) /** * new content doc which replaces the new one */ const contentDoc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false }) content.doc = contentDoc contentDoc._item = item transact(/** @type {any} */ (item).parent.doc, transaction => { if (!item.deleted) { transaction.subdocsAdded.add(contentDoc) } transaction.subdocsRemoved.add(this) }, null, true) } this.emit('destroy', [this]) super.destroy() } } Doc.prototype.$type = $ydoc