@ckeditor/ckeditor5-watchdog 16.0.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/watchdog.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
2
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
@@ -7,25 +7,21 @@
7
7
  * @module watchdog/watchdog
8
8
  */
9
9
 
10
- /* globals console, window */
11
-
12
- import mix from '@ckeditor/ckeditor5-utils/src/mix';
13
- import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
14
- import { throttle, cloneDeepWith, isElement } from 'lodash-es';
15
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
16
- import areConnectedThroughProperties from '@ckeditor/ckeditor5-utils/src/areconnectedthroughproperties';
10
+ /* globals window */
17
11
 
18
12
  /**
19
- * A watchdog for CKEditor 5 editors.
13
+ * An abstract watchdog class that handles most of the error handling process and the state of the underlying component.
14
+ *
15
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and how to use it.
20
16
  *
21
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
22
- * how to use it.
17
+ * @private
18
+ * @abstract
23
19
  */
24
20
  export default class Watchdog {
25
21
  /**
26
- * @param {module:watchdog/watchdog~WatchdogConfig} [config] The watchdog plugin configuration.
22
+ * @param {module:watchdog/watchdog~WatchdogConfig} config The watchdog plugin configuration.
27
23
  */
28
- constructor( config = {} ) {
24
+ constructor( config ) {
29
25
  /**
30
26
  * An array of crashes saved as an object with the following properties:
31
27
  *
@@ -43,46 +39,44 @@ export default class Watchdog {
43
39
  this.crashes = [];
44
40
 
45
41
  /**
46
- * Specifies the state of the editor handled by the watchdog. The state can be one of the following values:
42
+ * Specifies the state of the item watched by the watchdog. The state can be one of the following values:
47
43
  *
48
- * * `initializing` - before the first initialization, and after crashes, before the editor is ready,
49
- * * `ready` - a state when a user can interact with the editor,
50
- * * `crashed` - a state when an error occurs - it quickly changes to `initializing` or `crashedPermanently`
51
- * depending on how many and how frequency errors have been caught recently,
52
- * * `crashedPermanently` - a state when the watchdog stops reacting to errors and keeps the editor crashed,
53
- * * `destroyed` - a state when the editor is manually destroyed by the user after calling `watchdog.destroy()`
44
+ * * `initializing` – Before the first initialization, and after crashes, before the item is ready.
45
+ * * `ready` – A state when the user can interact with the item.
46
+ * * `crashed` – A state when an error occurs. It quickly changes to `initializing` or `crashedPermanently`
47
+ * depending on how many and how frequent errors have been caught recently.
48
+ * * `crashedPermanently` – A state when the watchdog stops reacting to errors and keeps the item it is watching crashed,
49
+ * * `destroyed` – A state when the item is manually destroyed by the user after calling `watchdog.destroy()`.
54
50
  *
55
51
  * @public
56
- * @observable
57
52
  * @member {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} #state
58
53
  */
59
- this.set( 'state', 'initializing' );
54
+ this.state = 'initializing';
60
55
 
61
56
  /**
62
- * @private
57
+ * @protected
63
58
  * @type {Number}
64
59
  * @see module:watchdog/watchdog~WatchdogConfig
65
60
  */
66
61
  this._crashNumberLimit = typeof config.crashNumberLimit === 'number' ? config.crashNumberLimit : 3;
67
62
 
68
63
  /**
69
- * Returns the result of `Date.now()` call. It can be overridden in tests to mock time as the popular
70
- * approaches like `sinon.useFakeTimers()` does not work well with error handling.
64
+ * Returns the result of the `Date.now()` call. It can be overridden in tests to mock time as some popular
65
+ * approaches like `sinon.useFakeTimers()` do not work well with error handling.
71
66
  *
72
67
  * @protected
73
68
  */
74
69
  this._now = Date.now;
75
70
 
76
71
  /**
77
- * @private
72
+ * @protected
78
73
  * @type {Number}
79
74
  * @see module:watchdog/watchdog~WatchdogConfig
80
75
  */
81
76
  this._minimumNonErrorTimePeriod = typeof config.minimumNonErrorTimePeriod === 'number' ? config.minimumNonErrorTimePeriod : 5000;
82
77
 
83
78
  /**
84
- * Checks if the event error comes from the editor that is handled by the watchdog (by checking the error context)
85
- * and restarts the editor.
79
+ * Checks if the event error comes from the underlying item and restarts the item.
86
80
  *
87
81
  * @private
88
82
  * @type {Function}
@@ -99,252 +93,171 @@ export default class Watchdog {
99
93
  };
100
94
 
101
95
  /**
102
- * Throttled save method. The `save()` method is called the specified `saveInterval` after `throttledSave()` is called,
103
- * unless a new action happens in the meantime.
96
+ * The creation method.
104
97
  *
105
- * @private
106
- * @type {Function}
107
- */
108
- this._throttledSave = throttle(
109
- this._save.bind( this ),
110
- typeof config.saveInterval === 'number' ? config.saveInterval : 5000
111
- );
112
-
113
- /**
114
- * The current editor instance.
115
- *
116
- * @private
117
- * @type {module:core/editor/editor~Editor}
118
- */
119
- this._editor = null;
120
-
121
- /**
122
- * The editor creation method.
123
- *
124
- * @private
98
+ * @protected
125
99
  * @member {Function} #_creator
126
100
  * @see #setCreator
127
101
  */
128
102
 
129
103
  /**
130
- * The editor destruction method.
104
+ * The destruction method.
131
105
  *
132
- * @private
106
+ * @protected
133
107
  * @member {Function} #_destructor
134
108
  * @see #setDestructor
135
109
  */
136
- this._destructor = editor => editor.destroy();
137
110
 
138
111
  /**
139
- * The latest saved editor data represented as a root name -> root data object.
112
+ * The watched item.
140
113
  *
141
- * @private
142
- * @member {Object.<String,String>} #_data
114
+ * @abstract
115
+ * @protected
116
+ * @member {Object|undefined} #_item
143
117
  */
144
118
 
145
119
  /**
146
- * The last document version.
120
+ * The method responsible for restarting the watched item.
147
121
  *
148
- * @private
149
- * @member {Number} #_lastDocumentVersion
122
+ * @abstract
123
+ * @protected
124
+ * @method #_restart
150
125
  */
151
126
 
152
127
  /**
153
- * The editor source element or data.
128
+ * Traverses the error context and the watched item to find out whether the error should
129
+ * be handled by the given item.
154
130
  *
155
- * @private
156
- * @member {HTMLElement|String|Object.<String|String>} #_elementOrData
131
+ * @abstract
132
+ * @protected
133
+ * @method #_isErrorComingFromThisItem
134
+ * @param {module:utils/ckeditorerror~CKEditorError} error
157
135
  */
158
136
 
159
137
  /**
160
- * The editor configuration.
138
+ * A dictionary of event emitter listeners.
161
139
  *
162
140
  * @private
163
- * @member {Object|undefined} #_config
141
+ * @type {Object.<String,Array.<Function>>}
164
142
  */
165
- }
143
+ this._listeners = {};
166
144
 
167
- /**
168
- * The current editor instance.
169
- *
170
- * @readonly
171
- * @type {module:core/editor/editor~Editor}
172
- */
173
- get editor() {
174
- return this._editor;
145
+ if ( !this._restart ) {
146
+ throw new Error(
147
+ 'The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. ' +
148
+ 'Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.'
149
+ );
150
+ }
175
151
  }
176
152
 
177
153
  /**
178
- * Sets the function that is responsible for the editor creation.
179
- * It expects a function that should return a promise.
154
+ * Sets the function that is responsible for creating watched items.
180
155
  *
181
- * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
182
- *
183
- * @param {Function} creator
156
+ * @param {Function} creator A callback responsible for creating an item. Returns a promise
157
+ * that is resolved when the item is created.
184
158
  */
185
159
  setCreator( creator ) {
186
160
  this._creator = creator;
187
161
  }
188
162
 
189
163
  /**
190
- * Sets the function that is responsible for the editor destruction.
191
- * Overrides the default destruction function, which destroys only the editor instance.
192
- * It expects a function that should return a promise or `undefined`.
193
- *
194
- * watchdog.setDestructor( editor => {
195
- * // Do something before the editor is destroyed.
164
+ * Sets the function that is responsible for destroying watched items.
196
165
  *
197
- * return editor
198
- * .destroy()
199
- * .then( () => {
200
- * // Do something after the editor is destroyed.
201
- * } );
202
- * } );
203
- *
204
- * @param {Function} destructor
166
+ * @param {Function} destructor A callback that takes the item and returns the promise
167
+ * to the destroying process.
205
168
  */
206
169
  setDestructor( destructor ) {
207
170
  this._destructor = destructor;
208
171
  }
209
172
 
210
173
  /**
211
- * Creates a watched editor instance using the creator passed to the {@link #setCreator `setCreator()`} method or
212
- * the {@link module:watchdog/watchdog~Watchdog.for `Watchdog.for()`} helper.
213
- *
214
- * @param {HTMLElement|String|Object.<String|String>} elementOrData
215
- * @param {module:core/editor/editorconfig~EditorConfig} [config]
216
- *
217
- * @returns {Promise}
174
+ * Destroys the watchdog and releases the resources.
218
175
  */
219
- create( elementOrData, config ) {
220
- if ( !this._creator ) {
221
- /**
222
- * The watchdog's editor creator is not defined. Define it by using
223
- * {@link module:watchdog/watchdog~Watchdog#setCreator `Watchdog#setCreator()`} or
224
- * the {@link module:watchdog/watchdog~Watchdog.for `Watchdog.for()`} helper.
225
- *
226
- * @error watchdog-creator-not-defined
227
- */
228
- throw new CKEditorError(
229
- 'watchdog-creator-not-defined: The watchdog\'s editor creator is not defined.',
230
- null
231
- );
232
- }
233
-
234
- this._elementOrData = elementOrData;
235
-
236
- // Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
237
- // when an error occurs in one of these editors, the watchdog will restart all of them.
238
- this._config = cloneDeepWith( config, value => {
239
- // Leave DOM references.
240
- return isElement( value ) ? value : undefined;
241
- } );
242
-
243
- return Promise.resolve()
244
- .then( () => this._creator( elementOrData, this._config ) )
245
- .then( editor => {
246
- this._editor = editor;
247
-
248
- window.addEventListener( 'error', this._boundErrorHandler );
249
- window.addEventListener( 'unhandledrejection', this._boundErrorHandler );
250
-
251
- this.listenTo( editor.model.document, 'change:data', this._throttledSave );
252
-
253
- this._lastDocumentVersion = editor.model.document.version;
176
+ destroy() {
177
+ this._stopErrorHandling();
254
178
 
255
- this._data = this._getData();
256
- this.state = 'ready';
257
- } );
179
+ this._listeners = {};
258
180
  }
259
181
 
260
182
  /**
261
- * Destroys the current editor instance by using the destructor passed to the {@link #setDestructor `setDestructor()`} method
262
- * and sets state to `destroyed`.
183
+ * Starts listening to a specific event name by registering a callback that will be executed
184
+ * whenever an event with a given name fires.
185
+ *
186
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
263
187
  *
264
- * @returns {Promise}
188
+ * @param {String} eventName The event name.
189
+ * @param {Function} callback A callback which will be added to event listeners.
265
190
  */
266
- destroy() {
267
- this.state = 'destroyed';
191
+ on( eventName, callback ) {
192
+ if ( !this._listeners[ eventName ] ) {
193
+ this._listeners[ eventName ] = [];
194
+ }
268
195
 
269
- return this._destroy();
196
+ this._listeners[ eventName ].push( callback );
270
197
  }
271
198
 
272
199
  /**
273
- * Destroys the current editor instance by using the destructor passed to the {@link #setDestructor `setDestructor()`} method.
200
+ * Stops listening to the specified event name by removing the callback from event listeners.
274
201
  *
275
- * @private
202
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
203
+ *
204
+ * @param {String} eventName The event name.
205
+ * @param {Function} callback A callback which will be removed from event listeners.
276
206
  */
277
- _destroy() {
278
- window.removeEventListener( 'error', this._boundErrorHandler );
279
- window.removeEventListener( 'unhandledrejection', this._boundErrorHandler );
280
-
281
- this.stopListening( this._editor.model.document, 'change:data', this._throttledSave );
282
-
283
- // Save data if there is a remaining editor data change.
284
- this._throttledSave.flush();
285
-
286
- return Promise.resolve()
287
- .then( () => this._destructor( this._editor ) )
288
- .then( () => {
289
- this._editor = null;
290
- } );
207
+ off( eventName, callback ) {
208
+ this._listeners[ eventName ] = this._listeners[ eventName ]
209
+ .filter( cb => cb !== callback );
291
210
  }
292
211
 
293
212
  /**
294
- * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
295
- * the moment of the crash.
213
+ * Fires an event with a given event name and arguments.
296
214
  *
297
- * @private
215
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
216
+ *
217
+ * @protected
218
+ * @param {String} eventName The event name.
219
+ * @param {...*} args Event arguments.
298
220
  */
299
- _save() {
300
- const version = this._editor.model.document.version;
221
+ _fire( eventName, ...args ) {
222
+ const callbacks = this._listeners[ eventName ] || [];
301
223
 
302
- // Change may not produce an operation, so the document's version
303
- // can be the same after that change.
304
- if ( version === this._lastDocumentVersion ) {
305
- return;
306
- }
307
-
308
- try {
309
- this._data = this._getData();
310
- this._lastDocumentVersion = version;
311
- } catch ( err ) {
312
- console.error(
313
- err,
314
- 'An error happened during restoring editor data. ' +
315
- 'Editor will be restored from the previously saved data.'
316
- );
224
+ for ( const callback of callbacks ) {
225
+ callback.apply( this, [ null, ...args ] );
317
226
  }
318
227
  }
319
228
 
320
229
  /**
321
- * Returns the editor data.
230
+ * Starts error handling by attaching global error handlers.
322
231
  *
323
- * @private
324
- * @returns {Object<String,String>}
232
+ * @protected
325
233
  */
326
- _getData() {
327
- const data = {};
328
-
329
- for ( const rootName of this._editor.model.document.getRootNames() ) {
330
- data[ rootName ] = this._editor.data.get( { rootName } );
331
- }
332
-
333
- return data;
234
+ _startErrorHandling() {
235
+ window.addEventListener( 'error', this._boundErrorHandler );
236
+ window.addEventListener( 'unhandledrejection', this._boundErrorHandler );
334
237
  }
335
238
 
336
239
  /**
337
- * Checks if the error comes from the editor that is handled by the watchdog (by checking the error context) and
338
- * restarts the editor. It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only.
240
+ * Stops error handling by detaching global error handlers.
339
241
  *
340
242
  * @protected
243
+ */
244
+ _stopErrorHandling() {
245
+ window.removeEventListener( 'error', this._boundErrorHandler );
246
+ window.removeEventListener( 'unhandledrejection', this._boundErrorHandler );
247
+ }
248
+
249
+ /**
250
+ * Checks if an error comes from the watched item and restarts it.
251
+ * It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only.
252
+ *
253
+ * @private
341
254
  * @fires error
342
255
  * @param {Error} error Error.
343
- * @param {ErrorEvent|PromiseRejectionEvent} evt Error event.
256
+ * @param {ErrorEvent|PromiseRejectionEvent} evt An error event.
344
257
  */
345
258
  _handleError( error, evt ) {
346
259
  // @if CK_DEBUG // if ( error.is && error.is( 'CKEditorError' ) && error.context === undefined ) {
347
- // @if CK_DEBUG // console.warn( 'The error is missing its context and Watchdog cannot restart the proper editor.' );
260
+ // @if CK_DEBUG // console.warn( 'The error is missing its context and Watchdog cannot restart the proper item.' );
348
261
  // @if CK_DEBUG // }
349
262
 
350
263
  if ( this._shouldReactToError( error ) ) {
@@ -359,22 +272,26 @@ export default class Watchdog {
359
272
  date: this._now()
360
273
  } );
361
274
 
362
- this.fire( 'error', { error } );
275
+ const causesRestart = this._shouldRestart();
276
+
363
277
  this.state = 'crashed';
278
+ this._fire( 'stateChange' );
279
+ this._fire( 'error', { error, causesRestart } );
364
280
 
365
- if ( this._shouldRestartEditor() ) {
281
+ if ( causesRestart ) {
366
282
  this._restart();
367
283
  } else {
368
284
  this.state = 'crashedPermanently';
285
+ this._fire( 'stateChange' );
369
286
  }
370
287
  }
371
288
  }
372
289
 
373
290
  /**
374
- * Checks whether the error should be handled.
291
+ * Checks whether an error should be handled by the watchdog.
375
292
  *
376
293
  * @private
377
- * @param {Error} error Error
294
+ * @param {Error} error An error that was caught by the error handling process.
378
295
  */
379
296
  _shouldReactToError( error ) {
380
297
  return (
@@ -382,21 +299,23 @@ export default class Watchdog {
382
299
  error.is( 'CKEditorError' ) &&
383
300
  error.context !== undefined &&
384
301
 
385
- // In some cases the editor should not be restarted - e.g. in case of the editor initialization.
302
+ // In some cases the watched item should not be restarted - e.g. during the item initialization.
386
303
  // That's why the `null` was introduced as a correct error context which does cause restarting.
387
304
  error.context !== null &&
388
305
 
389
306
  // Do not react to errors if the watchdog is in states other than `ready`.
390
307
  this.state === 'ready' &&
391
308
 
392
- this._isErrorComingFromThisEditor( error )
309
+ this._isErrorComingFromThisItem( error )
393
310
  );
394
311
  }
395
312
 
396
313
  /**
397
- * Checks if the editor should be restared or if it should be marked as crashed.
314
+ * Checks if the watchdog should restart the underlying item.
315
+ *
316
+ * @private
398
317
  */
399
- _shouldRestartEditor() {
318
+ _shouldRestart() {
400
319
  if ( this.crashes.length <= this._crashNumberLimit ) {
401
320
  return true;
402
321
  }
@@ -410,95 +329,31 @@ export default class Watchdog {
410
329
  }
411
330
 
412
331
  /**
413
- * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
414
- * the state to `initializing`.
415
- *
416
- * @private
417
- * @fires restart
418
- * @returns {Promise}
419
- */
420
- _restart() {
421
- this.state = 'initializing';
422
-
423
- return Promise.resolve()
424
- .then( () => this._destroy() )
425
- .catch( err => console.error( 'An error happened during the editor destructing.', err ) )
426
- .then( () => {
427
- if ( typeof this._elementOrData === 'string' ) {
428
- return this.create( this._data, this._config );
429
- }
430
-
431
- const updatedConfig = Object.assign( {}, this._config, {
432
- initialData: this._data
433
- } );
434
-
435
- return this.create( this._elementOrData, updatedConfig );
436
- } )
437
- .then( () => {
438
- this.fire( 'restart' );
439
- } );
440
- }
441
-
442
- /**
443
- * Traverses both structures to find out whether the error context is connected
444
- * with the current editor.
445
- *
446
- * @private
447
- * @param {module:utils/ckeditorerror~CKEditorError} error
448
- */
449
- _isErrorComingFromThisEditor( error ) {
450
- return areConnectedThroughProperties( this._editor, error.context );
451
- }
452
-
453
- /**
454
- * A shorthand method for creating an instance of the watchdog. For the full usage, see the
455
- * {@link ~Watchdog `Watchdog` class description}.
456
- *
457
- * Usage:
458
- *
459
- * const watchdog = Watchdog.for( ClassicEditor );
460
- *
461
- * watchdog.create( elementOrData, config );
462
- *
463
- * @param {*} Editor The editor class.
464
- * @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog plugin configuration.
465
- */
466
- static for( Editor, watchdogConfig ) {
467
- const watchdog = new Watchdog( watchdogConfig );
468
-
469
- watchdog.setCreator( ( elementOrData, config ) => Editor.create( elementOrData, config ) );
470
-
471
- return watchdog;
472
- }
473
-
474
- /**
475
- * Fired when a new {@link module:utils/ckeditorerror~CKEditorError `CKEditorError`} error connected to the watchdog editor occurs
332
+ * Fired when a new {@link module:utils/ckeditorerror~CKEditorError `CKEditorError`} error connected to the watchdog instance occurs
476
333
  * and the watchdog will react to it.
477
334
  *
478
- * @event error
479
- */
480
-
481
- /**
482
- * Fired after the watchdog restarts the error in case of a crash.
335
+ * watchdog.on( 'error', ( evt, { error, causesRestart } ) => {
336
+ * console.log( 'An error occurred.' );
337
+ * } );
483
338
  *
484
- * @event restart
339
+ * @event error
485
340
  */
486
341
  }
487
342
 
488
- mix( Watchdog, ObservableMixin );
489
-
490
343
  /**
491
344
  * The watchdog plugin configuration.
492
345
  *
493
346
  * @typedef {Object} WatchdogConfig
494
347
  *
495
- * @property {Number} [crashNumberLimit=3] A threshold specifying the number of editor crashes
496
- * when the watchdog stops restarting the editor in case of errors.
497
- * After this limit is reached and the time between last errors is shorter than `minimumNonErrorTimePeriod`
498
- * the watchdog changes its state to `crashedPermanently` and it stops restarting the editor. This prevents an infinite restart loop.
499
- * @property {Number} [minimumNonErrorTimePeriod=5000] An average amount of milliseconds between last editor errors
500
- * (defaults to 5000). When the period of time between errors is lower than that and the `crashNumberLimit` is also reached
501
- * the watchdog changes its state to `crashedPermanently` and it stops restarting the editor. This prevents an infinite restart loop.
502
- * @property {Number} [saveInterval=5000] A minimum number of milliseconds between saving editor data internally, (defaults to 5000).
503
- * Note that for large documents this might have an impact on the editor performance.
348
+ * @property {Number} [crashNumberLimit=3] A threshold specifying the number of watched item crashes
349
+ * when the watchdog stops restarting the item in case of errors.
350
+ * After this limit is reached and the time between the last errors is shorter than `minimumNonErrorTimePeriod`,
351
+ * the watchdog changes its state to `crashedPermanently` and it stops restarting the item. This prevents an infinite restart loop.
352
+ *
353
+ * @property {Number} [minimumNonErrorTimePeriod=5000] An average number of milliseconds between the last watched item errors
354
+ * (defaults to 5000). When the period of time between errors is lower than that and the `crashNumberLimit` is also reached,
355
+ * the watchdog changes its state to `crashedPermanently` and it stops restarting the item. This prevents an infinite restart loop.
356
+ *
357
+ * @property {Number} [saveInterval=5000] A minimum number of milliseconds between saving the editor data internally (defaults to 5000).
358
+ * Note that for large documents this might impact the editor performance.
504
359
  */