@ckeditor/ckeditor5-watchdog 38.1.1 → 38.2.0-alpha.1

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,185 +1,185 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, 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
- * An abstract watchdog class that handles most of the error handling process and the state of the underlying component.
7
- *
8
- * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and how to use it.
9
- *
10
- * @internal
11
- */
12
- export default class Watchdog {
13
- /**
14
- * @param {module:watchdog/watchdog~WatchdogConfig} config The watchdog plugin configuration.
15
- */
16
- constructor(config) {
17
- /**
18
- * An array of crashes saved as an object with the following properties:
19
- *
20
- * * `message`: `String`,
21
- * * `stack`: `String`,
22
- * * `date`: `Number`,
23
- * * `filename`: `String | undefined`,
24
- * * `lineno`: `Number | undefined`,
25
- * * `colno`: `Number | undefined`,
26
- */
27
- this.crashes = [];
28
- /**
29
- * Specifies the state of the item watched by the watchdog. The state can be one of the following values:
30
- *
31
- * * `initializing` – Before the first initialization, and after crashes, before the item is ready.
32
- * * `ready` – A state when the user can interact with the item.
33
- * * `crashed` – A state when an error occurs. It quickly changes to `initializing` or `crashedPermanently`
34
- * depending on how many and how frequent errors have been caught recently.
35
- * * `crashedPermanently` – A state when the watchdog stops reacting to errors and keeps the item it is watching crashed,
36
- * * `destroyed` – A state when the item is manually destroyed by the user after calling `watchdog.destroy()`.
37
- */
38
- this.state = 'initializing';
39
- /**
40
- * Returns the result of the `Date.now()` call. It can be overridden in tests to mock time as some popular
41
- * approaches like `sinon.useFakeTimers()` do not work well with error handling.
42
- */
43
- this._now = Date.now;
44
- this.crashes = [];
45
- this._crashNumberLimit = typeof config.crashNumberLimit === 'number' ? config.crashNumberLimit : 3;
46
- this._minimumNonErrorTimePeriod = typeof config.minimumNonErrorTimePeriod === 'number' ? config.minimumNonErrorTimePeriod : 5000;
47
- this._boundErrorHandler = evt => {
48
- // `evt.error` is exposed by EventError while `evt.reason` is available in PromiseRejectionEvent.
49
- const error = 'error' in evt ? evt.error : evt.reason;
50
- // Note that `evt.reason` might be everything that is in the promise rejection.
51
- // Similarly everything that is thrown lands in `evt.error`.
52
- if (error instanceof Error) {
53
- this._handleError(error, evt);
54
- }
55
- };
56
- this._listeners = {};
57
- if (!this._restart) {
58
- throw new Error('The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. ' +
59
- 'Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.');
60
- }
61
- }
62
- /**
63
- * Destroys the watchdog and releases the resources.
64
- */
65
- destroy() {
66
- this._stopErrorHandling();
67
- this._listeners = {};
68
- }
69
- /**
70
- * Starts listening to a specific event name by registering a callback that will be executed
71
- * whenever an event with a given name fires.
72
- *
73
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
74
- *
75
- * @param eventName The event name.
76
- * @param callback A callback which will be added to event listeners.
77
- */
78
- on(eventName, callback) {
79
- if (!this._listeners[eventName]) {
80
- this._listeners[eventName] = [];
81
- }
82
- this._listeners[eventName].push(callback);
83
- }
84
- /**
85
- * Stops listening to the specified event name by removing the callback from event listeners.
86
- *
87
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
88
- *
89
- * @param eventName The event name.
90
- * @param callback A callback which will be removed from event listeners.
91
- */
92
- off(eventName, callback) {
93
- this._listeners[eventName] = this._listeners[eventName]
94
- .filter(cb => cb !== callback);
95
- }
96
- /**
97
- * Fires an event with a given event name and arguments.
98
- *
99
- * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
100
- */
101
- _fire(eventName, ...args) {
102
- const callbacks = this._listeners[eventName] || [];
103
- for (const callback of callbacks) {
104
- callback.apply(this, [null, ...args]);
105
- }
106
- }
107
- /**
108
- * Starts error handling by attaching global error handlers.
109
- */
110
- _startErrorHandling() {
111
- window.addEventListener('error', this._boundErrorHandler);
112
- window.addEventListener('unhandledrejection', this._boundErrorHandler);
113
- }
114
- /**
115
- * Stops error handling by detaching global error handlers.
116
- */
117
- _stopErrorHandling() {
118
- window.removeEventListener('error', this._boundErrorHandler);
119
- window.removeEventListener('unhandledrejection', this._boundErrorHandler);
120
- }
121
- /**
122
- * Checks if an error comes from the watched item and restarts it.
123
- * It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only.
124
- *
125
- * @fires error
126
- * @param error Error.
127
- * @param evt An error event.
128
- */
129
- _handleError(error, evt) {
130
- // @if CK_DEBUG // const err = error as CKEditorError;
131
- // @if CK_DEBUG // if ( err.is && err.is( 'CKEditorError' ) && err.context === undefined ) {
132
- // @if CK_DEBUG // console.warn( 'The error is missing its context and Watchdog cannot restart the proper item.' );
133
- // @if CK_DEBUG // }
134
- if (this._shouldReactToError(error)) {
135
- this.crashes.push({
136
- message: error.message,
137
- stack: error.stack,
138
- // `evt.filename`, `evt.lineno` and `evt.colno` are available only in ErrorEvent events
139
- filename: evt instanceof ErrorEvent ? evt.filename : undefined,
140
- lineno: evt instanceof ErrorEvent ? evt.lineno : undefined,
141
- colno: evt instanceof ErrorEvent ? evt.colno : undefined,
142
- date: this._now()
143
- });
144
- const causesRestart = this._shouldRestart();
145
- this.state = 'crashed';
146
- this._fire('stateChange');
147
- this._fire('error', { error, causesRestart });
148
- if (causesRestart) {
149
- this._restart();
150
- }
151
- else {
152
- this.state = 'crashedPermanently';
153
- this._fire('stateChange');
154
- }
155
- }
156
- }
157
- /**
158
- * Checks whether an error should be handled by the watchdog.
159
- *
160
- * @param error An error that was caught by the error handling process.
161
- */
162
- _shouldReactToError(error) {
163
- return (error.is &&
164
- error.is('CKEditorError') &&
165
- error.context !== undefined &&
166
- // In some cases the watched item should not be restarted - e.g. during the item initialization.
167
- // That's why the `null` was introduced as a correct error context which does cause restarting.
168
- error.context !== null &&
169
- // Do not react to errors if the watchdog is in states other than `ready`.
170
- this.state === 'ready' &&
171
- this._isErrorComingFromThisItem(error));
172
- }
173
- /**
174
- * Checks if the watchdog should restart the underlying item.
175
- */
176
- _shouldRestart() {
177
- if (this.crashes.length <= this._crashNumberLimit) {
178
- return true;
179
- }
180
- const lastErrorTime = this.crashes[this.crashes.length - 1].date;
181
- const firstMeaningfulErrorTime = this.crashes[this.crashes.length - 1 - this._crashNumberLimit].date;
182
- const averageNonErrorTimePeriod = (lastErrorTime - firstMeaningfulErrorTime) / this._crashNumberLimit;
183
- return averageNonErrorTimePeriod > this._minimumNonErrorTimePeriod;
184
- }
185
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, 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
+ * An abstract watchdog class that handles most of the error handling process and the state of the underlying component.
7
+ *
8
+ * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and how to use it.
9
+ *
10
+ * @internal
11
+ */
12
+ export default class Watchdog {
13
+ /**
14
+ * @param {module:watchdog/watchdog~WatchdogConfig} config The watchdog plugin configuration.
15
+ */
16
+ constructor(config) {
17
+ /**
18
+ * An array of crashes saved as an object with the following properties:
19
+ *
20
+ * * `message`: `String`,
21
+ * * `stack`: `String`,
22
+ * * `date`: `Number`,
23
+ * * `filename`: `String | undefined`,
24
+ * * `lineno`: `Number | undefined`,
25
+ * * `colno`: `Number | undefined`,
26
+ */
27
+ this.crashes = [];
28
+ /**
29
+ * Specifies the state of the item watched by the watchdog. The state can be one of the following values:
30
+ *
31
+ * * `initializing` &ndash; Before the first initialization, and after crashes, before the item is ready.
32
+ * * `ready` &ndash; A state when the user can interact with the item.
33
+ * * `crashed` &ndash; A state when an error occurs. It quickly changes to `initializing` or `crashedPermanently`
34
+ * depending on how many and how frequent errors have been caught recently.
35
+ * * `crashedPermanently` &ndash; A state when the watchdog stops reacting to errors and keeps the item it is watching crashed,
36
+ * * `destroyed` &ndash; A state when the item is manually destroyed by the user after calling `watchdog.destroy()`.
37
+ */
38
+ this.state = 'initializing';
39
+ /**
40
+ * Returns the result of the `Date.now()` call. It can be overridden in tests to mock time as some popular
41
+ * approaches like `sinon.useFakeTimers()` do not work well with error handling.
42
+ */
43
+ this._now = Date.now;
44
+ this.crashes = [];
45
+ this._crashNumberLimit = typeof config.crashNumberLimit === 'number' ? config.crashNumberLimit : 3;
46
+ this._minimumNonErrorTimePeriod = typeof config.minimumNonErrorTimePeriod === 'number' ? config.minimumNonErrorTimePeriod : 5000;
47
+ this._boundErrorHandler = evt => {
48
+ // `evt.error` is exposed by EventError while `evt.reason` is available in PromiseRejectionEvent.
49
+ const error = 'error' in evt ? evt.error : evt.reason;
50
+ // Note that `evt.reason` might be everything that is in the promise rejection.
51
+ // Similarly everything that is thrown lands in `evt.error`.
52
+ if (error instanceof Error) {
53
+ this._handleError(error, evt);
54
+ }
55
+ };
56
+ this._listeners = {};
57
+ if (!this._restart) {
58
+ throw new Error('The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. ' +
59
+ 'Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.');
60
+ }
61
+ }
62
+ /**
63
+ * Destroys the watchdog and releases the resources.
64
+ */
65
+ destroy() {
66
+ this._stopErrorHandling();
67
+ this._listeners = {};
68
+ }
69
+ /**
70
+ * Starts listening to a specific event name by registering a callback that will be executed
71
+ * whenever an event with a given name fires.
72
+ *
73
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
74
+ *
75
+ * @param eventName The event name.
76
+ * @param callback A callback which will be added to event listeners.
77
+ */
78
+ on(eventName, callback) {
79
+ if (!this._listeners[eventName]) {
80
+ this._listeners[eventName] = [];
81
+ }
82
+ this._listeners[eventName].push(callback);
83
+ }
84
+ /**
85
+ * Stops listening to the specified event name by removing the callback from event listeners.
86
+ *
87
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
88
+ *
89
+ * @param eventName The event name.
90
+ * @param callback A callback which will be removed from event listeners.
91
+ */
92
+ off(eventName, callback) {
93
+ this._listeners[eventName] = this._listeners[eventName]
94
+ .filter(cb => cb !== callback);
95
+ }
96
+ /**
97
+ * Fires an event with a given event name and arguments.
98
+ *
99
+ * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
100
+ */
101
+ _fire(eventName, ...args) {
102
+ const callbacks = this._listeners[eventName] || [];
103
+ for (const callback of callbacks) {
104
+ callback.apply(this, [null, ...args]);
105
+ }
106
+ }
107
+ /**
108
+ * Starts error handling by attaching global error handlers.
109
+ */
110
+ _startErrorHandling() {
111
+ window.addEventListener('error', this._boundErrorHandler);
112
+ window.addEventListener('unhandledrejection', this._boundErrorHandler);
113
+ }
114
+ /**
115
+ * Stops error handling by detaching global error handlers.
116
+ */
117
+ _stopErrorHandling() {
118
+ window.removeEventListener('error', this._boundErrorHandler);
119
+ window.removeEventListener('unhandledrejection', this._boundErrorHandler);
120
+ }
121
+ /**
122
+ * Checks if an error comes from the watched item and restarts it.
123
+ * It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only.
124
+ *
125
+ * @fires error
126
+ * @param error Error.
127
+ * @param evt An error event.
128
+ */
129
+ _handleError(error, evt) {
130
+ // @if CK_DEBUG // const err = error as CKEditorError;
131
+ // @if CK_DEBUG // if ( err.is && err.is( 'CKEditorError' ) && err.context === undefined ) {
132
+ // @if CK_DEBUG // console.warn( 'The error is missing its context and Watchdog cannot restart the proper item.' );
133
+ // @if CK_DEBUG // }
134
+ if (this._shouldReactToError(error)) {
135
+ this.crashes.push({
136
+ message: error.message,
137
+ stack: error.stack,
138
+ // `evt.filename`, `evt.lineno` and `evt.colno` are available only in ErrorEvent events
139
+ filename: evt instanceof ErrorEvent ? evt.filename : undefined,
140
+ lineno: evt instanceof ErrorEvent ? evt.lineno : undefined,
141
+ colno: evt instanceof ErrorEvent ? evt.colno : undefined,
142
+ date: this._now()
143
+ });
144
+ const causesRestart = this._shouldRestart();
145
+ this.state = 'crashed';
146
+ this._fire('stateChange');
147
+ this._fire('error', { error, causesRestart });
148
+ if (causesRestart) {
149
+ this._restart();
150
+ }
151
+ else {
152
+ this.state = 'crashedPermanently';
153
+ this._fire('stateChange');
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Checks whether an error should be handled by the watchdog.
159
+ *
160
+ * @param error An error that was caught by the error handling process.
161
+ */
162
+ _shouldReactToError(error) {
163
+ return (error.is &&
164
+ error.is('CKEditorError') &&
165
+ error.context !== undefined &&
166
+ // In some cases the watched item should not be restarted - e.g. during the item initialization.
167
+ // That's why the `null` was introduced as a correct error context which does cause restarting.
168
+ error.context !== null &&
169
+ // Do not react to errors if the watchdog is in states other than `ready`.
170
+ this.state === 'ready' &&
171
+ this._isErrorComingFromThisItem(error));
172
+ }
173
+ /**
174
+ * Checks if the watchdog should restart the underlying item.
175
+ */
176
+ _shouldRestart() {
177
+ if (this.crashes.length <= this._crashNumberLimit) {
178
+ return true;
179
+ }
180
+ const lastErrorTime = this.crashes[this.crashes.length - 1].date;
181
+ const firstMeaningfulErrorTime = this.crashes[this.crashes.length - 1 - this._crashNumberLimit].date;
182
+ const averageNonErrorTimePeriod = (lastErrorTime - firstMeaningfulErrorTime) / this._crashNumberLimit;
183
+ return averageNonErrorTimePeriod > this._minimumNonErrorTimePeriod;
184
+ }
185
+ }