| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- /**
- * @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 a 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 frequency 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
- * @member {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} #state
- */
- this.state = 'initializing';
- /**
- * @protected
- * @type {Number}
- * @see module:watchdog/watchdog~WatchdogConfig
- */
- this._crashNumberLimit = typeof config.crashNumberLimit === 'number' ? config.crashNumberLimit : 3;
- /**
- * Returns the result of `Date.now()` call. It can be overridden in tests to mock time as the popular
- * approaches like `sinon.useFakeTimers()` does 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 watchded 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 destructing 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 the specific event name by registering a callback that will be executed
- * whenever an event with given name fires.
- *
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
- *
- * @param {String} eventName 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 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 given event name and arguments.
- *
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
- *
- * @protected
- * @param {String} eventName 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 the 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 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 the 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.
- */
- _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 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 amount of milliseconds between 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 editor data internally, (defaults to 5000).
- * Note that for large documents this might have an impact on the editor performance.
- */
|