@ckeditor/ckeditor5-watchdog 0.0.0-internal-20241017.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.
@@ -0,0 +1,410 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import Watchdog from './watchdog.js';
6
+ import EditorWatchdog from './editorwatchdog.js';
7
+ import areConnectedThroughProperties from './utils/areconnectedthroughproperties.js';
8
+ import getSubNodes from './utils/getsubnodes.js';
9
+ const mainQueueId = Symbol('MainQueueId');
10
+ /**
11
+ * A watchdog for the {@link module:core/context~Context} class.
12
+ *
13
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
14
+ * how to use it.
15
+ */
16
+ export default class ContextWatchdog extends Watchdog {
17
+ /**
18
+ * The context watchdog class constructor.
19
+ *
20
+ * ```ts
21
+ * const watchdog = new ContextWatchdog( Context );
22
+ *
23
+ * await watchdog.create( contextConfiguration );
24
+ *
25
+ * await watchdog.add( item );
26
+ * ```
27
+ *
28
+ * See the {@glink features/watchdog Watchdog feature guide} to learn more how to use this feature.
29
+ *
30
+ * @param Context The {@link module:core/context~Context} class.
31
+ * @param watchdogConfig The watchdog configuration.
32
+ */
33
+ constructor(Context, watchdogConfig = {}) {
34
+ super(watchdogConfig);
35
+ /**
36
+ * A map of internal watchdogs for added items.
37
+ */
38
+ this._watchdogs = new Map();
39
+ /**
40
+ * The current context instance.
41
+ */
42
+ this._context = null;
43
+ /**
44
+ * Context properties (nodes/references) that are gathered during the initial context creation
45
+ * and are used to distinguish the origin of an error.
46
+ */
47
+ this._contextProps = new Set();
48
+ /**
49
+ * An action queue, which is used to handle async functions queuing.
50
+ */
51
+ this._actionQueues = new ActionQueues();
52
+ this._watchdogConfig = watchdogConfig;
53
+ // Default creator and destructor.
54
+ this._creator = contextConfig => Context.create(contextConfig);
55
+ this._destructor = context => context.destroy();
56
+ this._actionQueues.onEmpty(() => {
57
+ if (this.state === 'initializing') {
58
+ this.state = 'ready';
59
+ this._fire('stateChange');
60
+ }
61
+ });
62
+ }
63
+ /**
64
+ * Sets the function that is responsible for the context creation.
65
+ * It expects a function that should return a promise (or `undefined`).
66
+ *
67
+ * ```ts
68
+ * watchdog.setCreator( config => Context.create( config ) );
69
+ * ```
70
+ */
71
+ setCreator(creator) {
72
+ this._creator = creator;
73
+ }
74
+ /**
75
+ * Sets the function that is responsible for the context destruction.
76
+ * Overrides the default destruction function, which destroys only the context instance.
77
+ * It expects a function that should return a promise (or `undefined`).
78
+ *
79
+ * ```ts
80
+ * watchdog.setDestructor( context => {
81
+ * // Do something before the context is destroyed.
82
+ *
83
+ * return context
84
+ * .destroy()
85
+ * .then( () => {
86
+ * // Do something after the context is destroyed.
87
+ * } );
88
+ * } );
89
+ * ```
90
+ */
91
+ setDestructor(destructor) {
92
+ this._destructor = destructor;
93
+ }
94
+ /**
95
+ * The context instance. Keep in mind that this property might be changed when the context watchdog restarts,
96
+ * so do not keep this instance internally. Always operate on the `ContextWatchdog#context` property.
97
+ */
98
+ get context() {
99
+ return this._context;
100
+ }
101
+ /**
102
+ * Initializes the context watchdog. Once it is created, the watchdog takes care about
103
+ * recreating the context and the provided items, and starts the error handling mechanism.
104
+ *
105
+ * ```ts
106
+ * await watchdog.create( {
107
+ * plugins: []
108
+ * } );
109
+ * ```
110
+ *
111
+ * @param contextConfig The context configuration. See {@link module:core/context~Context}.
112
+ */
113
+ create(contextConfig = {}) {
114
+ return this._actionQueues.enqueue(mainQueueId, () => {
115
+ this._contextConfig = contextConfig;
116
+ return this._create();
117
+ });
118
+ }
119
+ /**
120
+ * Returns an item instance with the given `itemId`.
121
+ *
122
+ * ```ts
123
+ * const editor1 = watchdog.getItem( 'editor1' );
124
+ * ```
125
+ *
126
+ * @param itemId The item ID.
127
+ * @returns The item instance or `undefined` if an item with a given ID has not been found.
128
+ */
129
+ getItem(itemId) {
130
+ const watchdog = this._getWatchdog(itemId);
131
+ return watchdog._item;
132
+ }
133
+ /**
134
+ * Gets the state of the given item. See {@link #state} for a list of available states.
135
+ *
136
+ * ```ts
137
+ * const editor1State = watchdog.getItemState( 'editor1' );
138
+ * ```
139
+ *
140
+ * @param itemId Item ID.
141
+ * @returns The state of the item.
142
+ */
143
+ getItemState(itemId) {
144
+ const watchdog = this._getWatchdog(itemId);
145
+ return watchdog.state;
146
+ }
147
+ /**
148
+ * Adds items to the watchdog. Once created, instances of these items will be available using the {@link #getItem} method.
149
+ *
150
+ * Items can be passed together as an array of objects:
151
+ *
152
+ * ```ts
153
+ * await watchdog.add( [ {
154
+ * id: 'editor1',
155
+ * type: 'editor',
156
+ * sourceElementOrData: document.querySelector( '#editor' ),
157
+ * config: {
158
+ * plugins: [ Essentials, Paragraph, Bold, Italic ],
159
+ * toolbar: [ 'bold', 'italic', 'alignment' ]
160
+ * },
161
+ * creator: ( element, config ) => ClassicEditor.create( element, config )
162
+ * } ] );
163
+ * ```
164
+ *
165
+ * Or one by one as objects:
166
+ *
167
+ * ```ts
168
+ * await watchdog.add( {
169
+ * id: 'editor1',
170
+ * type: 'editor',
171
+ * sourceElementOrData: document.querySelector( '#editor' ),
172
+ * config: {
173
+ * plugins: [ Essentials, Paragraph, Bold, Italic ],
174
+ * toolbar: [ 'bold', 'italic', 'alignment' ]
175
+ * },
176
+ * creator: ( element, config ) => ClassicEditor.create( element, config )
177
+ * ] );
178
+ * ```
179
+ *
180
+ * Then an instance can be retrieved using the {@link #getItem} method:
181
+ *
182
+ * ```ts
183
+ * const editor1 = watchdog.getItem( 'editor1' );
184
+ * ```
185
+ *
186
+ * Note that this method can be called multiple times, but for performance reasons it is better
187
+ * to pass all items together.
188
+ *
189
+ * @param itemConfigurationOrItemConfigurations An item configuration object or an array of item configurations.
190
+ */
191
+ add(itemConfigurationOrItemConfigurations) {
192
+ const itemConfigurations = toArray(itemConfigurationOrItemConfigurations);
193
+ return Promise.all(itemConfigurations.map(item => {
194
+ return this._actionQueues.enqueue(item.id, () => {
195
+ if (this.state === 'destroyed') {
196
+ throw new Error('Cannot add items to destroyed watchdog.');
197
+ }
198
+ if (!this._context) {
199
+ throw new Error('Context was not created yet. You should call the `ContextWatchdog#create()` method first.');
200
+ }
201
+ let watchdog;
202
+ if (this._watchdogs.has(item.id)) {
203
+ throw new Error(`Item with the given id is already added: '${item.id}'.`);
204
+ }
205
+ if (item.type === 'editor') {
206
+ watchdog = new EditorWatchdog(null, this._watchdogConfig);
207
+ watchdog.setCreator(item.creator);
208
+ watchdog._setExcludedProperties(this._contextProps);
209
+ if (item.destructor) {
210
+ watchdog.setDestructor(item.destructor);
211
+ }
212
+ this._watchdogs.set(item.id, watchdog);
213
+ // Enqueue the internal watchdog errors within the main queue.
214
+ // And propagate the internal `error` events as `itemError` event.
215
+ watchdog.on('error', (evt, { error, causesRestart }) => {
216
+ this._fire('itemError', { itemId: item.id, error });
217
+ // Do not enqueue the item restart action if the item will not restart.
218
+ if (!causesRestart) {
219
+ return;
220
+ }
221
+ this._actionQueues.enqueue(item.id, () => new Promise(res => {
222
+ const rethrowRestartEventOnce = () => {
223
+ watchdog.off('restart', rethrowRestartEventOnce);
224
+ this._fire('itemRestart', { itemId: item.id });
225
+ res();
226
+ };
227
+ watchdog.on('restart', rethrowRestartEventOnce);
228
+ }));
229
+ });
230
+ return watchdog.create(item.sourceElementOrData, item.config, this._context);
231
+ }
232
+ else {
233
+ throw new Error(`Not supported item type: '${item.type}'.`);
234
+ }
235
+ });
236
+ }));
237
+ }
238
+ /**
239
+ * Removes and destroys item(s) with given ID(s).
240
+ *
241
+ * ```ts
242
+ * await watchdog.remove( 'editor1' );
243
+ * ```
244
+ *
245
+ * Or
246
+ *
247
+ * ```ts
248
+ * await watchdog.remove( [ 'editor1', 'editor2' ] );
249
+ * ```
250
+ *
251
+ * @param itemIdOrItemIds Item ID or an array of item IDs.
252
+ */
253
+ remove(itemIdOrItemIds) {
254
+ const itemIds = toArray(itemIdOrItemIds);
255
+ return Promise.all(itemIds.map(itemId => {
256
+ return this._actionQueues.enqueue(itemId, () => {
257
+ const watchdog = this._getWatchdog(itemId);
258
+ this._watchdogs.delete(itemId);
259
+ return watchdog.destroy();
260
+ });
261
+ }));
262
+ }
263
+ /**
264
+ * Destroys the context watchdog and all added items.
265
+ * Once the context watchdog is destroyed, new items cannot be added.
266
+ *
267
+ * ```ts
268
+ * await watchdog.destroy();
269
+ * ```
270
+ */
271
+ destroy() {
272
+ return this._actionQueues.enqueue(mainQueueId, () => {
273
+ this.state = 'destroyed';
274
+ this._fire('stateChange');
275
+ super.destroy();
276
+ return this._destroy();
277
+ });
278
+ }
279
+ /**
280
+ * Restarts the context watchdog.
281
+ */
282
+ _restart() {
283
+ return this._actionQueues.enqueue(mainQueueId, () => {
284
+ this.state = 'initializing';
285
+ this._fire('stateChange');
286
+ return this._destroy()
287
+ .catch(err => {
288
+ console.error('An error happened during destroying the context or items.', err);
289
+ })
290
+ .then(() => this._create())
291
+ .then(() => this._fire('restart'));
292
+ });
293
+ }
294
+ /**
295
+ * Initializes the context watchdog.
296
+ */
297
+ _create() {
298
+ return Promise.resolve()
299
+ .then(() => {
300
+ this._startErrorHandling();
301
+ return this._creator(this._contextConfig);
302
+ })
303
+ .then(context => {
304
+ this._context = context;
305
+ this._contextProps = getSubNodes(this._context);
306
+ return Promise.all(Array.from(this._watchdogs.values())
307
+ .map(watchdog => {
308
+ watchdog._setExcludedProperties(this._contextProps);
309
+ return watchdog.create(undefined, undefined, this._context);
310
+ }));
311
+ });
312
+ }
313
+ /**
314
+ * Destroys the context instance and all added items.
315
+ */
316
+ _destroy() {
317
+ return Promise.resolve()
318
+ .then(() => {
319
+ this._stopErrorHandling();
320
+ const context = this._context;
321
+ this._context = null;
322
+ this._contextProps = new Set();
323
+ return Promise.all(Array.from(this._watchdogs.values())
324
+ .map(watchdog => watchdog.destroy()))
325
+ // Context destructor destroys each editor.
326
+ .then(() => this._destructor(context));
327
+ });
328
+ }
329
+ /**
330
+ * Returns the watchdog for a given item ID.
331
+ *
332
+ * @param itemId Item ID.
333
+ */
334
+ _getWatchdog(itemId) {
335
+ const watchdog = this._watchdogs.get(itemId);
336
+ if (!watchdog) {
337
+ throw new Error(`Item with the given id was not registered: ${itemId}.`);
338
+ }
339
+ return watchdog;
340
+ }
341
+ /**
342
+ * Checks whether an error comes from the context instance and not from the item instances.
343
+ *
344
+ * @internal
345
+ */
346
+ _isErrorComingFromThisItem(error) {
347
+ for (const watchdog of this._watchdogs.values()) {
348
+ if (watchdog._isErrorComingFromThisItem(error)) {
349
+ return false;
350
+ }
351
+ }
352
+ return areConnectedThroughProperties(this._context, error.context);
353
+ }
354
+ }
355
+ /**
356
+ * Manager of action queues that allows queuing async functions.
357
+ */
358
+ class ActionQueues {
359
+ constructor() {
360
+ this._onEmptyCallbacks = [];
361
+ this._queues = new Map();
362
+ this._activeActions = 0;
363
+ }
364
+ /**
365
+ * Used to register callbacks that will be run when the queue becomes empty.
366
+ *
367
+ * @param onEmptyCallback A callback that will be run whenever the queue becomes empty.
368
+ */
369
+ onEmpty(onEmptyCallback) {
370
+ this._onEmptyCallbacks.push(onEmptyCallback);
371
+ }
372
+ /**
373
+ * It adds asynchronous actions (functions) to the proper queue and runs them one by one.
374
+ *
375
+ * @param queueId The action queue ID.
376
+ * @param action A function that should be enqueued.
377
+ */
378
+ enqueue(queueId, action) {
379
+ const isMainAction = queueId === mainQueueId;
380
+ this._activeActions++;
381
+ if (!this._queues.get(queueId)) {
382
+ this._queues.set(queueId, Promise.resolve());
383
+ }
384
+ // List all sources of actions that the current action needs to await for.
385
+ // For the main action wait for all other actions.
386
+ // For the item action wait only for the item queue and the main queue.
387
+ const awaitedActions = isMainAction ?
388
+ Promise.all(this._queues.values()) :
389
+ Promise.all([this._queues.get(mainQueueId), this._queues.get(queueId)]);
390
+ const queueWithAction = awaitedActions.then(action);
391
+ // Catch all errors in the main queue to stack promises even if an error occurred in the past.
392
+ const nonErrorQueue = queueWithAction.catch(() => { });
393
+ this._queues.set(queueId, nonErrorQueue);
394
+ return queueWithAction.finally(() => {
395
+ this._activeActions--;
396
+ if (this._queues.get(queueId) === nonErrorQueue && this._activeActions === 0) {
397
+ this._onEmptyCallbacks.forEach(cb => cb());
398
+ }
399
+ });
400
+ }
401
+ }
402
+ /**
403
+ * Transforms any value to an array. If the provided value is already an array, it is returned unchanged.
404
+ *
405
+ * @param elementOrArray The value to transform to an array.
406
+ * @returns An array created from data.
407
+ */
408
+ function toArray(elementOrArray) {
409
+ return Array.isArray(elementOrArray) ? elementOrArray : [elementOrArray];
410
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module watchdog/editorwatchdog
7
+ */
8
+ import type { CKEditorError } from 'ckeditor5/src/utils.js';
9
+ import type { Editor, EditorConfig, Context } from 'ckeditor5/src/core.js';
10
+ import Watchdog, { type WatchdogConfig } from './watchdog.js';
11
+ /**
12
+ * A watchdog for CKEditor 5 editors.
13
+ *
14
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
15
+ * how to use it.
16
+ */
17
+ export default class EditorWatchdog<TEditor extends Editor = Editor> extends Watchdog {
18
+ /**
19
+ * The current editor instance.
20
+ */
21
+ private _editor;
22
+ /**
23
+ * A promise associated with the life cycle of the editor (creation or destruction processes).
24
+ *
25
+ * It is used to prevent the initialization of the editor if the previous instance has not been destroyed yet,
26
+ * and conversely, to prevent the destruction of the editor if it has not been initialized.
27
+ */
28
+ private _lifecyclePromise;
29
+ /**
30
+ * Throttled save method. The `save()` method is called the specified `saveInterval` after `throttledSave()` is called,
31
+ * unless a new action happens in the meantime.
32
+ */
33
+ private _throttledSave;
34
+ /**
35
+ * The latest saved editor data represented as a root name -> root data object.
36
+ */
37
+ private _data?;
38
+ /**
39
+ * The last document version.
40
+ */
41
+ private _lastDocumentVersion?;
42
+ /**
43
+ * The editor source element or data.
44
+ */
45
+ private _elementOrData?;
46
+ /**
47
+ * Specifies whether the editor was initialized using document data (`true`) or HTML elements (`false`).
48
+ */
49
+ private _initUsingData;
50
+ /**
51
+ * The latest record of the editor editable elements. Used to restart the editor.
52
+ */
53
+ private _editables;
54
+ /**
55
+ * The editor configuration.
56
+ */
57
+ private _config?;
58
+ /**
59
+ * The creation method.
60
+ *
61
+ * @see #setCreator
62
+ */
63
+ protected _creator: EditorCreatorFunction<TEditor>;
64
+ /**
65
+ * The destruction method.
66
+ *
67
+ * @see #setDestructor
68
+ */
69
+ protected _destructor: (editor: Editor) => Promise<unknown>;
70
+ private _excludedProps?;
71
+ /**
72
+ * @param Editor The editor class.
73
+ * @param watchdogConfig The watchdog plugin configuration.
74
+ */
75
+ constructor(Editor: {
76
+ create(...args: any): Promise<TEditor>;
77
+ } | null, watchdogConfig?: WatchdogConfig);
78
+ /**
79
+ * The current editor instance.
80
+ */
81
+ get editor(): TEditor | null;
82
+ /**
83
+ * @internal
84
+ */
85
+ get _item(): TEditor | null;
86
+ /**
87
+ * Sets the function that is responsible for the editor creation.
88
+ * It expects a function that should return a promise.
89
+ *
90
+ * ```ts
91
+ * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
92
+ * ```
93
+ */
94
+ setCreator(creator: EditorCreatorFunction<TEditor>): void;
95
+ /**
96
+ * Sets the function that is responsible for the editor destruction.
97
+ * Overrides the default destruction function, which destroys only the editor instance.
98
+ * It expects a function that should return a promise or `undefined`.
99
+ *
100
+ * ```ts
101
+ * watchdog.setDestructor( editor => {
102
+ * // Do something before the editor is destroyed.
103
+ *
104
+ * return editor
105
+ * .destroy()
106
+ * .then( () => {
107
+ * // Do something after the editor is destroyed.
108
+ * } );
109
+ * } );
110
+ * ```
111
+ */
112
+ setDestructor(destructor: (editor: Editor) => Promise<unknown>): void;
113
+ /**
114
+ * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
115
+ * the state to `initializing`.
116
+ *
117
+ * @fires restart
118
+ */
119
+ protected _restart(): Promise<unknown>;
120
+ /**
121
+ * Creates the editor instance and keeps it running, using the defined creator and destructor.
122
+ *
123
+ * @param elementOrData The editor source element or the editor data.
124
+ * @param config The editor configuration.
125
+ * @param context A context for the editor.
126
+ */
127
+ create(elementOrData?: HTMLElement | string | Record<string, string> | Record<string, HTMLElement>, config?: EditorConfig, context?: Context): Promise<unknown>;
128
+ /**
129
+ * Destroys the watchdog and the current editor instance. It fires the callback
130
+ * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
131
+ * It also sets the state to `destroyed`.
132
+ */
133
+ destroy(): Promise<unknown>;
134
+ private _destroy;
135
+ /**
136
+ * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
137
+ * the moment of the crash.
138
+ */
139
+ private _save;
140
+ /**
141
+ * @internal
142
+ */
143
+ _setExcludedProperties(props: Set<unknown>): void;
144
+ /**
145
+ * Gets all data that is required to reinitialize editor instance.
146
+ */
147
+ private _getData;
148
+ /**
149
+ * For each attached model root, returns its HTML editable element (if available).
150
+ */
151
+ private _getEditables;
152
+ /**
153
+ * Traverses the error context and the current editor to find out whether these structures are connected
154
+ * to each other via properties.
155
+ *
156
+ * @internal
157
+ */
158
+ _isErrorComingFromThisItem(error: CKEditorError): boolean;
159
+ /**
160
+ * Clones the editor configuration.
161
+ */
162
+ private _cloneEditorConfiguration;
163
+ }
164
+ export type EditorData = {
165
+ roots: Record<string, {
166
+ content: string;
167
+ attributes: string;
168
+ isLoaded: boolean;
169
+ }>;
170
+ markers: Record<string, {
171
+ rangeJSON: {
172
+ start: any;
173
+ end: any;
174
+ };
175
+ usingOperation: boolean;
176
+ affectsData: boolean;
177
+ }>;
178
+ commentThreads: string;
179
+ suggestions: string;
180
+ };
181
+ /**
182
+ * Fired after the watchdog restarts the error in case of a crash.
183
+ *
184
+ * @eventName ~EditorWatchdog#restart
185
+ */
186
+ export type EditorWatchdogRestartEvent = {
187
+ name: 'restart';
188
+ args: [];
189
+ return: undefined;
190
+ };
191
+ export type EditorCreatorFunction<TEditor = Editor> = (elementOrData: HTMLElement | string | Record<string, string> | Record<string, HTMLElement>, config: EditorConfig) => Promise<TEditor>;