| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module watchdog/contextwatchdog
- */
- /* globals console */
- import Watchdog from './watchdog';
- import EditorWatchdog from './editorwatchdog';
- import areConnectedThroughProperties from './utils/areconnectedthroughproperties';
- import getSubNodes from './utils/getsubnodes';
- /**
- * A watchdog for the {@link module:core/context~Context} class.
- *
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
- * how to use it.
- *
- * @extends {module:watchdog/watchdog~Watchdog}
- */
- export default class ContextWatchdog extends Watchdog {
- /**
- * The context watchdog class constructor.
- *
- * const watchdog = new ContextWatchdog( Context );
- *
- * await watchdog.create( contextConfiguration );
- *
- * await watchdog.add( item );
- *
- * See the {@glink features/watchdog Watchdog feature guide} to learn more how to use this feature.
- *
- * @param {Function} Context The {@link module:core/context~Context} class.
- * @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog configuration.
- */
- constructor( Context, watchdogConfig = {} ) {
- super( watchdogConfig );
- /**
- * A map of internal watchdogs for added items.
- *
- * @protected
- * @type {Map.<string,module:watchdog/watchdog~EditorWatchdog>}
- */
- this._watchdogs = new Map();
- /**
- * The watchdog configuration.
- *
- * @private
- * @type {module:watchdog/watchdog~WatchdogConfig}
- */
- this._watchdogConfig = watchdogConfig;
- /**
- * The current context instance.
- *
- * @private
- * @type {module:core/context~Context|null}
- */
- this._context = null;
- /**
- * Context properties (nodes/references) that are gathered during the initial context creation
- * and are used to distinguish the origin of an error.
- *
- * @private
- * @type {Set.<*>}
- */
- this._contextProps = new Set();
- /**
- * An action queue, which is used to handle async functions queuing.
- *
- * @private
- * @type {ActionQueue}
- */
- this._actionQueue = new ActionQueue();
- /**
- * The configuration for the {@link module:core/context~Context}.
- *
- * @private
- * @member {Object} #_contextConfig
- */
- /**
- * The context configuration.
- *
- * @private
- * @member {Object|undefined} #_config
- */
- // Default creator and destructor.
- this._creator = contextConfig => Context.create( contextConfig );
- this._destructor = context => context.destroy();
- this._actionQueue.onEmpty( () => {
- if ( this.state === 'initializing' ) {
- this.state = 'ready';
- this._fire( 'stateChange' );
- }
- } );
- /**
- * Sets the function that is responsible for the context creation.
- * It expects a function that should return a promise (or `undefined`).
- *
- * watchdog.setCreator( config => Context.create( config ) );
- *
- * @method #setCreator
- * @param {Function} creator
- */
- /**
- * Sets the function that is responsible for the context destruction.
- * Overrides the default destruction function, which destroys only the context instance.
- * It expects a function that should return a promise (or `undefined`).
- *
- * watchdog.setDestructor( context => {
- * // Do something before the context is destroyed.
- *
- * return context
- * .destroy()
- * .then( () => {
- * // Do something after the context is destroyed.
- * } );
- * } );
- *
- * @method #setDestructor
- * @param {Function} destructor
- */
- }
- /**
- * The context instance. Keep in mind that this property might be changed when the context watchdog restarts,
- * so do not keep this instance internally. Always operate on the `ContextWatchdog#context` property.
- *
- * @type {module:core/context~Context|null}
- */
- get context() {
- return this._context;
- }
- /**
- * Initializes the context watchdog. Once it is created, the watchdog takes care about
- * recreating the context and the provided items, and starts the error handling mechanism.
- *
- * await watchdog.create( {
- * plugins: []
- * } );
- *
- * @param {Object} [contextConfig] The context configuration. See {@link module:core/context~Context}.
- * @returns {Promise}
- */
- create( contextConfig = {} ) {
- return this._actionQueue.enqueue( () => {
- this._contextConfig = contextConfig;
- return this._create();
- } );
- }
- /**
- * Returns an item instance with the given `itemId`.
- *
- * const editor1 = watchdog.getItem( 'editor1' );
- *
- * @param {String} itemId The item ID.
- * @returns {*} The item instance or `undefined` if an item with a given ID has not been found.
- */
- getItem( itemId ) {
- const watchdog = this._getWatchdog( itemId );
- return watchdog._item;
- }
- /**
- * Gets the state of the given item. See {@link #state} for a list of available states.
- *
- * const editor1State = watchdog.getItemState( 'editor1' );
- *
- * @param {String} itemId Item ID.
- * @returns {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} The state of the item.
- */
- getItemState( itemId ) {
- const watchdog = this._getWatchdog( itemId );
- return watchdog.state;
- }
- /**
- * Adds items to the watchdog. Once created, instances of these items will be available using the {@link #getItem} method.
- *
- * Items can be passed together as an array of objects:
- *
- * await watchdog.add( [ {
- * id: 'editor1',
- * type: 'editor',
- * sourceElementOrData: document.querySelector( '#editor' ),
- * config: {
- * plugins: [ Essentials, Paragraph, Bold, Italic ],
- * toolbar: [ 'bold', 'italic', 'alignment' ]
- * },
- * creator: ( element, config ) => ClassicEditor.create( element, config )
- * } ] );
- *
- * Or one by one as objects:
- *
- * await watchdog.add( {
- * id: 'editor1',
- * type: 'editor',
- * sourceElementOrData: document.querySelector( '#editor' ),
- * config: {
- * plugins: [ Essentials, Paragraph, Bold, Italic ],
- * toolbar: [ 'bold', 'italic', 'alignment' ]
- * },
- * creator: ( element, config ) => ClassicEditor.create( element, config )
- * ] );
- *
- * Then an instance can be retrieved using the {@link #getItem} method:
- *
- * const editor1 = watchdog.getItem( 'editor1' );
- *
- * Note that this method can be called multiple times, but for performance reasons it is better
- * to pass all items together.
- *
- * @param {module:watchdog/contextwatchdog~WatchdogItemConfiguration|Array.<module:watchdog/contextwatchdog~WatchdogItemConfiguration>}
- * itemConfigurationOrItemConfigurations An item configuration object or an array of item configurations.
- * @returns {Promise}
- */
- add( itemConfigurationOrItemConfigurations ) {
- const itemConfigurations = Array.isArray( itemConfigurationOrItemConfigurations ) ?
- itemConfigurationOrItemConfigurations :
- [ itemConfigurationOrItemConfigurations ];
- return this._actionQueue.enqueue( () => {
- if ( this.state === 'destroyed' ) {
- throw new Error( 'Cannot add items to destroyed watchdog.' );
- }
- if ( !this._context ) {
- throw new Error( 'Context was not created yet. You should call the `ContextWatchdog#create()` method first.' );
- }
- // Create new watchdogs.
- return Promise.all( itemConfigurations.map( item => {
- let watchdog;
- if ( this._watchdogs.has( item.id ) ) {
- throw new Error( `Item with the given id is already added: '${ item.id }'.` );
- }
- if ( item.type === 'editor' ) {
- watchdog = new EditorWatchdog( this._watchdogConfig );
- watchdog.setCreator( item.creator );
- watchdog._setExcludedProperties( this._contextProps );
- if ( item.destructor ) {
- watchdog.setDestructor( item.destructor );
- }
- this._watchdogs.set( item.id, watchdog );
- // Enqueue the internal watchdog errors within the main queue.
- // And propagate the internal `error` events as `itemError` event.
- watchdog.on( 'error', ( evt, { error, causesRestart } ) => {
- this._fire( 'itemError', { itemId: item.id, error } );
- // Do not enqueue the item restart action if the item will not restart.
- if ( !causesRestart ) {
- return;
- }
- this._actionQueue.enqueue( () => new Promise( res => {
- watchdog.on( 'restart', rethrowRestartEventOnce.bind( this ) );
- function rethrowRestartEventOnce() {
- watchdog.off( 'restart', rethrowRestartEventOnce );
- this._fire( 'itemRestart', { itemId: item.id } );
- res();
- }
- } ) );
- } );
- return watchdog.create( item.sourceElementOrData, item.config, this._context );
- } else {
- throw new Error( `Not supported item type: '${ item.type }'.` );
- }
- } ) );
- } );
- }
- /**
- * Removes and destroys item(s) with given ID(s).
- *
- * await watchdog.remove( 'editor1' );
- *
- * Or
- *
- * await watchdog.remove( [ 'editor1', 'editor2' ] );
- *
- * @param {Array.<String>|String} itemIdOrItemIds Item ID or an array of item IDs.
- * @returns {Promise}
- */
- remove( itemIdOrItemIds ) {
- const itemIds = Array.isArray( itemIdOrItemIds ) ?
- itemIdOrItemIds :
- [ itemIdOrItemIds ];
- return this._actionQueue.enqueue( () => {
- return Promise.all( itemIds.map( itemId => {
- const watchdog = this._getWatchdog( itemId );
- this._watchdogs.delete( itemId );
- return watchdog.destroy();
- } ) );
- } );
- }
- /**
- * Destroys the context watchdog and all added items.
- * Once the context watchdog is destroyed, new items cannot be added.
- *
- * await watchdog.destroy();
- *
- * @returns {Promise}
- */
- destroy() {
- return this._actionQueue.enqueue( () => {
- this.state = 'destroyed';
- this._fire( 'stateChange' );
- super.destroy();
- return this._destroy();
- } );
- }
- /**
- * Restarts the context watchdog.
- *
- * @protected
- * @returns {Promise}
- */
- _restart() {
- return this._actionQueue.enqueue( () => {
- this.state = 'initializing';
- this._fire( 'stateChange' );
- return this._destroy()
- .catch( err => {
- console.error( 'An error happened during destroying the context or items.', err );
- } )
- .then( () => this._create() )
- .then( () => this._fire( 'restart' ) );
- } );
- }
- /**
- * @private
- * @returns {Promise}
- */
- _create() {
- return Promise.resolve()
- .then( () => {
- this._startErrorHandling();
- return this._creator( this._contextConfig );
- } )
- .then( context => {
- this._context = context;
- this._contextProps = getSubNodes( this._context );
- return Promise.all(
- Array.from( this._watchdogs.values() )
- .map( watchdog => {
- watchdog._setExcludedProperties( this._contextProps );
- return watchdog.create( undefined, undefined, this._context );
- } )
- );
- } );
- }
- /**
- * Destroys the context instance and all added items.
- *
- * @private
- * @returns {Promise}
- */
- _destroy() {
- return Promise.resolve()
- .then( () => {
- this._stopErrorHandling();
- const context = this._context;
- this._context = null;
- this._contextProps = new Set();
- return Promise.all(
- Array.from( this._watchdogs.values() )
- .map( watchdog => watchdog.destroy() )
- )
- // Context destructor destroys each editor.
- .then( () => this._destructor( context ) );
- } );
- }
- /**
- * Returns the watchdog for a given item ID.
- *
- * @protected
- * @param {String} itemId Item ID.
- * @returns {module:watchdog/watchdog~Watchdog} Watchdog
- */
- _getWatchdog( itemId ) {
- const watchdog = this._watchdogs.get( itemId );
- if ( !watchdog ) {
- throw new Error( `Item with the given id was not registered: ${ itemId }.` );
- }
- return watchdog;
- }
- /**
- * Checks whether an error comes from the context instance and not from the item instances.
- *
- * @protected
- * @param {Error} error
- * @returns {Boolean}
- */
- _isErrorComingFromThisItem( error ) {
- for ( const watchdog of this._watchdogs.values() ) {
- if ( watchdog._isErrorComingFromThisItem( error ) ) {
- return false;
- }
- }
- return areConnectedThroughProperties( this._context, error.context );
- }
- /**
- * Fired after the watchdog restarts the context and the added items because of a crash.
- *
- * watchdog.on( 'restart', () => {
- * console.log( 'The context has been restarted.' );
- * } );
- *
- * @event restart
- */
- /**
- * Fired when a new error occurred in one of the added items.
- *
- * watchdog.on( 'itemError', ( evt, { error, itemId, causesRestart } ) => {
- * console.log( `An error occurred in an item with the '${ itemId }' ID.` );
- * } );
- *
- * @event itemError
- */
- /**
- * Fired after an item has been restarted.
- *
- * watchdog.on( 'itemRestart', ( evt, { itemId } ) => {
- * console.log( 'An item with with the '${ itemId }' ID has been restarted.' );
- * } );
- *
- * @event itemRestart
- */
- }
- // An action queue that allows queuing async functions.
- class ActionQueue {
- constructor() {
- // @type {Promise}
- this._promiseQueue = Promise.resolve();
- // @type {Array.<Function>}
- this._onEmptyCallbacks = [];
- }
- // Used to register callbacks that will be run when the queue becomes empty.
- //
- // @param {Function} onEmptyCallback A callback that will be run whenever the queue becomes empty.
- onEmpty( onEmptyCallback ) {
- this._onEmptyCallbacks.push( onEmptyCallback );
- }
- // It adds asynchronous actions (functions) to the queue and runs them one by one.
- //
- // @param {Function} action A function that should be enqueued.
- // @returns {Promise}
- enqueue( action ) {
- let nonErrorQueue;
- const queueWithAction = this._promiseQueue
- .then( action )
- .then( () => {
- if ( this._promiseQueue === nonErrorQueue ) {
- this._onEmptyCallbacks.forEach( cb => cb() );
- }
- } );
- // Catch all errors in the main queue to stack promises even if an error occurred in the past.
- nonErrorQueue = this._promiseQueue = queueWithAction.catch( () => { } );
- return queueWithAction;
- }
- }
- /**
- * The watchdog item configuration interface.
- *
- * @typedef {module:watchdog/contextwatchdog~EditorWatchdogConfiguration} module:watchdog/contextwatchdog~WatchdogItemConfiguration
- */
- /**
- * The editor watchdog configuration interface specifies how editors should be created and destroyed.
- *
- * @typedef {Object} module:watchdog/contextwatchdog~EditorWatchdogConfiguration
- *
- * @property {String} id A unique item identificator.
- *
- * @property {'editor'} type The type of the item to create. At the moment, only `'editor'` is supported.
- *
- * @property {Function} creator A function that initializes the item (the editor). The function takes editor initialization arguments
- * and should return a promise. For example: `( el, config ) => ClassicEditor.create( el, config )`.
- *
- * @property {Function} [destructor] A function that destroys the item instance (the editor). The function
- * takes an item and should return a promise. For example: `editor => editor.destroy()`
- *
- * @property {String|HTMLElement} sourceElementOrData The source element or data that will be passed
- * as the first argument to the `Editor.create()` method.
- *
- * @property {Object} config An editor configuration.
- */
|