editorwatchdog.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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/editorwatchdog
  7. */
  8. /* globals console */
  9. import { throttle, cloneDeepWith, isElement } from 'lodash-es';
  10. import areConnectedThroughProperties from './utils/areconnectedthroughproperties';
  11. import Watchdog from './watchdog';
  12. /**
  13. * A watchdog for CKEditor 5 editors.
  14. *
  15. * See the {@glink features/watchdog Watchdog feature guide} to learn the rationale behind it and
  16. * how to use it.
  17. *
  18. * @extends {module:watchdog/watchdog~Watchdog}
  19. */
  20. export default class EditorWatchdog extends Watchdog {
  21. /**
  22. * @param {*} Editor The editor class.
  23. * @param {module:watchdog/watchdog~WatchdogConfig} [watchdogConfig] The watchdog plugin configuration.
  24. */
  25. constructor( Editor, watchdogConfig = {} ) {
  26. super( watchdogConfig );
  27. /**
  28. * The current editor instance.
  29. *
  30. * @private
  31. * @type {module:core/editor/editor~Editor}
  32. */
  33. this._editor = null;
  34. /**
  35. * Throttled save method. The `save()` method is called the specified `saveInterval` after `throttledSave()` is called,
  36. * unless a new action happens in the meantime.
  37. *
  38. * @private
  39. * @type {Function}
  40. */
  41. this._throttledSave = throttle(
  42. this._save.bind( this ),
  43. typeof watchdogConfig.saveInterval === 'number' ? watchdogConfig.saveInterval : 5000
  44. );
  45. /**
  46. * The latest saved editor data represented as a root name -> root data object.
  47. *
  48. * @private
  49. * @member {Object.<String,String>} #_data
  50. */
  51. /**
  52. * The last document version.
  53. *
  54. * @private
  55. * @member {Number} #_lastDocumentVersion
  56. */
  57. /**
  58. * The editor source element or data.
  59. *
  60. * @private
  61. * @member {HTMLElement|String|Object.<String|String>} #_elementOrData
  62. */
  63. /**
  64. * The editor configuration.
  65. *
  66. * @private
  67. * @member {Object|undefined} #_config
  68. */
  69. // Set default creator and destructor functions:
  70. this._creator = ( ( elementOrData, config ) => Editor.create( elementOrData, config ) );
  71. this._destructor = editor => editor.destroy();
  72. }
  73. /**
  74. * The current editor instance.
  75. *
  76. * @readonly
  77. * @type {module:core/editor/editor~Editor}
  78. */
  79. get editor() {
  80. return this._editor;
  81. }
  82. /**
  83. * @inheritDoc
  84. */
  85. get _item() {
  86. return this._editor;
  87. }
  88. /**
  89. * Sets the function that is responsible for the editor creation.
  90. * It expects a function that should return a promise.
  91. *
  92. * watchdog.setCreator( ( element, config ) => ClassicEditor.create( element, config ) );
  93. *
  94. * @method #setCreator
  95. * @param {Function} creator
  96. */
  97. /**
  98. * Sets the function that is responsible for the editor destruction.
  99. * Overrides the default destruction function, which destroys only the editor instance.
  100. * It expects a function that should return a promise or `undefined`.
  101. *
  102. * watchdog.setDestructor( editor => {
  103. * // Do something before the editor is destroyed.
  104. *
  105. * return editor
  106. * .destroy()
  107. * .then( () => {
  108. * // Do something after the editor is destroyed.
  109. * } );
  110. * } );
  111. *
  112. * @method #setDestructor
  113. * @param {Function} destructor
  114. */
  115. /**
  116. * Restarts the editor instance. This method is called whenever an editor error occurs. It fires the `restart` event and changes
  117. * the state to `initializing`.
  118. *
  119. * @protected
  120. * @fires restart
  121. * @returns {Promise}
  122. */
  123. _restart() {
  124. return Promise.resolve()
  125. .then( () => {
  126. this.state = 'initializing';
  127. this._fire( 'stateChange' );
  128. return this._destroy();
  129. } )
  130. .catch( err => {
  131. console.error( 'An error happened during the editor destroying.', err );
  132. } )
  133. .then( () => {
  134. if ( typeof this._elementOrData === 'string' ) {
  135. return this.create( this._data, this._config, this._config.context );
  136. } else {
  137. const updatedConfig = Object.assign( {}, this._config, {
  138. initialData: this._data
  139. } );
  140. return this.create( this._elementOrData, updatedConfig, updatedConfig.context );
  141. }
  142. } )
  143. .then( () => {
  144. this._fire( 'restart' );
  145. } );
  146. }
  147. /**
  148. * Creates the editor instance and keeps it running, using the defined creator and destructor.
  149. *
  150. * @param {HTMLElement|String|Object.<String|String>} [elementOrData] The editor source element or the editor data.
  151. * @param {module:core/editor/editorconfig~EditorConfig} [config] The editor configuration.
  152. * @param {Object} [context] A context for the editor.
  153. *
  154. * @returns {Promise}
  155. */
  156. create( elementOrData = this._elementOrData, config = this._config, context ) {
  157. return Promise.resolve()
  158. .then( () => {
  159. super._startErrorHandling();
  160. this._elementOrData = elementOrData;
  161. // Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
  162. // when an error occurs in one of these editors, the watchdog will restart all of them.
  163. this._config = this._cloneEditorConfiguration( config ) || {};
  164. this._config.context = context;
  165. return this._creator( elementOrData, this._config );
  166. } )
  167. .then( editor => {
  168. this._editor = editor;
  169. editor.model.document.on( 'change:data', this._throttledSave );
  170. this._lastDocumentVersion = editor.model.document.version;
  171. this._data = this._getData();
  172. this.state = 'ready';
  173. this._fire( 'stateChange' );
  174. } );
  175. }
  176. /**
  177. * Destroys the watchdog and the current editor instance. It fires the callback
  178. * registered in {@link #setDestructor `setDestructor()`} and uses it to destroy the editor instance.
  179. * It also sets the state to `destroyed`.
  180. *
  181. * @returns {Promise}
  182. */
  183. destroy() {
  184. return Promise.resolve()
  185. .then( () => {
  186. this.state = 'destroyed';
  187. this._fire( 'stateChange' );
  188. super.destroy();
  189. return this._destroy();
  190. } );
  191. }
  192. /**
  193. * @private
  194. * @returns {Promise}
  195. */
  196. _destroy() {
  197. return Promise.resolve()
  198. .then( () => {
  199. this._stopErrorHandling();
  200. // Save data if there is a remaining editor data change.
  201. this._throttledSave.flush();
  202. const editor = this._editor;
  203. this._editor = null;
  204. return this._destructor( editor );
  205. } );
  206. }
  207. /**
  208. * Saves the editor data, so it can be restored after the crash even if the data cannot be fetched at
  209. * the moment of the crash.
  210. *
  211. * @private
  212. */
  213. _save() {
  214. const version = this._editor.model.document.version;
  215. // Operation may not result in a model change, so the document's version can be the same.
  216. if ( version === this._lastDocumentVersion ) {
  217. return;
  218. }
  219. try {
  220. this._data = this._getData();
  221. this._lastDocumentVersion = version;
  222. } catch ( err ) {
  223. console.error(
  224. err,
  225. 'An error happened during restoring editor data. ' +
  226. 'Editor will be restored from the previously saved data.'
  227. );
  228. }
  229. }
  230. /**
  231. * @protected
  232. * @param {Set} props
  233. */
  234. _setExcludedProperties( props ) {
  235. this._excludedProps = props;
  236. }
  237. /**
  238. * Returns the editor data.
  239. *
  240. * @private
  241. * @returns {Object<String,String>}
  242. */
  243. _getData() {
  244. const data = {};
  245. for ( const rootName of this._editor.model.document.getRootNames() ) {
  246. data[ rootName ] = this._editor.data.get( { rootName } );
  247. }
  248. return data;
  249. }
  250. /**
  251. * Traverses the error context and the current editor to find out whether these structures are connected
  252. * to each other via properties.
  253. *
  254. * @protected
  255. * @param {module:utils/ckeditorerror~CKEditorError} error
  256. */
  257. _isErrorComingFromThisItem( error ) {
  258. return areConnectedThroughProperties( this._editor, error.context, this._excludedProps );
  259. }
  260. /**
  261. * Clones the editor configuration.
  262. *
  263. * @private
  264. * @param {Object} config
  265. */
  266. _cloneEditorConfiguration( config ) {
  267. return cloneDeepWith( config, ( value, key ) => {
  268. // Leave DOM references.
  269. if ( isElement( value ) ) {
  270. return value;
  271. }
  272. if ( key === 'context' ) {
  273. return value;
  274. }
  275. } );
  276. }
  277. /**
  278. * Fired after the watchdog restarts the error in case of a crash.
  279. *
  280. * @event restart
  281. */
  282. }