@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/CHANGELOG.md CHANGED
@@ -1,6 +1,19 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## [17.0.0](https://github.com/ckeditor/ckeditor5-watchdog/compare/v16.0.0...v17.0.0) (2020-02-19)
5
+
6
+ ### MAJOR BREAKING CHANGES
7
+
8
+ * The `Watchdog` class was renamed to `EditorWatchdog` and is available in `src/editorwatchdog.js`.
9
+ * The `EditorWatchdog.for()` method was removed in favor of the constructor.
10
+ * The `EditorWatchdog#constructor()` API changed. Now the `EditorWatchdog` constructor accepts the editor class as the first argument and the watchdog configuration as the second argument. The `EditorWatchdog` editor creator now defaults to `( sourceElementOrData, config ) => Editor.create( sourceElementOrData, config )`.
11
+
12
+ ### Features
13
+
14
+ * Introduced `ContextWatchdog` which is a watchdog for `Context`. Closes [ckeditor/ckeditor5#6079](https://github.com/ckeditor/ckeditor5/issues/6079). Closes [ckeditor/ckeditor5#6042](https://github.com/ckeditor/ckeditor5/issues/6042). Closes [ckeditor/ckeditor5#4696](https://github.com/ckeditor/ckeditor5/issues/4696). ([76c4938](https://github.com/ckeditor/ckeditor5-watchdog/commit/76c4938))
15
+
16
+
4
17
  ## [16.0.0](https://github.com/ckeditor/ckeditor5-watchdog/compare/v15.0.0...v16.0.0) (2019-12-04)
5
18
 
6
19
  ### Bug fixes
package/LICENSE.md CHANGED
@@ -2,7 +2,7 @@ Software License Agreement
2
2
  ==========================
3
3
 
4
4
  **CKEditor 5 watchdog feature** – https://github.com/ckeditor/ckeditor5-watchdog <br>
5
- Copyright (c) 2003-2019, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
5
+ Copyright (c) 2003-2020, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
6
6
 
7
7
  Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-watchdog",
3
- "version": "16.0.0",
3
+ "version": "17.0.0",
4
4
  "description": "A watchdog feature for CKEditor 5 editors. It keeps a CKEditor 5 editor instance running.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -9,14 +9,14 @@
9
9
  "ckeditor5-lib"
10
10
  ],
11
11
  "dependencies": {
12
- "@ckeditor/ckeditor5-utils": "^16.0.0",
13
12
  "lodash-es": "^4.17.10"
14
13
  },
15
14
  "devDependencies": {
16
- "@ckeditor/ckeditor5-core": "^16.0.0",
17
- "@ckeditor/ckeditor5-editor-classic": "^16.0.0",
18
- "@ckeditor/ckeditor5-engine": "^16.0.0",
19
- "@ckeditor/ckeditor5-paragraph": "^16.0.0",
15
+ "@ckeditor/ckeditor5-core": "^17.0.0",
16
+ "@ckeditor/ckeditor5-editor-classic": "^17.0.0",
17
+ "@ckeditor/ckeditor5-engine": "^17.0.0",
18
+ "@ckeditor/ckeditor5-paragraph": "^17.0.0",
19
+ "@ckeditor/ckeditor5-utils": "^17.0.0",
20
20
  "eslint": "^5.5.0",
21
21
  "eslint-config-ckeditor5": "^2.0.0",
22
22
  "husky": "^2.4.1",
@@ -0,0 +1,547 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /**
7
+ * @module watchdog/contextwatchdog
8
+ */
9
+
10
+ /* globals console */
11
+
12
+ import Watchdog from './watchdog';
13
+ import EditorWatchdog from './editorwatchdog';
14
+ import areConnectedThroughProperties from './utils/areconnectedthroughproperties';
15
+ import getSubNodes from './utils/getsubnodes';
16
+
17
+ /**
18
+ * A watchdog for the {@link module:core/context~Context} class.
19
+ *
20
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
21
+ * how to use it.
22
+ *
23
+ * @extends {module:watchdog/watchdog~Watchdog}
24
+ */
25
+ export default class ContextWatchdog extends Watchdog {
26
+ /**
27
+ * The context watchdog class constructor.
28
+ *
29
+ * const watchdog = new ContextWatchdog( Context );
30
+ *
31
+ * await watchdog.create( contextConfiguration );
32
+ *
33
+ * await watchdog.add( item );
34
+ *
35
+ * See the {@glink features/watchdog Watchdog feature guide} to learn more how to use this feature.
36
+ *
37
+ * @param {Function} Context The {@link module:core/context~Context} class.
38
+ * @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog configuration.
39
+ */
40
+ constructor( Context, watchdogConfig = {} ) {
41
+ super( watchdogConfig );
42
+
43
+ /**
44
+ * A map of internal watchdogs for added items.
45
+ *
46
+ * @protected
47
+ * @type {Map.<string,module:watchdog/watchdog~EditorWatchdog>}
48
+ */
49
+ this._watchdogs = new Map();
50
+
51
+ /**
52
+ * The watchdog configuration.
53
+ *
54
+ * @private
55
+ * @type {module:watchdog/watchdog~WatchdogConfig}
56
+ */
57
+ this._watchdogConfig = watchdogConfig;
58
+
59
+ /**
60
+ * The current context instance.
61
+ *
62
+ * @private
63
+ * @type {module:core/context~Context|null}
64
+ */
65
+ this._context = null;
66
+
67
+ /**
68
+ * Context properties (nodes/references) that are gathered during the initial context creation
69
+ * and are used to distinguish the origin of an error.
70
+ *
71
+ * @private
72
+ * @type {Set.<*>}
73
+ */
74
+ this._contextProps = new Set();
75
+
76
+ /**
77
+ * An action queue, which is used to handle async functions queuing.
78
+ *
79
+ * @private
80
+ * @type {ActionQueue}
81
+ */
82
+ this._actionQueue = new ActionQueue();
83
+
84
+ /**
85
+ * The configuration for the {@link module:core/context~Context}.
86
+ *
87
+ * @private
88
+ * @member {Object} #_contextConfig
89
+ */
90
+
91
+ /**
92
+ * The context configuration.
93
+ *
94
+ * @private
95
+ * @member {Object|undefined} #_config
96
+ */
97
+
98
+ // Default creator and destructor.
99
+ this._creator = contextConfig => Context.create( contextConfig );
100
+ this._destructor = context => context.destroy();
101
+
102
+ this._actionQueue.onEmpty( () => {
103
+ if ( this.state === 'initializing' ) {
104
+ this.state = 'ready';
105
+ this._fire( 'stateChange' );
106
+ }
107
+ } );
108
+
109
+ /**
110
+ * Sets the function that is responsible for the context creation.
111
+ * It expects a function that should return a promise (or `undefined`).
112
+ *
113
+ * watchdog.setCreator( config => Context.create( config ) );
114
+ *
115
+ * @method #setCreator
116
+ * @param {Function} creator
117
+ */
118
+
119
+ /**
120
+ * Sets the function that is responsible for the context destruction.
121
+ * Overrides the default destruction function, which destroys only the context instance.
122
+ * It expects a function that should return a promise (or `undefined`).
123
+ *
124
+ * watchdog.setDestructor( context => {
125
+ * // Do something before the context is destroyed.
126
+ *
127
+ * return context
128
+ * .destroy()
129
+ * .then( () => {
130
+ * // Do something after the context is destroyed.
131
+ * } );
132
+ * } );
133
+ *
134
+ * @method #setDestructor
135
+ * @param {Function} destructor
136
+ */
137
+ }
138
+
139
+ /**
140
+ * The context instance. Keep in mind that this property might be changed when the context watchdog restarts,
141
+ * so do not keep this instance internally. Always operate on the `ContextWatchdog#context` property.
142
+ *
143
+ * @type {module:core/context~Context|null}
144
+ */
145
+ get context() {
146
+ return this._context;
147
+ }
148
+
149
+ /**
150
+ * Initializes the context watchdog. Once it is created, the watchdog takes care about
151
+ * recreating the context and the provided items, and starts the error handling mechanism.
152
+ *
153
+ * await watchdog.create( {
154
+ * plugins: []
155
+ * } );
156
+ *
157
+ * @param {Object} [contextConfig] The context configuration. See {@link module:core/context~Context}.
158
+ * @returns {Promise}
159
+ */
160
+ create( contextConfig = {} ) {
161
+ return this._actionQueue.enqueue( () => {
162
+ this._contextConfig = contextConfig;
163
+
164
+ return this._create();
165
+ } );
166
+ }
167
+
168
+ /**
169
+ * Returns an item instance with the given `itemId`.
170
+ *
171
+ * const editor1 = watchdog.getItem( 'editor1' );
172
+ *
173
+ * @param {String} itemId The item ID.
174
+ * @returns {*} The item instance or `undefined` if an item with a given ID has not been found.
175
+ */
176
+ getItem( itemId ) {
177
+ const watchdog = this._getWatchdog( itemId );
178
+
179
+ return watchdog._item;
180
+ }
181
+
182
+ /**
183
+ * Gets the state of the given item. See {@link #state} for a list of available states.
184
+ *
185
+ * const editor1State = watchdog.getItemState( 'editor1' );
186
+ *
187
+ * @param {String} itemId Item ID.
188
+ * @returns {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} The state of the item.
189
+ */
190
+ getItemState( itemId ) {
191
+ const watchdog = this._getWatchdog( itemId );
192
+
193
+ return watchdog.state;
194
+ }
195
+
196
+ /**
197
+ * Adds items to the watchdog. Once created, instances of these items will be available using the {@link #getItem} method.
198
+ *
199
+ * Items can be passed together as an array of objects:
200
+ *
201
+ * await watchdog.add( [ {
202
+ * id: 'editor1',
203
+ * type: 'editor',
204
+ * sourceElementOrData: document.querySelector( '#editor' ),
205
+ * config: {
206
+ * plugins: [ Essentials, Paragraph, Bold, Italic ],
207
+ * toolbar: [ 'bold', 'italic', 'alignment' ]
208
+ * },
209
+ * creator: ( element, config ) => ClassicEditor.create( element, config )
210
+ * } ] );
211
+ *
212
+ * Or one by one as objects:
213
+ *
214
+ * await watchdog.add( {
215
+ * id: 'editor1',
216
+ * type: 'editor',
217
+ * sourceElementOrData: document.querySelector( '#editor' ),
218
+ * config: {
219
+ * plugins: [ Essentials, Paragraph, Bold, Italic ],
220
+ * toolbar: [ 'bold', 'italic', 'alignment' ]
221
+ * },
222
+ * creator: ( element, config ) => ClassicEditor.create( element, config )
223
+ * ] );
224
+ *
225
+ * Then an instance can be retrieved using the {@link #getItem} method:
226
+ *
227
+ * const editor1 = watchdog.getItem( 'editor1' );
228
+ *
229
+ * Note that this method can be called multiple times, but for performance reasons it is better
230
+ * to pass all items together.
231
+ *
232
+ * @param {module:watchdog/contextwatchdog~WatchdogItemConfiguration|Array.<module:watchdog/contextwatchdog~WatchdogItemConfiguration>}
233
+ * itemConfigurationOrItemConfigurations An item configuration object or an array of item configurations.
234
+ * @returns {Promise}
235
+ */
236
+ add( itemConfigurationOrItemConfigurations ) {
237
+ const itemConfigurations = Array.isArray( itemConfigurationOrItemConfigurations ) ?
238
+ itemConfigurationOrItemConfigurations :
239
+ [ itemConfigurationOrItemConfigurations ];
240
+
241
+ return this._actionQueue.enqueue( () => {
242
+ if ( this.state === 'destroyed' ) {
243
+ throw new Error( 'Cannot add items to destroyed watchdog.' );
244
+ }
245
+
246
+ if ( !this._context ) {
247
+ throw new Error( 'Context was not created yet. You should call the `ContextWatchdog#create()` method first.' );
248
+ }
249
+
250
+ // Create new watchdogs.
251
+ return Promise.all( itemConfigurations.map( item => {
252
+ let watchdog;
253
+
254
+ if ( this._watchdogs.has( item.id ) ) {
255
+ throw new Error( `Item with the given id is already added: '${ item.id }'.` );
256
+ }
257
+
258
+ if ( item.type === 'editor' ) {
259
+ watchdog = new EditorWatchdog( this._watchdogConfig );
260
+ watchdog.setCreator( item.creator );
261
+ watchdog._setExcludedProperties( this._contextProps );
262
+
263
+ if ( item.destructor ) {
264
+ watchdog.setDestructor( item.destructor );
265
+ }
266
+
267
+ this._watchdogs.set( item.id, watchdog );
268
+
269
+ // Enqueue the internal watchdog errors within the main queue.
270
+ // And propagate the internal `error` events as `itemError` event.
271
+ watchdog.on( 'error', ( evt, { error, causesRestart } ) => {
272
+ this._fire( 'itemError', { itemId: item.id, error } );
273
+
274
+ // Do not enqueue the item restart action if the item will not restart.
275
+ if ( !causesRestart ) {
276
+ return;
277
+ }
278
+
279
+ this._actionQueue.enqueue( () => new Promise( res => {
280
+ watchdog.on( 'restart', rethrowRestartEventOnce.bind( this ) );
281
+
282
+ function rethrowRestartEventOnce() {
283
+ watchdog.off( 'restart', rethrowRestartEventOnce );
284
+
285
+ this._fire( 'itemRestart', { itemId: item.id } );
286
+
287
+ res();
288
+ }
289
+ } ) );
290
+ } );
291
+
292
+ return watchdog.create( item.sourceElementOrData, item.config, this._context );
293
+ } else {
294
+ throw new Error( `Not supported item type: '${ item.type }'.` );
295
+ }
296
+ } ) );
297
+ } );
298
+ }
299
+
300
+ /**
301
+ * Removes and destroys item(s) with given ID(s).
302
+ *
303
+ * await watchdog.remove( 'editor1' );
304
+ *
305
+ * Or
306
+ *
307
+ * await watchdog.remove( [ 'editor1', 'editor2' ] );
308
+ *
309
+ * @param {Array.<String>|String} itemIdOrItemIds Item ID or an array of item IDs.
310
+ * @returns {Promise}
311
+ */
312
+ remove( itemIdOrItemIds ) {
313
+ const itemIds = Array.isArray( itemIdOrItemIds ) ?
314
+ itemIdOrItemIds :
315
+ [ itemIdOrItemIds ];
316
+
317
+ return this._actionQueue.enqueue( () => {
318
+ return Promise.all( itemIds.map( itemId => {
319
+ const watchdog = this._getWatchdog( itemId );
320
+
321
+ this._watchdogs.delete( itemId );
322
+
323
+ return watchdog.destroy();
324
+ } ) );
325
+ } );
326
+ }
327
+
328
+ /**
329
+ * Destroys the context watchdog and all added items.
330
+ * Once the context watchdog is destroyed, new items cannot be added.
331
+ *
332
+ * await watchdog.destroy();
333
+ *
334
+ * @returns {Promise}
335
+ */
336
+ destroy() {
337
+ return this._actionQueue.enqueue( () => {
338
+ this.state = 'destroyed';
339
+ this._fire( 'stateChange' );
340
+
341
+ super.destroy();
342
+
343
+ return this._destroy();
344
+ } );
345
+ }
346
+
347
+ /**
348
+ * Restarts the context watchdog.
349
+ *
350
+ * @protected
351
+ * @returns {Promise}
352
+ */
353
+ _restart() {
354
+ return this._actionQueue.enqueue( () => {
355
+ this.state = 'initializing';
356
+ this._fire( 'stateChange' );
357
+
358
+ return this._destroy()
359
+ .catch( err => {
360
+ console.error( 'An error happened during destroying the context or items.', err );
361
+ } )
362
+ .then( () => this._create() )
363
+ .then( () => this._fire( 'restart' ) );
364
+ } );
365
+ }
366
+
367
+ /**
368
+ * @private
369
+ * @returns {Promise}
370
+ */
371
+ _create() {
372
+ return Promise.resolve()
373
+ .then( () => {
374
+ this._startErrorHandling();
375
+
376
+ return this._creator( this._contextConfig );
377
+ } )
378
+ .then( context => {
379
+ this._context = context;
380
+ this._contextProps = getSubNodes( this._context );
381
+
382
+ return Promise.all(
383
+ Array.from( this._watchdogs.values() )
384
+ .map( watchdog => {
385
+ watchdog._setExcludedProperties( this._contextProps );
386
+
387
+ return watchdog.create( undefined, undefined, this._context );
388
+ } )
389
+ );
390
+ } );
391
+ }
392
+
393
+ /**
394
+ * Destroys the context instance and all added items.
395
+ *
396
+ * @private
397
+ * @returns {Promise}
398
+ */
399
+ _destroy() {
400
+ return Promise.resolve()
401
+ .then( () => {
402
+ this._stopErrorHandling();
403
+
404
+ const context = this._context;
405
+
406
+ this._context = null;
407
+ this._contextProps = new Set();
408
+
409
+ return Promise.all(
410
+ Array.from( this._watchdogs.values() )
411
+ .map( watchdog => watchdog.destroy() )
412
+ )
413
+ // Context destructor destroys each editor.
414
+ .then( () => this._destructor( context ) );
415
+ } );
416
+ }
417
+
418
+ /**
419
+ * Returns the watchdog for a given item ID.
420
+ *
421
+ * @protected
422
+ * @param {String} itemId Item ID.
423
+ * @returns {module:watchdog/watchdog~Watchdog} Watchdog
424
+ */
425
+ _getWatchdog( itemId ) {
426
+ const watchdog = this._watchdogs.get( itemId );
427
+
428
+ if ( !watchdog ) {
429
+ throw new Error( `Item with the given id was not registered: ${ itemId }.` );
430
+ }
431
+
432
+ return watchdog;
433
+ }
434
+
435
+ /**
436
+ * Checks whether an error comes from the context instance and not from the item instances.
437
+ *
438
+ * @protected
439
+ * @param {Error} error
440
+ * @returns {Boolean}
441
+ */
442
+ _isErrorComingFromThisItem( error ) {
443
+ for ( const watchdog of this._watchdogs.values() ) {
444
+ if ( watchdog._isErrorComingFromThisItem( error ) ) {
445
+ return false;
446
+ }
447
+ }
448
+
449
+ return areConnectedThroughProperties( this._context, error.context );
450
+ }
451
+
452
+ /**
453
+ * Fired after the watchdog restarts the context and the added items because of a crash.
454
+ *
455
+ * watchdog.on( 'restart', () => {
456
+ * console.log( 'The context has been restarted.' );
457
+ * } );
458
+ *
459
+ * @event restart
460
+ */
461
+
462
+ /**
463
+ * Fired when a new error occurred in one of the added items.
464
+ *
465
+ * watchdog.on( 'itemError', ( evt, { error, itemId, causesRestart } ) => {
466
+ * console.log( `An error occurred in an item with the '${ itemId }' ID.` );
467
+ * } );
468
+ *
469
+ * @event itemError
470
+ */
471
+
472
+ /**
473
+ * Fired after an item has been restarted.
474
+ *
475
+ * watchdog.on( 'itemRestart', ( evt, { itemId } ) => {
476
+ * console.log( 'An item with with the '${ itemId }' ID has been restarted.' );
477
+ * } );
478
+ *
479
+ * @event itemRestart
480
+ */
481
+ }
482
+
483
+ // An action queue that allows queuing async functions.
484
+ class ActionQueue {
485
+ constructor() {
486
+ // @type {Promise}
487
+ this._promiseQueue = Promise.resolve();
488
+
489
+ // @type {Array.<Function>}
490
+ this._onEmptyCallbacks = [];
491
+ }
492
+
493
+ // Used to register callbacks that will be run when the queue becomes empty.
494
+ //
495
+ // @param {Function} onEmptyCallback A callback that will be run whenever the queue becomes empty.
496
+ onEmpty( onEmptyCallback ) {
497
+ this._onEmptyCallbacks.push( onEmptyCallback );
498
+ }
499
+
500
+ // It adds asynchronous actions (functions) to the queue and runs them one by one.
501
+ //
502
+ // @param {Function} action A function that should be enqueued.
503
+ // @returns {Promise}
504
+ enqueue( action ) {
505
+ let nonErrorQueue;
506
+
507
+ const queueWithAction = this._promiseQueue
508
+ .then( action )
509
+ .then( () => {
510
+ if ( this._promiseQueue === nonErrorQueue ) {
511
+ this._onEmptyCallbacks.forEach( cb => cb() );
512
+ }
513
+ } );
514
+
515
+ // Catch all errors in the main queue to stack promises even if an error occurred in the past.
516
+ nonErrorQueue = this._promiseQueue = queueWithAction.catch( () => { } );
517
+
518
+ return queueWithAction;
519
+ }
520
+ }
521
+
522
+ /**
523
+ * The watchdog item configuration interface.
524
+ *
525
+ * @typedef {module:watchdog/contextwatchdog~EditorWatchdogConfiguration} module:watchdog/contextwatchdog~WatchdogItemConfiguration
526
+ */
527
+
528
+ /**
529
+ * The editor watchdog configuration interface specifies how editors should be created and destroyed.
530
+ *
531
+ * @typedef {Object} module:watchdog/contextwatchdog~EditorWatchdogConfiguration
532
+ *
533
+ * @property {String} id A unique item identificator.
534
+ *
535
+ * @property {'editor'} type The type of the item to create. At the moment, only `'editor'` is supported.
536
+ *
537
+ * @property {Function} creator A function that initializes the item (the editor). The function takes editor initialization arguments
538
+ * and should return a promise. For example: `( el, config ) => ClassicEditor.create( el, config )`.
539
+ *
540
+ * @property {Function} [destructor] A function that destroys the item instance (the editor). The function
541
+ * takes an item and should return a promise. For example: `editor => editor.destroy()`
542
+ *
543
+ * @property {String|HTMLElement} sourceElementOrData The source element or data that will be passed
544
+ * as the first argument to the `Editor.create()` method.
545
+ *
546
+ * @property {Object} config An editor configuration.
547
+ */