UNPKG

ckeditor5-image-upload-base64

Version:

The development environment of CKEditor 5 – the best browser-based rich text editor.

360 lines (316 loc) 10.9 kB
/** * @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/watchdog */ /* globals window */ /** * An abstract watchdog class that handles most of the error handling process and the state of the underlying component. * * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and how to use it. * * @private * @abstract */ export default class Watchdog { /** * @param {module:watchdog/watchdog~WatchdogConfig} config The watchdog plugin configuration. */ constructor( config ) { /** * An array of crashes saved as an object with the following properties: * * * `message`: `String`, * * `stack`: `String`, * * `date`: `Number`, * * `filename`: `String | undefined`, * * `lineno`: `Number | undefined`, * * `colno`: `Number | undefined`, * * @public * @readonly * @type {Array.<Object>} */ this.crashes = []; /** * Specifies the state of the item watched by the watchdog. The state can be one of the following values: * * * `initializing` &ndash; Before the first initialization, and after crashes, before the item is ready. * * `ready` &ndash; A state when the user can interact with the item. * * `crashed` &ndash; A state when an error occurs. It quickly changes to `initializing` or `crashedPermanently` * depending on how many and how frequent errors have been caught recently. * * `crashedPermanently` &ndash; A state when the watchdog stops reacting to errors and keeps the item it is watching crashed, * * `destroyed` &ndash; A state when the item is manually destroyed by the user after calling `watchdog.destroy()`. * * @public * @type {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} */ this.state = 'initializing'; /** * @protected * @type {Number} * @see module:watchdog/watchdog~WatchdogConfig */ this._crashNumberLimit = typeof config.crashNumberLimit === 'number' ? config.crashNumberLimit : 3; /** * Returns the result of the `Date.now()` call. It can be overridden in tests to mock time as some popular * approaches like `sinon.useFakeTimers()` do not work well with error handling. * * @protected */ this._now = Date.now; /** * @protected * @type {Number} * @see module:watchdog/watchdog~WatchdogConfig */ this._minimumNonErrorTimePeriod = typeof config.minimumNonErrorTimePeriod === 'number' ? config.minimumNonErrorTimePeriod : 5000; /** * Checks if the event error comes from the underlying item and restarts the item. * * @private * @type {Function} */ this._boundErrorHandler = evt => { // `evt.error` is exposed by EventError while `evt.reason` is available in PromiseRejectionEvent. const error = evt.error || evt.reason; // Note that `evt.reason` might be everything that is in the promise rejection. // Similarly everything that is thrown lands in `evt.error`. if ( error instanceof Error ) { this._handleError( error, evt ); } }; /** * The creation method. * * @protected * @member {Function} #_creator * @see #setCreator */ /** * The destruction method. * * @protected * @member {Function} #_destructor * @see #setDestructor */ /** * The watched item. * * @abstract * @protected * @member {Object|undefined} #_item */ /** * The method responsible for restarting the watched item. * * @abstract * @protected * @method #_restart */ /** * Traverses the error context and the watched item to find out whether the error should * be handled by the given item. * * @abstract * @protected * @method #_isErrorComingFromThisItem * @param {module:utils/ckeditorerror~CKEditorError} error */ /** * A dictionary of event emitter listeners. * * @private * @type {Object.<String,Array.<Function>>} */ this._listeners = {}; if ( !this._restart ) { throw new Error( 'The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. ' + 'Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.' ); } } /** * Sets the function that is responsible for creating watched items. * * @param {Function} creator A callback responsible for creating an item. Returns a promise * that is resolved when the item is created. */ setCreator( creator ) { this._creator = creator; } /** * Sets the function that is responsible for destroying watched items. * * @param {Function} destructor A callback that takes the item and returns the promise * to the destroying process. */ setDestructor( destructor ) { this._destructor = destructor; } /** * Destroys the watchdog and releases the resources. */ destroy() { this._stopErrorHandling(); this._listeners = {}; } /** * Starts listening to a specific event name by registering a callback that will be executed * whenever an event with a given name fires. * * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation. * * @param {String} eventName The event name. * @param {Function} callback A callback which will be added to event listeners. */ on( eventName, callback ) { if ( !this._listeners[ eventName ] ) { this._listeners[ eventName ] = []; } this._listeners[ eventName ].push( callback ); } /** * Stops listening to the specified event name by removing the callback from event listeners. * * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation. * * @param {String} eventName The event name. * @param {Function} callback A callback which will be removed from event listeners. */ off( eventName, callback ) { this._listeners[ eventName ] = this._listeners[ eventName ] .filter( cb => cb !== callback ); } /** * Fires an event with a given event name and arguments. * * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation. * * @protected * @param {String} eventName The event name. * @param {...*} args Event arguments. */ _fire( eventName, ...args ) { const callbacks = this._listeners[ eventName ] || []; for ( const callback of callbacks ) { callback.apply( this, [ null, ...args ] ); } } /** * Starts error handling by attaching global error handlers. * * @protected */ _startErrorHandling() { window.addEventListener( 'error', this._boundErrorHandler ); window.addEventListener( 'unhandledrejection', this._boundErrorHandler ); } /** * Stops error handling by detaching global error handlers. * * @protected */ _stopErrorHandling() { window.removeEventListener( 'error', this._boundErrorHandler ); window.removeEventListener( 'unhandledrejection', this._boundErrorHandler ); } /** * Checks if an error comes from the watched item and restarts it. * It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only. * * @private * @fires error * @param {Error} error Error. * @param {ErrorEvent|PromiseRejectionEvent} evt An error event. */ _handleError( error, evt ) { // @if CK_DEBUG // if ( error.is && error.is( 'CKEditorError' ) && error.context === undefined ) { // @if CK_DEBUG // console.warn( 'The error is missing its context and Watchdog cannot restart the proper item.' ); // @if CK_DEBUG // } if ( this._shouldReactToError( error ) ) { this.crashes.push( { message: error.message, stack: error.stack, // `evt.filename`, `evt.lineno` and `evt.colno` are available only in ErrorEvent events filename: evt.filename, lineno: evt.lineno, colno: evt.colno, date: this._now() } ); const causesRestart = this._shouldRestart(); this.state = 'crashed'; this._fire( 'stateChange' ); this._fire( 'error', { error, causesRestart } ); if ( causesRestart ) { this._restart(); } else { this.state = 'crashedPermanently'; this._fire( 'stateChange' ); } } } /** * Checks whether an error should be handled by the watchdog. * * @private * @param {Error} error An error that was caught by the error handling process. */ _shouldReactToError( error ) { return ( error.is && error.is( 'CKEditorError' ) && error.context !== undefined && // In some cases the watched item should not be restarted - e.g. during the item initialization. // That's why the `null` was introduced as a correct error context which does cause restarting. error.context !== null && // Do not react to errors if the watchdog is in states other than `ready`. this.state === 'ready' && this._isErrorComingFromThisItem( error ) ); } /** * Checks if the watchdog should restart the underlying item. * * @private */ _shouldRestart() { if ( this.crashes.length <= this._crashNumberLimit ) { return true; } const lastErrorTime = this.crashes[ this.crashes.length - 1 ].date; const firstMeaningfulErrorTime = this.crashes[ this.crashes.length - 1 - this._crashNumberLimit ].date; const averageNonErrorTimePeriod = ( lastErrorTime - firstMeaningfulErrorTime ) / this._crashNumberLimit; return averageNonErrorTimePeriod > this._minimumNonErrorTimePeriod; } /** * Fired when a new {@link module:utils/ckeditorerror~CKEditorError `CKEditorError`} error connected to the watchdog instance occurs * and the watchdog will react to it. * * watchdog.on( 'error', ( evt, { error, causesRestart } ) => { * console.log( 'An error occurred.' ); * } ); * * @event error */ } /** * The watchdog plugin configuration. * * @typedef {Object} WatchdogConfig * * @property {Number} [crashNumberLimit=3] A threshold specifying the number of watched item crashes * when the watchdog stops restarting the item in case of errors. * After this limit is reached and the time between the last errors is shorter than `minimumNonErrorTimePeriod`, * the watchdog changes its state to `crashedPermanently` and it stops restarting the item. This prevents an infinite restart loop. * * @property {Number} [minimumNonErrorTimePeriod=5000] An average number of milliseconds between the last watched item errors * (defaults to 5000). When the period of time between errors is lower than that and the `crashNumberLimit` is also reached, * the watchdog changes its state to `crashedPermanently` and it stops restarting the item. This prevents an infinite restart loop. * * @property {Number} [saveInterval=5000] A minimum number of milliseconds between saving the editor data internally (defaults to 5000). * Note that for large documents this might impact the editor performance. */