8
0

watchdog.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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. * @module watchdog/watchdog
  7. */
  8. /* globals window */
  9. /**
  10. * An abstract watchdog class that handles most of the error handling process and the state of the underlying component.
  11. *
  12. * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and how to use it.
  13. *
  14. * @private
  15. * @abstract
  16. */
  17. export default class Watchdog {
  18. /**
  19. * @param {module:watchdog/watchdog~WatchdogConfig} config The watchdog plugin configuration.
  20. */
  21. constructor( config ) {
  22. /**
  23. * An array of crashes saved as an object with the following properties:
  24. *
  25. * * `message`: `String`,
  26. * * `stack`: `String`,
  27. * * `date`: `Number`,
  28. * * `filename`: `String | undefined`,
  29. * * `lineno`: `Number | undefined`,
  30. * * `colno`: `Number | undefined`,
  31. *
  32. * @public
  33. * @readonly
  34. * @type {Array.<Object>}
  35. */
  36. this.crashes = [];
  37. /**
  38. * Specifies the state of the item watched by the watchdog. The state can be one of the following values:
  39. *
  40. * * `initializing` - before the first initialization, and after crashes, before the item is ready,
  41. * * `ready` - a state when a user can interact with the item,
  42. * * `crashed` - a state when an error occurs - it quickly changes to `initializing` or `crashedPermanently`
  43. * depending on how many and how frequency errors have been caught recently,
  44. * * `crashedPermanently` - a state when the watchdog stops reacting to errors and keeps the item it is watching crashed,
  45. * * `destroyed` - a state when the item is manually destroyed by the user after calling `watchdog.destroy()`
  46. *
  47. * @public
  48. * @member {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} #state
  49. */
  50. this.state = 'initializing';
  51. /**
  52. * @protected
  53. * @type {Number}
  54. * @see module:watchdog/watchdog~WatchdogConfig
  55. */
  56. this._crashNumberLimit = typeof config.crashNumberLimit === 'number' ? config.crashNumberLimit : 3;
  57. /**
  58. * Returns the result of `Date.now()` call. It can be overridden in tests to mock time as the popular
  59. * approaches like `sinon.useFakeTimers()` does not work well with error handling.
  60. *
  61. * @protected
  62. */
  63. this._now = Date.now;
  64. /**
  65. * @protected
  66. * @type {Number}
  67. * @see module:watchdog/watchdog~WatchdogConfig
  68. */
  69. this._minimumNonErrorTimePeriod = typeof config.minimumNonErrorTimePeriod === 'number' ? config.minimumNonErrorTimePeriod : 5000;
  70. /**
  71. * Checks if the event error comes from the underlying item and restarts the item.
  72. *
  73. * @private
  74. * @type {Function}
  75. */
  76. this._boundErrorHandler = evt => {
  77. // `evt.error` is exposed by EventError while `evt.reason` is available in PromiseRejectionEvent.
  78. const error = evt.error || evt.reason;
  79. // Note that `evt.reason` might be everything that is in the promise rejection.
  80. // Similarly everything that is thrown lands in `evt.error`.
  81. if ( error instanceof Error ) {
  82. this._handleError( error, evt );
  83. }
  84. };
  85. /**
  86. * The creation method.
  87. *
  88. * @protected
  89. * @member {Function} #_creator
  90. * @see #setCreator
  91. */
  92. /**
  93. * The destruction method.
  94. *
  95. * @protected
  96. * @member {Function} #_destructor
  97. * @see #setDestructor
  98. */
  99. /**
  100. * The watched item.
  101. *
  102. * @abstract
  103. * @protected
  104. * @member {Object|undefined} #_item
  105. */
  106. /**
  107. * The method responsible for restarting the watched item.
  108. *
  109. * @abstract
  110. * @protected
  111. * @method #_restart
  112. */
  113. /**
  114. * Traverses the error context and the watched item to find out whether the error should
  115. * be handled by the given item.
  116. *
  117. * @abstract
  118. * @protected
  119. * @method #_isErrorComingFromThisItem
  120. * @param {module:utils/ckeditorerror~CKEditorError} error
  121. */
  122. /**
  123. * A dictionary of event emitter listeners.
  124. *
  125. * @private
  126. * @type {Object.<String,Array.<Function>>}
  127. */
  128. this._listeners = {};
  129. if ( !this._restart ) {
  130. throw new Error(
  131. 'The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. ' +
  132. 'Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.'
  133. );
  134. }
  135. }
  136. /**
  137. * Sets the function that is responsible for creating watchded items.
  138. *
  139. * @param {Function} creator A callback responsible for creating an item. Returns a promise
  140. * that is resolved when the item is created.
  141. */
  142. setCreator( creator ) {
  143. this._creator = creator;
  144. }
  145. /**
  146. * Sets the function that is responsible for destructing watched items.
  147. *
  148. * @param {Function} destructor A callback that takes the item and returns the promise
  149. * to the destroying process.
  150. */
  151. setDestructor( destructor ) {
  152. this._destructor = destructor;
  153. }
  154. /**
  155. * Destroys the watchdog and releases the resources.
  156. */
  157. destroy() {
  158. this._stopErrorHandling();
  159. this._listeners = {};
  160. }
  161. /**
  162. * Starts listening to the specific event name by registering a callback that will be executed
  163. * whenever an event with given name fires.
  164. *
  165. * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
  166. *
  167. * @param {String} eventName Event name.
  168. * @param {Function} callback A callback which will be added to event listeners.
  169. */
  170. on( eventName, callback ) {
  171. if ( !this._listeners[ eventName ] ) {
  172. this._listeners[ eventName ] = [];
  173. }
  174. this._listeners[ eventName ].push( callback );
  175. }
  176. /**
  177. * Stops listening to the specified event name by removing the callback from event listeners.
  178. *
  179. * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
  180. *
  181. * @param {String} eventName Event name.
  182. * @param {Function} callback A callback which will be removed from event listeners.
  183. */
  184. off( eventName, callback ) {
  185. this._listeners[ eventName ] = this._listeners[ eventName ]
  186. .filter( cb => cb !== callback );
  187. }
  188. /**
  189. * Fires an event with given event name and arguments.
  190. *
  191. * Note that this method differs from the CKEditor 5's default `EventEmitterMixin` implementation.
  192. *
  193. * @protected
  194. * @param {String} eventName Event name.
  195. * @param {...*} args Event arguments.
  196. */
  197. _fire( eventName, ...args ) {
  198. const callbacks = this._listeners[ eventName ] || [];
  199. for ( const callback of callbacks ) {
  200. callback.apply( this, [ null, ...args ] );
  201. }
  202. }
  203. /**
  204. * Starts error handling by attaching global error handlers.
  205. *
  206. * @protected
  207. */
  208. _startErrorHandling() {
  209. window.addEventListener( 'error', this._boundErrorHandler );
  210. window.addEventListener( 'unhandledrejection', this._boundErrorHandler );
  211. }
  212. /**
  213. * Stops error handling by detaching global error handlers.
  214. *
  215. * @protected
  216. */
  217. _stopErrorHandling() {
  218. window.removeEventListener( 'error', this._boundErrorHandler );
  219. window.removeEventListener( 'unhandledrejection', this._boundErrorHandler );
  220. }
  221. /**
  222. * Checks if the error comes from the watched item and restarts it.
  223. * It reacts to {@link module:utils/ckeditorerror~CKEditorError `CKEditorError` errors} only.
  224. *
  225. * @private
  226. * @fires error
  227. * @param {Error} error Error.
  228. * @param {ErrorEvent|PromiseRejectionEvent} evt Error event.
  229. */
  230. _handleError( error, evt ) {
  231. // @if CK_DEBUG // if ( error.is && error.is( 'CKEditorError' ) && error.context === undefined ) {
  232. // @if CK_DEBUG // console.warn( 'The error is missing its context and Watchdog cannot restart the proper item.' );
  233. // @if CK_DEBUG // }
  234. if ( this._shouldReactToError( error ) ) {
  235. this.crashes.push( {
  236. message: error.message,
  237. stack: error.stack,
  238. // `evt.filename`, `evt.lineno` and `evt.colno` are available only in ErrorEvent events
  239. filename: evt.filename,
  240. lineno: evt.lineno,
  241. colno: evt.colno,
  242. date: this._now()
  243. } );
  244. const causesRestart = this._shouldRestart();
  245. this.state = 'crashed';
  246. this._fire( 'stateChange' );
  247. this._fire( 'error', { error, causesRestart } );
  248. if ( causesRestart ) {
  249. this._restart();
  250. } else {
  251. this.state = 'crashedPermanently';
  252. this._fire( 'stateChange' );
  253. }
  254. }
  255. }
  256. /**
  257. * Checks whether the error should be handled by the watchdog.
  258. *
  259. * @private
  260. * @param {Error} error An error that was caught by the error handling process.
  261. */
  262. _shouldReactToError( error ) {
  263. return (
  264. error.is &&
  265. error.is( 'CKEditorError' ) &&
  266. error.context !== undefined &&
  267. // In some cases the watched item should not be restarted - e.g. during the item initialization.
  268. // That's why the `null` was introduced as a correct error context which does cause restarting.
  269. error.context !== null &&
  270. // Do not react to errors if the watchdog is in states other than `ready`.
  271. this.state === 'ready' &&
  272. this._isErrorComingFromThisItem( error )
  273. );
  274. }
  275. /**
  276. * Checks if the watchdog should restart the underlying item.
  277. */
  278. _shouldRestart() {
  279. if ( this.crashes.length <= this._crashNumberLimit ) {
  280. return true;
  281. }
  282. const lastErrorTime = this.crashes[ this.crashes.length - 1 ].date;
  283. const firstMeaningfulErrorTime = this.crashes[ this.crashes.length - 1 - this._crashNumberLimit ].date;
  284. const averageNonErrorTimePeriod = ( lastErrorTime - firstMeaningfulErrorTime ) / this._crashNumberLimit;
  285. return averageNonErrorTimePeriod > this._minimumNonErrorTimePeriod;
  286. }
  287. /**
  288. * Fired when a new {@link module:utils/ckeditorerror~CKEditorError `CKEditorError`} error connected to the watchdog instance occurs
  289. * and the watchdog will react to it.
  290. *
  291. * watchdog.on( 'error', ( evt, { error, causesRestart } ) => {
  292. * console.log( 'An error occurred.' );
  293. * } );
  294. *
  295. * @event error
  296. */
  297. }
  298. /**
  299. * The watchdog plugin configuration.
  300. *
  301. * @typedef {Object} WatchdogConfig
  302. *
  303. * @property {Number} [crashNumberLimit=3] A threshold specifying the number of watched item crashes
  304. * when the watchdog stops restarting the item in case of errors.
  305. * After this limit is reached and the time between last errors is shorter than `minimumNonErrorTimePeriod`
  306. * the watchdog changes its state to `crashedPermanently` and it stops restarting the item. This prevents an infinite restart loop.
  307. *
  308. * @property {Number} [minimumNonErrorTimePeriod=5000] An average amount of milliseconds between last watched item errors
  309. * (defaults to 5000). When the period of time between errors is lower than that and the `crashNumberLimit` is also reached
  310. * the watchdog changes its state to `crashedPermanently` and it stops restarting the item. This prevents an infinite restart loop.
  311. *
  312. * @property {Number} [saveInterval=5000] A minimum number of milliseconds between saving editor data internally, (defaults to 5000).
  313. * Note that for large documents this might have an impact on the editor performance.
  314. */