Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit bacc764

Browse files
author
Piotr Jasiun
authored
Merge pull request #289 from ckeditor/t/ckeditor5-watchdog/1
Other: Added context as second required argument to the `CKEditorError`'s constructor, changed `isCKEditorError()` method to `is()`. Introduced the `areConnectedThroughProperties()` utility Part of the ckeditor/ckeditor5-watchdog#1.
2 parents 591f641 + d182bdb commit bacc764

14 files changed

+633
-243
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, 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 utils/arestructuresconnected
8+
*/
9+
10+
/* globals EventTarget, Event */
11+
12+
/**
13+
* Traverses both structures to find out whether there is a reference that is shared between both structures.
14+
*
15+
* @param {Object|Array} obj1
16+
* @param {Object|Array} obj2
17+
*/
18+
export default function areConnectedThroughProperties( obj1, obj2 ) {
19+
if ( obj1 === obj2 && isObject( obj1 ) ) {
20+
return true;
21+
}
22+
23+
const subNodes1 = getSubNodes( obj1 );
24+
const subNodes2 = getSubNodes( obj2 );
25+
26+
for ( const node of subNodes1 ) {
27+
if ( subNodes2.has( node ) ) {
28+
return true;
29+
}
30+
}
31+
32+
return false;
33+
}
34+
35+
// Traverses JS structure and stores all sub-nodes, including the head node.
36+
// It walks into each iterable structures with the `try catch` block to omit errors that might be thrown during
37+
// tree walking. All primitives, functions and built-ins are skipped.
38+
function getSubNodes( head ) {
39+
const nodes = [ head ];
40+
41+
// Nodes are stored to prevent infinite looping.
42+
const subNodes = new Set();
43+
44+
while ( nodes.length > 0 ) {
45+
const node = nodes.shift();
46+
47+
if ( subNodes.has( node ) || shouldNodeBeSkipped( node ) ) {
48+
continue;
49+
}
50+
51+
subNodes.add( node );
52+
53+
// Handle arrays, maps, sets, custom collections that implements `[ Symbol.iterator ]()`, etc.
54+
if ( node[ Symbol.iterator ] ) {
55+
// The custom editor iterators might cause some problems if the editor is crashed.
56+
try {
57+
nodes.push( ...node );
58+
} catch ( err ) {
59+
// eslint-disable-line no-empty
60+
}
61+
} else {
62+
nodes.push( ...Object.values( node ) );
63+
}
64+
}
65+
66+
return subNodes;
67+
}
68+
69+
function shouldNodeBeSkipped( node ) {
70+
const type = Object.prototype.toString.call( node );
71+
72+
return (
73+
type === '[object Number]' ||
74+
type === '[object Boolean]' ||
75+
type === '[object String]' ||
76+
type === '[object Symbol]' ||
77+
type === '[object Function]' ||
78+
type === '[object Date]' ||
79+
type === '[object RegExp]' ||
80+
81+
node === undefined ||
82+
node === null ||
83+
84+
// Skip native DOM objects, e.g. Window, nodes, events, etc.
85+
node instanceof EventTarget ||
86+
node instanceof Event
87+
);
88+
}
89+
90+
function isObject( structure ) {
91+
return typeof structure === 'object' && structure !== null;
92+
}

src/ckeditorerror.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,16 @@ export default class CKEditorError extends Error {
3232
* @param {String} message The error message in an `error-name: Error message.` format.
3333
* During the minification process the "Error message" part will be removed to limit the code size
3434
* and a link to this error documentation will be added to the `message`.
35+
* @param {Object|null} context A context of the error by which the {@link module:watchdog/watchdog~Watchdog watchdog}
36+
* is able to determine which editor crashed. It should be an editor instance or a property connected to it. It can be also
37+
* a `null` value if the editor should not be restarted in case of the error (e.g. during the editor initialization).
38+
* The error context should be checked using the `areConnectedThroughProperties( editor, context )` utility
39+
* to check if the object works as the context.
3540
* @param {Object} [data] Additional data describing the error. A stringified version of this object
3641
* will be appended to the error message, so the data are quickly visible in the console. The original
3742
* data object will also be later available under the {@link #data} property.
3843
*/
39-
constructor( message, data ) {
44+
constructor( message, context, data ) {
4045
message = attachLinkToDocumentation( message );
4146

4247
if ( data ) {
@@ -46,26 +51,30 @@ export default class CKEditorError extends Error {
4651
super( message );
4752

4853
/**
49-
* @member {String}
54+
* @type {String}
5055
*/
5156
this.name = 'CKEditorError';
5257

58+
/**
59+
* A context of the error by which the Watchdog is able to determine which editor crashed.
60+
*
61+
* @type {Object|null}
62+
*/
63+
this.context = context;
64+
5365
/**
5466
* The additional error data passed to the constructor. Undefined if none was passed.
5567
*
56-
* @member {Object|undefined}
68+
* @type {Object|undefined}
5769
*/
5870
this.data = data;
5971
}
6072

6173
/**
62-
* Checks if error is an instance of CKEditorError class.
63-
*
64-
* @param {Object} error Object to check.
65-
* @returns {Boolean}
74+
* Checks if the error is of the `CKEditorError` type.
6675
*/
67-
static isCKEditorError( error ) {
68-
return error instanceof CKEditorError;
76+
is( type ) {
77+
return type === 'CKEditorError';
6978
}
7079
}
7180

src/collection.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export default class Collection {
148148
*
149149
* @error collection-add-invalid-id
150150
*/
151-
throw new CKEditorError( 'collection-add-invalid-id' );
151+
throw new CKEditorError( 'collection-add-invalid-id', this );
152152
}
153153

154154
if ( this.get( itemId ) ) {
@@ -157,7 +157,7 @@ export default class Collection {
157157
*
158158
* @error collection-add-item-already-exists
159159
*/
160-
throw new CKEditorError( 'collection-add-item-already-exists' );
160+
throw new CKEditorError( 'collection-add-item-already-exists', this );
161161
}
162162
} else {
163163
item[ idProperty ] = itemId = uid();
@@ -172,7 +172,7 @@ export default class Collection {
172172
*
173173
* @error collection-add-item-bad-index
174174
*/
175-
throw new CKEditorError( 'collection-add-item-invalid-index' );
175+
throw new CKEditorError( 'collection-add-item-invalid-index', this );
176176
}
177177

178178
this._items.splice( index, 0, item );
@@ -203,7 +203,7 @@ export default class Collection {
203203
*
204204
* @error collection-get-invalid-arg
205205
*/
206-
throw new CKEditorError( 'collection-get-invalid-arg: Index or id must be given.' );
206+
throw new CKEditorError( 'collection-get-invalid-arg: Index or id must be given.', this );
207207
}
208208

209209
return item || null;
@@ -286,7 +286,7 @@ export default class Collection {
286286
*
287287
* @error collection-remove-404
288288
*/
289-
throw new CKEditorError( 'collection-remove-404: Item not found.' );
289+
throw new CKEditorError( 'collection-remove-404: Item not found.', this );
290290
}
291291

292292
this._items.splice( index, 1 );
@@ -459,7 +459,7 @@ export default class Collection {
459459
*
460460
* @error collection-bind-to-rebind
461461
*/
462-
throw new CKEditorError( 'collection-bind-to-rebind: The collection cannot be bound more than once.' );
462+
throw new CKEditorError( 'collection-bind-to-rebind: The collection cannot be bound more than once.', this );
463463
}
464464

465465
this._bindToCollection = externalCollection;

src/focustracker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default class FocusTracker {
7575
*/
7676
add( element ) {
7777
if ( this._elements.has( element ) ) {
78-
throw new CKEditorError( 'focusTracker-add-element-already-exist' );
78+
throw new CKEditorError( 'focusTracker-add-element-already-exist', this );
7979
}
8080

8181
this.listenTo( element, 'focus', () => this._focus( element ), { useCapture: true } );

src/keyboard.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ export function getCode( key ) {
6060
* @errror keyboard-unknown-key
6161
* @param {String} key
6262
*/
63-
throw new CKEditorError( 'keyboard-unknown-key: Unknown key name.', { key } );
63+
throw new CKEditorError(
64+
'keyboard-unknown-key: Unknown key name.',
65+
null, { key }
66+
);
6467
}
6568
} else {
6669
keyCode = key.keyCode +

src/observablemixin.js

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const ObservableMixin = {
6262
*
6363
* @error observable-set-cannot-override
6464
*/
65-
throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.' );
65+
throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.', this );
6666
}
6767

6868
Object.defineProperty( this, name, {
@@ -107,7 +107,7 @@ const ObservableMixin = {
107107
*
108108
* @error observable-bind-wrong-properties
109109
*/
110-
throw new CKEditorError( 'observable-bind-wrong-properties: All properties must be strings.' );
110+
throw new CKEditorError( 'observable-bind-wrong-properties: All properties must be strings.', this );
111111
}
112112

113113
if ( ( new Set( bindProperties ) ).size !== bindProperties.length ) {
@@ -116,7 +116,7 @@ const ObservableMixin = {
116116
*
117117
* @error observable-bind-duplicate-properties
118118
*/
119-
throw new CKEditorError( 'observable-bind-duplicate-properties: Properties must be unique.' );
119+
throw new CKEditorError( 'observable-bind-duplicate-properties: Properties must be unique.', this );
120120
}
121121

122122
initObservable( this );
@@ -130,7 +130,7 @@ const ObservableMixin = {
130130
*
131131
* @error observable-bind-rebind
132132
*/
133-
throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same property more that once.' );
133+
throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same property more that once.', this );
134134
}
135135
} );
136136

@@ -186,7 +186,7 @@ const ObservableMixin = {
186186
*
187187
* @error observable-unbind-wrong-properties
188188
*/
189-
throw new CKEditorError( 'observable-unbind-wrong-properties: Properties must be strings.' );
189+
throw new CKEditorError( 'observable-unbind-wrong-properties: Properties must be strings.', this );
190190
}
191191

192192
unbindProperties.forEach( propertyName => {
@@ -246,6 +246,7 @@ const ObservableMixin = {
246246
*/
247247
throw new CKEditorError(
248248
'observablemixin-cannot-decorate-undefined: Cannot decorate an undefined method.',
249+
this,
249250
{ object: this, methodName }
250251
);
251252
}
@@ -380,7 +381,10 @@ function bindTo( ...args ) {
380381
*
381382
* @error observable-bind-no-callback
382383
*/
383-
throw new CKEditorError( 'observable-bind-to-no-callback: Binding multiple observables only possible with callback.' );
384+
throw new CKEditorError(
385+
'observable-bind-to-no-callback: Binding multiple observables only possible with callback.',
386+
this
387+
);
384388
}
385389

386390
// Eliminate A.bind( 'x', 'y' ).to( B, callback )
@@ -390,7 +394,10 @@ function bindTo( ...args ) {
390394
*
391395
* @error observable-bind-to-extra-callback
392396
*/
393-
throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple properties and use a callback in one binding.' );
397+
throw new CKEditorError(
398+
'observable-bind-to-extra-callback: Cannot bind multiple properties and use a callback in one binding.',
399+
this
400+
);
394401
}
395402

396403
parsedArgs.to.forEach( to => {
@@ -401,7 +408,7 @@ function bindTo( ...args ) {
401408
*
402409
* @error observable-bind-to-properties-length
403410
*/
404-
throw new CKEditorError( 'observable-bind-to-properties-length: The number of properties must match.' );
411+
throw new CKEditorError( 'observable-bind-to-properties-length: The number of properties must match.', this );
405412
}
406413

407414
// When no to.properties specified, observing source properties instead i.e.
@@ -442,7 +449,7 @@ function bindToMany( observables, attribute, callback ) {
442449
*
443450
* @error observable-bind-to-many-not-one-binding
444451
*/
445-
throw new CKEditorError( 'observable-bind-to-many-not-one-binding: Cannot bind multiple properties with toMany().' );
452+
throw new CKEditorError( 'observable-bind-to-many-not-one-binding: Cannot bind multiple properties with toMany().', this );
446453
}
447454

448455
this.to(
@@ -501,7 +508,7 @@ function parseBindToArgs( ...args ) {
501508
*
502509
* @error observable-bind-to-parse-error
503510
*/
504-
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
511+
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.', null );
505512
}
506513

507514
const parsed = { to: [] };
@@ -518,7 +525,7 @@ function parseBindToArgs( ...args ) {
518525
lastObservable = { observable: a, properties: [] };
519526
parsed.to.push( lastObservable );
520527
} else {
521-
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
528+
throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.', null );
522529
}
523530
} );
524531

0 commit comments

Comments
 (0)