@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 +13 -0
- package/LICENSE.md +1 -1
- package/package.json +6 -6
- package/src/contextwatchdog.js +547 -0
- package/src/editorwatchdog.js +327 -0
- package/src/utils/areconnectedthroughproperties.js +80 -0
- package/src/utils/getsubnodes.js +89 -0
- package/src/watchdog.js +137 -282
|
@@ -0,0 +1,327 @@
|
|
|
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/editorwatchdog
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* globals console */
|
|
11
|
+
|
|
12
|
+
import { throttle, cloneDeepWith, isElement } from 'lodash-es';
|
|
13
|
+
import areConnectedThroughProperties from './utils/areconnectedthroughproperties';
|
|
14
|
+
import Watchdog from './watchdog';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A watchdog for CKEditor 5 editors.
|
|
18
|
+
*
|
|
19
|
+
* See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
|
|
20
|
+
* how to use it.
|
|
21
|
+
*
|
|
22
|
+
* @extends {module:watchdog/watchdog~Watchdog}
|
|
23
|
+
*/
|
|
24
|
+
export default class EditorWatchdog extends Watchdog {
|
|
25
|
+
/**
|
|
26
|
+
* @param {*} Editor The editor class.
|
|
27
|
+
* @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog plugin configuration.
|
|
28
|
+
*/
|
|
29
|
+
constructor( Editor, watchdogConfig = {} ) {
|
|
30
|
+
super( watchdogConfig );
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The current editor instance.
|
|
34
|
+
*
|
|
35
|
+
* @private
|
|
36
|
+
* @type {module:core/editor/editor~Editor}
|
|
37
|
+
*/
|
|
38
|
+
this._editor = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Throttled save method. The `save()` method is called the specified `saveInterval` after `throttledSave()` is called,
|
|
42
|
+
* unless a new action happens in the meantime.
|
|
43
|
+
*
|
|
44
|
+
* @private
|
|
45
|
+
* @type {Function}
|
|
46
|
+
*/
|
|
47
|
+
this._throttledSave = throttle(
|
|
48
|
+
this._save.bind( this ),
|
|
49
|
+
typeof watchdogConfig.saveInterval === 'number' ? watchdogConfig.saveInterval : 5000
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The latest saved editor data represented as a root name -> root data object.
|
|
54
|
+
*
|
|
55
|
+
* @private
|
|
56
|
+
* @member {Object.<String,String>} #_data
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The last document version.
|
|
61
|
+
*
|
|
62
|
+
* @private
|
|
63
|
+
* @member {Number} #_lastDocumentVersion
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The editor source element or data.
|
|
68
|
+
*
|
|
69
|
+
* @private
|
|
70
|
+
* @member {HTMLElement|String|Object.<String|String>} #_elementOrData
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The editor configuration.
|
|
75
|
+
*
|
|
76
|
+
* @private
|
|
77
|
+
* @member {Object|undefined} #_config
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
// Set default creator and destructor functions:
|
|
81
|
+
this._creator = ( ( elementOrData, config ) => Editor.create( elementOrData, config ) );
|
|
82
|
+
this._destructor = editor => editor.destroy();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The current editor instance.
|
|
87
|
+
*
|
|
88
|
+
* @readonly
|
|
89
|
+
* @type {module:core/editor/editor~Editor}
|
|
90
|
+
*/
|
|
91
|
+
get editor() {
|
|
92
|
+
return this._editor;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @inheritDoc
|
|
97
|
+
*/
|
|
98
|
+
get _item() {
|
|
99
|
+
return this._editor;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sets the function that is responsible for the editor creation.
|
|
104
|
+
* It expects a function that should return a promise.
|
|
105
|
+
*
|
|
106
|
+
* watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
|
|
107
|
+
*
|
|
108
|
+
* @method #setCreator
|
|
109
|
+
* @param {Function} creator
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sets the function that is responsible for the editor destruction.
|
|
114
|
+
* Overrides the default destruction function, which destroys only the editor instance.
|
|
115
|
+
* It expects a function that should return a promise or `undefined`.
|
|
116
|
+
*
|
|
117
|
+
* watchdog.setDestructor( editor => {
|
|
118
|
+
* // Do something before the editor is destroyed.
|
|
119
|
+
*
|
|
120
|
+
* return editor
|
|
121
|
+
* .destroy()
|
|
122
|
+
* .then( () => {
|
|
123
|
+
* // Do something after the editor is destroyed.
|
|
124
|
+
* } );
|
|
125
|
+
* } );
|
|
126
|
+
*
|
|
127
|
+
* @method #setDestructor
|
|
128
|
+
* @param {Function} destructor
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
|
|
133
|
+
* the state to `initializing`.
|
|
134
|
+
*
|
|
135
|
+
* @protected
|
|
136
|
+
* @fires restart
|
|
137
|
+
* @returns {Promise}
|
|
138
|
+
*/
|
|
139
|
+
_restart() {
|
|
140
|
+
return Promise.resolve()
|
|
141
|
+
.then( () => {
|
|
142
|
+
this.state = 'initializing';
|
|
143
|
+
this._fire( 'stateChange' );
|
|
144
|
+
|
|
145
|
+
return this._destroy();
|
|
146
|
+
} )
|
|
147
|
+
.catch( err => {
|
|
148
|
+
console.error( 'An error happened during the editor destroying.', err );
|
|
149
|
+
} )
|
|
150
|
+
.then( () => {
|
|
151
|
+
if ( typeof this._elementOrData === 'string' ) {
|
|
152
|
+
return this.create( this._data, this._config, this._config.context );
|
|
153
|
+
} else {
|
|
154
|
+
const updatedConfig = Object.assign( {}, this._config, {
|
|
155
|
+
initialData: this._data
|
|
156
|
+
} );
|
|
157
|
+
|
|
158
|
+
return this.create( this._elementOrData, updatedConfig, updatedConfig.context );
|
|
159
|
+
}
|
|
160
|
+
} )
|
|
161
|
+
.then( () => {
|
|
162
|
+
this._fire( 'restart' );
|
|
163
|
+
} );
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Creates the editor instance and keeps it running, using the defined creator and destructor.
|
|
168
|
+
*
|
|
169
|
+
* @param {HTMLElement|String|Object.<String|String>} [elementOrData] The editor source element or the editor data.
|
|
170
|
+
* @param {module:core/editor/editorconfig~EditorConfig} [config] The editor configuration.
|
|
171
|
+
* @param {Object} [context] A context for the editor.
|
|
172
|
+
*
|
|
173
|
+
* @returns {Promise}
|
|
174
|
+
*/
|
|
175
|
+
create( elementOrData = this._elementOrData, config = this._config, context ) {
|
|
176
|
+
return Promise.resolve()
|
|
177
|
+
.then( () => {
|
|
178
|
+
super._startErrorHandling();
|
|
179
|
+
|
|
180
|
+
this._elementOrData = elementOrData;
|
|
181
|
+
|
|
182
|
+
// Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
|
|
183
|
+
// when an error occurs in one of these editors, the watchdog will restart all of them.
|
|
184
|
+
this._config = this._cloneEditorConfiguration( config ) || {};
|
|
185
|
+
|
|
186
|
+
this._config.context = context;
|
|
187
|
+
|
|
188
|
+
return this._creator( elementOrData, this._config );
|
|
189
|
+
} )
|
|
190
|
+
.then( editor => {
|
|
191
|
+
this._editor = editor;
|
|
192
|
+
|
|
193
|
+
editor.model.document.on( 'change:data', this._throttledSave );
|
|
194
|
+
|
|
195
|
+
this._lastDocumentVersion = editor.model.document.version;
|
|
196
|
+
this._data = this._getData();
|
|
197
|
+
|
|
198
|
+
this.state = 'ready';
|
|
199
|
+
this._fire( 'stateChange' );
|
|
200
|
+
} );
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Destroys the watchdog and the current editor instance. It fires the callback
|
|
205
|
+
* registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
|
|
206
|
+
* It also sets the state to `destroyed`.
|
|
207
|
+
*
|
|
208
|
+
* @returns {Promise}
|
|
209
|
+
*/
|
|
210
|
+
destroy() {
|
|
211
|
+
return Promise.resolve()
|
|
212
|
+
.then( () => {
|
|
213
|
+
this.state = 'destroyed';
|
|
214
|
+
this._fire( 'stateChange' );
|
|
215
|
+
|
|
216
|
+
super.destroy();
|
|
217
|
+
|
|
218
|
+
return this._destroy();
|
|
219
|
+
} );
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @private
|
|
224
|
+
* @returns {Promise}
|
|
225
|
+
*/
|
|
226
|
+
_destroy() {
|
|
227
|
+
return Promise.resolve()
|
|
228
|
+
.then( () => {
|
|
229
|
+
this._stopErrorHandling();
|
|
230
|
+
|
|
231
|
+
// Save data if there is a remaining editor data change.
|
|
232
|
+
this._throttledSave.flush();
|
|
233
|
+
|
|
234
|
+
const editor = this._editor;
|
|
235
|
+
|
|
236
|
+
this._editor = null;
|
|
237
|
+
|
|
238
|
+
return this._destructor( editor );
|
|
239
|
+
} );
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
|
|
244
|
+
* the moment of the crash.
|
|
245
|
+
*
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_save() {
|
|
249
|
+
const version = this._editor.model.document.version;
|
|
250
|
+
|
|
251
|
+
// Operation may not result in a model change, so the document's version can be the same.
|
|
252
|
+
if ( version === this._lastDocumentVersion ) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
this._data = this._getData();
|
|
258
|
+
this._lastDocumentVersion = version;
|
|
259
|
+
} catch ( err ) {
|
|
260
|
+
console.error(
|
|
261
|
+
err,
|
|
262
|
+
'An error happened during restoring editor data. ' +
|
|
263
|
+
'Editor will be restored from the previously saved data.'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @protected
|
|
270
|
+
* @param {Set} props
|
|
271
|
+
*/
|
|
272
|
+
_setExcludedProperties( props ) {
|
|
273
|
+
this._excludedProps = props;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Returns the editor data.
|
|
278
|
+
*
|
|
279
|
+
* @private
|
|
280
|
+
* @returns {Object<String,String>}
|
|
281
|
+
*/
|
|
282
|
+
_getData() {
|
|
283
|
+
const data = {};
|
|
284
|
+
|
|
285
|
+
for ( const rootName of this._editor.model.document.getRootNames() ) {
|
|
286
|
+
data[ rootName ] = this._editor.data.get( { rootName } );
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return data;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Traverses the error context and the current editor to find out whether these structures are connected
|
|
294
|
+
* to each other via properties.
|
|
295
|
+
*
|
|
296
|
+
* @protected
|
|
297
|
+
* @param {module:utils/ckeditorerror~CKEditorError} error
|
|
298
|
+
*/
|
|
299
|
+
_isErrorComingFromThisItem( error ) {
|
|
300
|
+
return areConnectedThroughProperties( this._editor, error.context, this._excludedProps );
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Clones the editor configuration.
|
|
305
|
+
*
|
|
306
|
+
* @private
|
|
307
|
+
* @param {Object} config
|
|
308
|
+
*/
|
|
309
|
+
_cloneEditorConfiguration( config ) {
|
|
310
|
+
return cloneDeepWith( config, ( value, key ) => {
|
|
311
|
+
// Leave DOM references.
|
|
312
|
+
if ( isElement( value ) ) {
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if ( key === 'context' ) {
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
} );
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Fired after the watchdog restarts the error in case of a crash.
|
|
324
|
+
*
|
|
325
|
+
* @event restart
|
|
326
|
+
*/
|
|
327
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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/utils/areconnectedthroughproperties
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* globals console */
|
|
11
|
+
|
|
12
|
+
import getSubNodes from './getsubnodes';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Traverses both structures to find out whether there is a reference that is shared between both structures.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object|Array} target1
|
|
18
|
+
* @param {Object|Array} target2
|
|
19
|
+
*/
|
|
20
|
+
export default function areConnectedThroughProperties( target1, target2, excludedNodes = new Set() ) {
|
|
21
|
+
if ( target1 === target2 && isObject( target1 ) ) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// @if CK_DEBUG_WATCHDOG // return checkConnectionBetweenProps( target1, target2, excludedNodes );
|
|
26
|
+
|
|
27
|
+
const subNodes1 = getSubNodes( target1, excludedNodes );
|
|
28
|
+
const subNodes2 = getSubNodes( target2, excludedNodes );
|
|
29
|
+
|
|
30
|
+
for ( const node of subNodes1 ) {
|
|
31
|
+
if ( subNodes2.has( node ) ) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* istanbul ignore next */
|
|
40
|
+
// eslint-disable-next-line
|
|
41
|
+
function checkConnectionBetweenProps( target1, target2, excludedNodes ) {
|
|
42
|
+
const { subNodes: subNodes1, prevNodeMap: prevNodeMap1 } = getSubNodes( target1, excludedNodes.subNodes );
|
|
43
|
+
const { subNodes: subNodes2, prevNodeMap: prevNodeMap2 } = getSubNodes( target2, excludedNodes.subNodes );
|
|
44
|
+
|
|
45
|
+
for ( const sharedNode of subNodes1 ) {
|
|
46
|
+
if ( subNodes2.has( sharedNode ) ) {
|
|
47
|
+
const connection = [];
|
|
48
|
+
|
|
49
|
+
connection.push( sharedNode );
|
|
50
|
+
|
|
51
|
+
let node = prevNodeMap1.get( sharedNode );
|
|
52
|
+
|
|
53
|
+
while ( node && node !== target1 ) {
|
|
54
|
+
connection.push( node );
|
|
55
|
+
node = prevNodeMap1.get( node );
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
node = prevNodeMap2.get( sharedNode );
|
|
59
|
+
|
|
60
|
+
while ( node && node !== target2 ) {
|
|
61
|
+
connection.unshift( node );
|
|
62
|
+
node = prevNodeMap2.get( node );
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log( '--------' );
|
|
66
|
+
console.log( { target1 } );
|
|
67
|
+
console.log( { sharedNode } );
|
|
68
|
+
console.log( { target2 } );
|
|
69
|
+
console.log( { connection } );
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isObject( structure ) {
|
|
79
|
+
return typeof structure === 'object' && structure !== null;
|
|
80
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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/utils/getsubnodes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* globals EventTarget, Event */
|
|
11
|
+
|
|
12
|
+
export default function getSubNodes( head, excludedProperties = new Set() ) {
|
|
13
|
+
const nodes = [ head ];
|
|
14
|
+
|
|
15
|
+
// @if CK_DEBUG_WATCHDOG // const prevNodeMap = new Map();
|
|
16
|
+
|
|
17
|
+
// Nodes are stored to prevent infinite looping.
|
|
18
|
+
const subNodes = new Set();
|
|
19
|
+
|
|
20
|
+
while ( nodes.length > 0 ) {
|
|
21
|
+
const node = nodes.shift();
|
|
22
|
+
|
|
23
|
+
if ( subNodes.has( node ) || shouldNodeBeSkipped( node ) || excludedProperties.has( node ) ) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
subNodes.add( node );
|
|
28
|
+
|
|
29
|
+
// Handle arrays, maps, sets, custom collections that implements `[ Symbol.iterator ]()`, etc.
|
|
30
|
+
if ( node[ Symbol.iterator ] ) {
|
|
31
|
+
// The custom editor iterators might cause some problems if the editor is crashed.
|
|
32
|
+
try {
|
|
33
|
+
for ( const n of node ) {
|
|
34
|
+
nodes.push( n );
|
|
35
|
+
|
|
36
|
+
// @if CK_DEBUG_WATCHDOG // if ( !prevNodeMap.has( n ) ) {
|
|
37
|
+
// @if CK_DEBUG_WATCHDOG // prevNodeMap.set( n, node );
|
|
38
|
+
// @if CK_DEBUG_WATCHDOG // }
|
|
39
|
+
}
|
|
40
|
+
} catch ( err ) {
|
|
41
|
+
// Do not log errors for broken structures
|
|
42
|
+
// since we are in the error handling process already.
|
|
43
|
+
// eslint-disable-line no-empty
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
for ( const key in node ) {
|
|
47
|
+
// We share a reference via the protobuf library within the editors,
|
|
48
|
+
// hence the shared value should be skipped. Although, it's not a perfect
|
|
49
|
+
// solution since new places like that might occur in the future.
|
|
50
|
+
if ( key === 'defaultValue' ) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
nodes.push( node[ key ] );
|
|
55
|
+
|
|
56
|
+
// @if CK_DEBUG_WATCHDOG // if ( !prevNodeMap.has( node[ key ] ) ) {
|
|
57
|
+
// @if CK_DEBUG_WATCHDOG // prevNodeMap.set( node[ key ], node );
|
|
58
|
+
// @if CK_DEBUG_WATCHDOG // }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// @if CK_DEBUG_WATCHDOG // return { subNodes, prevNodeMap };
|
|
64
|
+
|
|
65
|
+
return subNodes;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function shouldNodeBeSkipped( node ) {
|
|
69
|
+
const type = Object.prototype.toString.call( node );
|
|
70
|
+
const typeOfNode = typeof node;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
typeOfNode === 'number' ||
|
|
74
|
+
typeOfNode === 'boolean' ||
|
|
75
|
+
typeOfNode === 'string' ||
|
|
76
|
+
typeOfNode === 'symbol' ||
|
|
77
|
+
typeOfNode === 'function' ||
|
|
78
|
+
type === '[object Date]' ||
|
|
79
|
+
type === '[object RegExp]' ||
|
|
80
|
+
type === '[object Module]' ||
|
|
81
|
+
|
|
82
|
+
node === undefined ||
|
|
83
|
+
node === null ||
|
|
84
|
+
|
|
85
|
+
// Skip native DOM objects, e.g. Window, nodes, events, etc.
|
|
86
|
+
node instanceof EventTarget ||
|
|
87
|
+
node instanceof Event
|
|
88
|
+
);
|
|
89
|
+
}
|