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
JavaScript
/**
* @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` – Before the first initialization, and after crashes, before the item is ready.
* * `ready` – A state when the user can interact with the item.
* * `crashed` – 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` – A state when the watchdog stops reacting to errors and keeps the item it is watching crashed,
* * `destroyed` – 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.
*/