| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- /**
- * @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';
- /**
- * The Context Watchdog class.
- *
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
- * how to use it.
- */
- export default class ContextWatchdog extends Watchdog {
- /**
- * The `ContextWatchdog` class constructor.
- *
- * const contextWatchdog = new ContextWatchdog( Context );
- *
- * await contextWatchdog.create( contextConfiguration );
- *
- * await contextWatchdog.add( watchdogItem );
- *
- * See {@glink features/watchdog the 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 props (nodes/references) that are gathered during the initial context creation
- * and are used to distinguish error origin.
- *
- * @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();
- /**
- * Config 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';
- }
- } );
- }
- /**
- * The context instance. Keep in mind that this property might be changed when the ContextWatchdog 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's created the watchdog takes care about
- * recreating the context and provided items and starts the error handling mechanism.
- *
- * @param {Object} [contextConfig] Context configuration. See {@link module:core/context~Context}.
- * @returns {Promise}
- */
- create( contextConfig = {} ) {
- return this._actionQueue.enqueue( () => {
- this._contextConfig = contextConfig;
- return this._create();
- } );
- }
- /**
- * Returns the item instance with the given `itemId`.
- *
- * const editor1 = contextWatchdog.get( 'editor1' );
- *
- * @param {String} itemId The item id.
- * @returns {*} The item instance or `undefined`.
- */
- get( itemId ) {
- const watchdog = this._getWatchdog( itemId );
- return watchdog._instance;
- }
- /**
- * Gets state of the given item. For the list of available states see {@link #state}.
- *
- * @param {String} itemId Item id.
- * @returns {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} The state of the item.
- */
- getState( itemId ) {
- const watchdog = this._getWatchdog( itemId );
- return watchdog.state;
- }
- /**
- * Adds items to the watchdog. Instances of these items once created will be available using the {@link #get} 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 )
- * ] );
- *
- * And then the instance can be retrieved using the {@link #get} method:
- *
- * const editor1 = watchdog.get( 'editor1' );
- *
- * Note that this method can be called multiple times, but for performance reasons it's better
- * to pass all items together.
- *
- * @param {module:watchdog/contextwatchdog~WatchdogItemConfiguration|Array.<module:watchdog/contextwatchdog~WatchdogItemConfiguration>}
- * itemConfigurationOrItemConfigurations 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.
- watchdog.on( 'error', () => {
- if ( watchdog._shouldRestart() ) {
- this._actionQueue.enqueue( () => new Promise( res => {
- watchdog.once( 'restart', () => 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) by its / their id(s).
- *
- * @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 `ContextWatchdog` and all added items.
- * Once the `ContextWatchdog` is destroyed new items can not be added.
- *
- * @returns {Promise}
- */
- destroy() {
- return this._actionQueue.enqueue( () => {
- this.state = 'destroyed';
- super.destroy();
- return this._destroy();
- } );
- }
- /**
- * Restarts the `ContextWatchdog`.
- *
- * @protected
- * @returns {Promise}
- */
- _restart() {
- return this._actionQueue.enqueue( () => {
- this.state = 'initializing';
- return this._destroy()
- .catch( err => {
- console.error( 'An error happened during destructing.', 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 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 watchdog for the 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 the error comes from the Context and not from Editor or ContextItem instances.
- *
- * @protected
- * @param {Error} error
- * @returns {Boolean}
- */
- _isErrorComingFromThisInstance( error ) {
- for ( const watchdog of this._watchdogs.values() ) {
- if ( watchdog._isErrorComingFromThisInstance( error ) ) {
- return false;
- }
- }
- // Return true only if the error comes directly from the context.
- // Ignore cases when the error comes from editors.
- return areConnectedThroughProperties( this._contextProps, error.context );
- }
- }
- // An action queue that allows queuing async functions.
- class ActionQueue {
- constructor() {
- // @type {Array.<Function>}
- this._queuedActions = [];
- // @type {WeakMap.<Function, Function>}
- this._resolveCallbacks = new WeakMap();
- // @type {Array.<Function>}
- this._onEmptyCallbacks = [];
- }
- // A method 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 ) {
- this._queuedActions.push( action );
- if ( this._queuedActions.length > 1 ) {
- // This means that the action handler is already running.
- // Hence we can just register the resolve callback and wait.
- return new Promise( res => {
- this._resolveCallbacks.set( action, res );
- } );
- }
- return new Promise( ( res, rej ) => {
- this._handleActions( res, rej );
- } );
- }
- // It handles queued actions one by one.
- // @param {Function} res
- // @param {Function} rej
- _handleActions( res, rej ) {
- const action = this._queuedActions[ 0 ];
- if ( !action ) {
- this._onEmptyCallbacks.forEach( cb => cb() );
- res();
- return;
- }
- const resolve = this._resolveCallbacks.get( action );
- return Promise.resolve()
- .then( () => action() )
- .then( () => {
- if ( resolve ) {
- resolve();
- }
- this._queuedActions.shift();
- return this._handleActions( res, rej );
- } )
- .catch( err => {
- rej( err );
- this._queuedActions.shift();
- // Run pending actions even if an error has happened to unlock the queue.
- return this._handleActions( res, rej );
- } );
- }
- }
- /**
- * The WatchdogItemConfiguration interface.
- *
- * @typedef {module:watchdog/contextwatchdog~EditorWatchdogConfiguration} module:watchdog/contextwatchdog~WatchdogItemConfiguration
- */
- /**
- * The EditorWatchdogConfiguration 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 Type of the item to create. At the moment, only `'editor'` is supported.
- *
- * @property {Function} creator A function that initializes the editor. E.g. `( el, config ) => ClassicEditor.create( el, config )`.
- *
- * @property {Function} [destructor] A function that destroys the editor. E.g. `editor => editor.destroy()`
- *
- * @property {String|HTMLElement} sourceElementOrData The source element or data which will be passed
- * as the first argument to the `Editor.create()` method.
- *
- * @property {Object} config An editor configuration.
- */
|