8
0

contextwatchdog.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  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/contextwatchdog
  7. */
  8. /* globals console */
  9. import Watchdog from './watchdog';
  10. import EditorWatchdog from './editorwatchdog';
  11. import areConnectedThroughProperties from './utils/areconnectedthroughproperties';
  12. import getSubNodes from './utils/getsubnodes';
  13. /**
  14. * A watchdog for the {@link module:core/context~Context} class.
  15. *
  16. * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
  17. * how to use it.
  18. *
  19. * @extends {module:watchdog/watchdog~Watchdog}
  20. */
  21. export default class ContextWatchdog extends Watchdog {
  22. /**
  23. * The context watchdog class constructor.
  24. *
  25. * const watchdog = new ContextWatchdog( Context );
  26. *
  27. * await watchdog.create( contextConfiguration );
  28. *
  29. * await watchdog.add( item );
  30. *
  31. * See the {@glink features/watchdog Watchdog feature guide} to learn more how to use this feature.
  32. *
  33. * @param {Function} Context The {@link module:core/context~Context} class.
  34. * @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog configuration.
  35. */
  36. constructor( Context, watchdogConfig = {} ) {
  37. super( watchdogConfig );
  38. /**
  39. * A map of internal watchdogs for added items.
  40. *
  41. * @protected
  42. * @type {Map.<string,module:watchdog/watchdog~EditorWatchdog>}
  43. */
  44. this._watchdogs = new Map();
  45. /**
  46. * The watchdog configuration.
  47. *
  48. * @private
  49. * @type {module:watchdog/watchdog~WatchdogConfig}
  50. */
  51. this._watchdogConfig = watchdogConfig;
  52. /**
  53. * The current context instance.
  54. *
  55. * @private
  56. * @type {module:core/context~Context|null}
  57. */
  58. this._context = null;
  59. /**
  60. * Context properties (nodes/references) that are gathered during the initial context creation
  61. * and are used to distinguish the origin of an error.
  62. *
  63. * @private
  64. * @type {Set.<*>}
  65. */
  66. this._contextProps = new Set();
  67. /**
  68. * An action queue, which is used to handle async functions queuing.
  69. *
  70. * @private
  71. * @type {ActionQueue}
  72. */
  73. this._actionQueue = new ActionQueue();
  74. /**
  75. * The configuration for the {@link module:core/context~Context}.
  76. *
  77. * @private
  78. * @member {Object} #_contextConfig
  79. */
  80. /**
  81. * The context configuration.
  82. *
  83. * @private
  84. * @member {Object|undefined} #_config
  85. */
  86. // Default creator and destructor.
  87. this._creator = contextConfig => Context.create( contextConfig );
  88. this._destructor = context => context.destroy();
  89. this._actionQueue.onEmpty( () => {
  90. if ( this.state === 'initializing' ) {
  91. this.state = 'ready';
  92. this._fire( 'stateChange' );
  93. }
  94. } );
  95. /**
  96. * Sets the function that is responsible for the context creation.
  97. * It expects a function that should return a promise (or `undefined`).
  98. *
  99. * watchdog.setCreator( config => Context.create( config ) );
  100. *
  101. * @method #setCreator
  102. * @param {Function} creator
  103. */
  104. /**
  105. * Sets the function that is responsible for the context destruction.
  106. * Overrides the default destruction function, which destroys only the context instance.
  107. * It expects a function that should return a promise (or `undefined`).
  108. *
  109. * watchdog.setDestructor( context => {
  110. * // Do something before the context is destroyed.
  111. *
  112. * return context
  113. * .destroy()
  114. * .then( () => {
  115. * // Do something after the context is destroyed.
  116. * } );
  117. * } );
  118. *
  119. * @method #setDestructor
  120. * @param {Function} destructor
  121. */
  122. }
  123. /**
  124. * The context instance. Keep in mind that this property might be changed when the context watchdog restarts,
  125. * so do not keep this instance internally. Always operate on the `ContextWatchdog#context` property.
  126. *
  127. * @type {module:core/context~Context|null}
  128. */
  129. get context() {
  130. return this._context;
  131. }
  132. /**
  133. * Initializes the context watchdog. Once it is created, the watchdog takes care about
  134. * recreating the context and the provided items, and starts the error handling mechanism.
  135. *
  136. * await watchdog.create( {
  137. * plugins: []
  138. * } );
  139. *
  140. * @param {Object} [contextConfig] The context configuration. See {@link module:core/context~Context}.
  141. * @returns {Promise}
  142. */
  143. create( contextConfig = {} ) {
  144. return this._actionQueue.enqueue( () => {
  145. this._contextConfig = contextConfig;
  146. return this._create();
  147. } );
  148. }
  149. /**
  150. * Returns an item instance with the given `itemId`.
  151. *
  152. * const editor1 = watchdog.getItem( 'editor1' );
  153. *
  154. * @param {String} itemId The item ID.
  155. * @returns {*} The item instance or `undefined` if an item with a given ID has not been found.
  156. */
  157. getItem( itemId ) {
  158. const watchdog = this._getWatchdog( itemId );
  159. return watchdog._item;
  160. }
  161. /**
  162. * Gets the state of the given item. See {@link #state} for a list of available states.
  163. *
  164. * const editor1State = watchdog.getItemState( 'editor1' );
  165. *
  166. * @param {String} itemId Item ID.
  167. * @returns {'initializing'|'ready'|'crashed'|'crashedPermanently'|'destroyed'} The state of the item.
  168. */
  169. getItemState( itemId ) {
  170. const watchdog = this._getWatchdog( itemId );
  171. return watchdog.state;
  172. }
  173. /**
  174. * Adds items to the watchdog. Once created, instances of these items will be available using the {@link #getItem} method.
  175. *
  176. * Items can be passed together as an array of objects:
  177. *
  178. * await watchdog.add( [ {
  179. * id: 'editor1',
  180. * type: 'editor',
  181. * sourceElementOrData: document.querySelector( '#editor' ),
  182. * config: {
  183. * plugins: [ Essentials, Paragraph, Bold, Italic ],
  184. * toolbar: [ 'bold', 'italic', 'alignment' ]
  185. * },
  186. * creator: ( element, config ) => ClassicEditor.create( element, config )
  187. * } ] );
  188. *
  189. * Or one by one as objects:
  190. *
  191. * await watchdog.add( {
  192. * id: 'editor1',
  193. * type: 'editor',
  194. * sourceElementOrData: document.querySelector( '#editor' ),
  195. * config: {
  196. * plugins: [ Essentials, Paragraph, Bold, Italic ],
  197. * toolbar: [ 'bold', 'italic', 'alignment' ]
  198. * },
  199. * creator: ( element, config ) => ClassicEditor.create( element, config )
  200. * ] );
  201. *
  202. * Then an instance can be retrieved using the {@link #getItem} method:
  203. *
  204. * const editor1 = watchdog.getItem( 'editor1' );
  205. *
  206. * Note that this method can be called multiple times, but for performance reasons it is better
  207. * to pass all items together.
  208. *
  209. * @param {module:watchdog/contextwatchdog~WatchdogItemConfiguration|Array.<module:watchdog/contextwatchdog~WatchdogItemConfiguration>}
  210. * itemConfigurationOrItemConfigurations An item configuration object or an array of item configurations.
  211. * @returns {Promise}
  212. */
  213. add( itemConfigurationOrItemConfigurations ) {
  214. const itemConfigurations = Array.isArray( itemConfigurationOrItemConfigurations ) ?
  215. itemConfigurationOrItemConfigurations :
  216. [ itemConfigurationOrItemConfigurations ];
  217. return this._actionQueue.enqueue( () => {
  218. if ( this.state === 'destroyed' ) {
  219. throw new Error( 'Cannot add items to destroyed watchdog.' );
  220. }
  221. if ( !this._context ) {
  222. throw new Error( 'Context was not created yet. You should call the `ContextWatchdog#create()` method first.' );
  223. }
  224. // Create new watchdogs.
  225. return Promise.all( itemConfigurations.map( item => {
  226. let watchdog;
  227. if ( this._watchdogs.has( item.id ) ) {
  228. throw new Error( `Item with the given id is already added: '${ item.id }'.` );
  229. }
  230. if ( item.type === 'editor' ) {
  231. watchdog = new EditorWatchdog( this._watchdogConfig );
  232. watchdog.setCreator( item.creator );
  233. watchdog._setExcludedProperties( this._contextProps );
  234. if ( item.destructor ) {
  235. watchdog.setDestructor( item.destructor );
  236. }
  237. this._watchdogs.set( item.id, watchdog );
  238. // Enqueue the internal watchdog errors within the main queue.
  239. // And propagate the internal `error` events as `itemError` event.
  240. watchdog.on( 'error', ( evt, { error, causesRestart } ) => {
  241. this._fire( 'itemError', { itemId: item.id, error } );
  242. // Do not enqueue the item restart action if the item will not restart.
  243. if ( !causesRestart ) {
  244. return;
  245. }
  246. this._actionQueue.enqueue( () => new Promise( res => {
  247. watchdog.on( 'restart', rethrowRestartEventOnce.bind( this ) );
  248. function rethrowRestartEventOnce() {
  249. watchdog.off( 'restart', rethrowRestartEventOnce );
  250. this._fire( 'itemRestart', { itemId: item.id } );
  251. res();
  252. }
  253. } ) );
  254. } );
  255. return watchdog.create( item.sourceElementOrData, item.config, this._context );
  256. } else {
  257. throw new Error( `Not supported item type: '${ item.type }'.` );
  258. }
  259. } ) );
  260. } );
  261. }
  262. /**
  263. * Removes and destroys item(s) with given ID(s).
  264. *
  265. * await watchdog.remove( 'editor1' );
  266. *
  267. * Or
  268. *
  269. * await watchdog.remove( [ 'editor1', 'editor2' ] );
  270. *
  271. * @param {Array.<String>|String} itemIdOrItemIds Item ID or an array of item IDs.
  272. * @returns {Promise}
  273. */
  274. remove( itemIdOrItemIds ) {
  275. const itemIds = Array.isArray( itemIdOrItemIds ) ?
  276. itemIdOrItemIds :
  277. [ itemIdOrItemIds ];
  278. return this._actionQueue.enqueue( () => {
  279. return Promise.all( itemIds.map( itemId => {
  280. const watchdog = this._getWatchdog( itemId );
  281. this._watchdogs.delete( itemId );
  282. return watchdog.destroy();
  283. } ) );
  284. } );
  285. }
  286. /**
  287. * Destroys the context watchdog and all added items.
  288. * Once the context watchdog is destroyed, new items cannot be added.
  289. *
  290. * await watchdog.destroy();
  291. *
  292. * @returns {Promise}
  293. */
  294. destroy() {
  295. return this._actionQueue.enqueue( () => {
  296. this.state = 'destroyed';
  297. this._fire( 'stateChange' );
  298. super.destroy();
  299. return this._destroy();
  300. } );
  301. }
  302. /**
  303. * Restarts the context watchdog.
  304. *
  305. * @protected
  306. * @returns {Promise}
  307. */
  308. _restart() {
  309. return this._actionQueue.enqueue( () => {
  310. this.state = 'initializing';
  311. this._fire( 'stateChange' );
  312. return this._destroy()
  313. .catch( err => {
  314. console.error( 'An error happened during destroying the context or items.', err );
  315. } )
  316. .then( () => this._create() )
  317. .then( () => this._fire( 'restart' ) );
  318. } );
  319. }
  320. /**
  321. * @private
  322. * @returns {Promise}
  323. */
  324. _create() {
  325. return Promise.resolve()
  326. .then( () => {
  327. this._startErrorHandling();
  328. return this._creator( this._contextConfig );
  329. } )
  330. .then( context => {
  331. this._context = context;
  332. this._contextProps = getSubNodes( this._context );
  333. return Promise.all(
  334. Array.from( this._watchdogs.values() )
  335. .map( watchdog => {
  336. watchdog._setExcludedProperties( this._contextProps );
  337. return watchdog.create( undefined, undefined, this._context );
  338. } )
  339. );
  340. } );
  341. }
  342. /**
  343. * Destroys the context instance and all added items.
  344. *
  345. * @private
  346. * @returns {Promise}
  347. */
  348. _destroy() {
  349. return Promise.resolve()
  350. .then( () => {
  351. this._stopErrorHandling();
  352. const context = this._context;
  353. this._context = null;
  354. this._contextProps = new Set();
  355. return Promise.all(
  356. Array.from( this._watchdogs.values() )
  357. .map( watchdog => watchdog.destroy() )
  358. )
  359. // Context destructor destroys each editor.
  360. .then( () => this._destructor( context ) );
  361. } );
  362. }
  363. /**
  364. * Returns the watchdog for a given item ID.
  365. *
  366. * @protected
  367. * @param {String} itemId Item ID.
  368. * @returns {module:watchdog/watchdog~Watchdog} Watchdog
  369. */
  370. _getWatchdog( itemId ) {
  371. const watchdog = this._watchdogs.get( itemId );
  372. if ( !watchdog ) {
  373. throw new Error( `Item with the given id was not registered: ${ itemId }.` );
  374. }
  375. return watchdog;
  376. }
  377. /**
  378. * Checks whether an error comes from the context instance and not from the item instances.
  379. *
  380. * @protected
  381. * @param {Error} error
  382. * @returns {Boolean}
  383. */
  384. _isErrorComingFromThisItem( error ) {
  385. for ( const watchdog of this._watchdogs.values() ) {
  386. if ( watchdog._isErrorComingFromThisItem( error ) ) {
  387. return false;
  388. }
  389. }
  390. return areConnectedThroughProperties( this._context, error.context );
  391. }
  392. /**
  393. * Fired after the watchdog restarts the context and the added items because of a crash.
  394. *
  395. * watchdog.on( 'restart', () => {
  396. * console.log( 'The context has been restarted.' );
  397. * } );
  398. *
  399. * @event restart
  400. */
  401. /**
  402. * Fired when a new error occurred in one of the added items.
  403. *
  404. * watchdog.on( 'itemError', ( evt, { error, itemId, causesRestart } ) => {
  405. * console.log( `An error occurred in an item with the '${ itemId }' ID.` );
  406. * } );
  407. *
  408. * @event itemError
  409. */
  410. /**
  411. * Fired after an item has been restarted.
  412. *
  413. * watchdog.on( 'itemRestart', ( evt, { itemId } ) => {
  414. * console.log( 'An item with with the '${ itemId }' ID has been restarted.' );
  415. * } );
  416. *
  417. * @event itemRestart
  418. */
  419. }
  420. // An action queue that allows queuing async functions.
  421. class ActionQueue {
  422. constructor() {
  423. // @type {Promise}
  424. this._promiseQueue = Promise.resolve();
  425. // @type {Array.<Function>}
  426. this._onEmptyCallbacks = [];
  427. }
  428. // Used to register callbacks that will be run when the queue becomes empty.
  429. //
  430. // @param {Function} onEmptyCallback A callback that will be run whenever the queue becomes empty.
  431. onEmpty( onEmptyCallback ) {
  432. this._onEmptyCallbacks.push( onEmptyCallback );
  433. }
  434. // It adds asynchronous actions (functions) to the queue and runs them one by one.
  435. //
  436. // @param {Function} action A function that should be enqueued.
  437. // @returns {Promise}
  438. enqueue( action ) {
  439. let nonErrorQueue;
  440. const queueWithAction = this._promiseQueue
  441. .then( action )
  442. .then( () => {
  443. if ( this._promiseQueue === nonErrorQueue ) {
  444. this._onEmptyCallbacks.forEach( cb => cb() );
  445. }
  446. } );
  447. // Catch all errors in the main queue to stack promises even if an error occurred in the past.
  448. nonErrorQueue = this._promiseQueue = queueWithAction.catch( () => { } );
  449. return queueWithAction;
  450. }
  451. }
  452. /**
  453. * The watchdog item configuration interface.
  454. *
  455. * @typedef {module:watchdog/contextwatchdog~EditorWatchdogConfiguration} module:watchdog/contextwatchdog~WatchdogItemConfiguration
  456. */
  457. /**
  458. * The editor watchdog configuration interface specifies how editors should be created and destroyed.
  459. *
  460. * @typedef {Object} module:watchdog/contextwatchdog~EditorWatchdogConfiguration
  461. *
  462. * @property {String} id A unique item identificator.
  463. *
  464. * @property {'editor'} type The type of the item to create. At the moment, only `'editor'` is supported.
  465. *
  466. * @property {Function} creator A function that initializes the item (the editor). The function takes editor initialization arguments
  467. * and should return a promise. For example: `( el, config ) => ClassicEditor.create( el, config )`.
  468. *
  469. * @property {Function} [destructor] A function that destroys the item instance (the editor). The function
  470. * takes an item and should return a promise. For example: `editor => editor.destroy()`
  471. *
  472. * @property {String|HTMLElement} sourceElementOrData The source element or data that will be passed
  473. * as the first argument to the `Editor.create()` method.
  474. *
  475. * @property {Object} config An editor configuration.
  476. */