contextwatchdog.js 13 KB

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