8
0
Просмотр исходного кода

Introduced the ContextWatchdog class.

Maciej Bukowski 6 лет назад
Родитель
Сommit
e84de00ebe

+ 4 - 2
packages/ckeditor5-watchdog/package.json

@@ -10,6 +10,8 @@
   ],
   ],
   "dependencies": {
   "dependencies": {
     "@ckeditor/ckeditor5-utils": "^16.0.0",
     "@ckeditor/ckeditor5-utils": "^16.0.0",
+    "@types/mocha": "^5.2.7",
+    "@types/sinon": "^7.5.1",
     "lodash-es": "^4.17.10"
     "lodash-es": "^4.17.10"
   },
   },
   "devDependencies": {
   "devDependencies": {
@@ -21,8 +23,8 @@
     "eslint-config-ckeditor5": "^2.0.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^2.4.1",
     "husky": "^2.4.1",
     "lint-staged": "^8.2.1",
     "lint-staged": "^8.2.1",
-    "stylelint-config-ckeditor5": "^1.0.0",
-    "stylelint": "^11.1.1"
+    "stylelint": "^11.1.1",
+    "stylelint-config-ckeditor5": "^1.0.0"
   },
   },
   "engines": {
   "engines": {
     "node": ">=8.0.0",
     "node": ">=8.0.0",

+ 239 - 0
packages/ckeditor5-watchdog/src/contextwatchdog.js

@@ -0,0 +1,239 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module watchdog/contextwatchdog
+ */
+
+/* globals console */
+
+import Watchdog from './watchdog';
+import EditorWatchdog from './editorwatchdog';
+import areConnectedThroughProperties from '@ckeditor/ckeditor5-utils/src/areconnectedthroughproperties';
+
+export default class ContextWatchdog extends Watchdog {
+	/**
+	 * @param {module:watchdog/watchdog~WatchdogConfig} [config] The watchdog plugin configuration.
+	 */
+	constructor( config = {}, contextConfig = {} ) {
+		super( config );
+
+		/**
+		 * @type {Map.<string,EditorWatchdog>}
+		 */
+		this._watchdogs = new Map();
+
+		/** @type {Context|null} */
+		this._context = null;
+
+		/**
+		 * The context configuration.
+		 *
+		 * @private
+		 * @member {Object|undefined} #_config
+		 */
+
+		this._actionQueue = new ActionQueue();
+
+		this._contextConfig = contextConfig;
+	}
+
+	/**
+	 * @type {Context|null}
+	 */
+	get context() {
+		return this._context;
+	}
+
+	async add( items ) {
+		await this._actionQueue.enqueue( async () => {
+			if ( this.state === 'destroyed' ) {
+				throw new Error( 'Cannot add items do destroyed watchdog.' );
+			}
+
+			await Promise.all( Object.entries( items ).map( async ( [ itemName, itemConfig ] ) => {
+				let watchdog;
+
+				if ( itemConfig.type === 'editor' ) {
+					watchdog = new EditorWatchdog();
+					watchdog.setCreator( itemConfig.creator );
+
+					if ( itemConfig.destructor ) {
+						watchdog.setDestructor( itemConfig.destructor );
+					}
+
+					this._watchdogs.set( itemName, watchdog );
+
+					await watchdog.create( itemConfig.sourceElementOrData, itemConfig.config );
+				} else {
+					throw new Error( 'Not supported editor type ' + itemConfig.type );
+				}
+			} ) );
+
+			this.state = 'ready';
+		} );
+	}
+
+	/**
+	 * TODO
+	 *
+	 * @param {Array.<String>} itemNames
+	 */
+	async remove( itemNames ) {
+		await this._actionQueue.enqueue( async () => {
+			await Promise.all( itemNames.map( async itemName => {
+				const watchdog = this._watchdogs.get( itemName );
+
+				this._watchdogs.delete( itemName );
+
+				if ( !watchdog ) {
+					throw new Error( 'Watchdog with the given name was not added: ' + itemName );
+				}
+
+				await watchdog.destroy();
+			} ) );
+		} );
+	}
+
+	/**
+	 * TODO
+	 */
+	async waitForReady() {
+		await this._actionQueue.enqueue( () => { } );
+	}
+
+	async destroy() {
+		await this._actionQueue.enqueue( async () => {
+			this.state = 'destroyed';
+
+			await this._destroy( true );
+		} );
+	}
+
+	/**
+	 * @protected
+	 */
+	async _restart() {
+		await this._actionQueue.enqueue( async () => {
+			this.state = 'initializing';
+
+			try {
+				await this._destroy( true );
+			} catch ( err ) {
+				console.error( 'An error happened during the editor destructing.', err );
+			}
+
+			await this._create( true );
+
+			this.fire( 'restart' );
+		} );
+	}
+
+	/**
+	 * @protected
+	 */
+	async _create( isInternal = false ) {
+		await this._actionQueue.enqueue( async () => {
+			this._context = await this._creator( this._contextConfig );
+
+			await Promise.all(
+				Array.from( this._watchdogs.values() )
+					.map( watchdog => this._setupWatchdog( watchdog ) )
+			);
+
+			this.state = 'ready';
+		}, isInternal );
+	}
+
+	async _setupWatchdog( watchdog ) {
+		watchdog.updateContext( this._context );
+
+		// TODO
+		await watchdog.create();
+	}
+
+	async _destroy( isInternal = false ) {
+		await this._actionQueue.enqueue( async () => {
+			this._stopErrorHandling();
+
+			const context = this._context;
+
+			this._context = null;
+
+			await Promise.all(
+				Array.from( this._watchdogs.values() )
+					.map( async watchdog => watchdog.destroy() )
+			);
+
+			// Context destructor destroys each editor.
+			await this._destructor( context );
+		}, isInternal );
+	}
+
+	_isErrorComingFromThisInstance( error ) {
+		// TODO
+
+		return areConnectedThroughProperties( this._context, error.context );
+	}
+
+	static for( Context, watchdogConfig ) {
+		const watchdog = new this( watchdogConfig );
+
+		watchdog.setCreator( config => Context.create( config ) );
+		watchdog.setDestructor( context => context.destroy() );
+
+		watchdog._create();
+
+		return watchdog;
+	}
+}
+
+class ActionQueue {
+	constructor() {
+		/**
+		 * @type {Array.<Function>}
+		 */
+		this._queuedActions = [];
+
+		/**
+		 * @type {WeakMap.<Function, Function>}
+		 */
+		this._resolveCallbacks = new WeakMap();
+	}
+
+	/**
+	 * @param {Function} action
+	 * @param {Boolean} isInternal
+	 */
+	async enqueue( action, isInternal = false ) {
+		// Run all internal callbacks immediately.
+		if ( isInternal ) {
+			return action();
+		}
+
+		this._queuedActions.push( action );
+
+		if ( this._queuedActions.length > 1 ) {
+			await new Promise( res => {
+				this._resolveCallbacks.set( action, res );
+			} );
+
+			return;
+		}
+
+		while ( this._queuedActions.length ) {
+			const action = this._queuedActions[ 0 ];
+			const resolve = this._resolveCallbacks.get( action );
+
+			await action();
+
+			this._queuedActions.shift();
+
+			if ( resolve ) {
+				resolve();
+			}
+		}
+	}
+}

+ 20 - 20
packages/ckeditor5-watchdog/src/editorwatchdog.js

@@ -9,8 +9,6 @@
 
 
 /* globals console */
 /* globals console */
 
 
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import { throttle, cloneDeepWith, isElement } from 'lodash-es';
 import { throttle, cloneDeepWith, isElement } from 'lodash-es';
 import areConnectedThroughProperties from '@ckeditor/ckeditor5-utils/src/areconnectedthroughproperties';
 import areConnectedThroughProperties from '@ckeditor/ckeditor5-utils/src/areconnectedthroughproperties';
 import Watchdog from './watchdog';
 import Watchdog from './watchdog';
@@ -121,15 +119,7 @@ export default class EditorWatchdog extends Watchdog {
 	 * @param {Function} destructor
 	 * @param {Function} destructor
 	 */
 	 */
 
 
-	setInitializationArgs( elementOrData, config ) {
-		this._elementOrData = elementOrData;
-
-		this._config = cloneDeepWith( config, value => {
-			// Leave DOM references.
-			return isElement( value ) ? value : undefined;
-		} );
-	}
-
+	/** @param {Context} */
 	updateContext( context ) {
 	updateContext( context ) {
 		this._config.context = context;
 		this._config.context = context;
 	}
 	}
@@ -142,7 +132,7 @@ export default class EditorWatchdog extends Watchdog {
 	 * @fires restart
 	 * @fires restart
 	 * @returns {Promise}
 	 * @returns {Promise}
 	 */
 	 */
-	async restart() {
+	async _restart() {
 		this.state = 'initializing';
 		this.state = 'initializing';
 
 
 		try {
 		try {
@@ -194,10 +184,9 @@ export default class EditorWatchdog extends Watchdog {
 
 
 		// Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
 		// Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
 		// when an error occurs in one of these editors, the watchdog will restart all of them.
 		// when an error occurs in one of these editors, the watchdog will restart all of them.
-		this._config = cloneDeepWith( config, value => {
-			// Leave DOM references.
-			return isElement( value ) ? value : undefined;
-		} );
+		this._config = this._cloneConfig( config );
+
+		// console.log( this._config.context.toString() );
 
 
 		const editor = await this._creator( elementOrData, this._config );
 		const editor = await this._creator( elementOrData, this._config );
 
 
@@ -229,11 +218,11 @@ export default class EditorWatchdog extends Watchdog {
 		// Save data if there is a remaining editor data change.
 		// Save data if there is a remaining editor data change.
 		this._throttledSave.flush();
 		this._throttledSave.flush();
 
 
-		const pendingDestruction = this._destructor( this._editor );
+		const editor = this._editor;
 
 
 		this._editor = null;
 		this._editor = null;
 
 
-		await pendingDestruction;
+		await this._destructor( editor );
 	}
 	}
 
 
 	/**
 	/**
@@ -292,6 +281,19 @@ export default class EditorWatchdog extends Watchdog {
 		return areConnectedThroughProperties( this._editor, error.context );
 		return areConnectedThroughProperties( this._editor, error.context );
 	}
 	}
 
 
+	_cloneConfig( config ) {
+		return cloneDeepWith( config, ( value, key ) => {
+			// Leave DOM references.
+			if ( isElement( value ) ) {
+				return value;
+			}
+
+			if ( key === 'context' ) {
+				return context;
+			}
+		} );
+	}
+
 	/**
 	/**
 	 * A shorthand method for creating an instance of the watchdog. For the full usage, see the
 	 * A shorthand method for creating an instance of the watchdog. For the full usage, see the
 	 * {@link ~Watchdog `Watchdog` class description}.
 	 * {@link ~Watchdog `Watchdog` class description}.
@@ -320,8 +322,6 @@ export default class EditorWatchdog extends Watchdog {
 	 */
 	 */
 }
 }
 
 
-mix( Watchdog, ObservableMixin );
-
 /**
 /**
  * The watchdog plugin configuration.
  * The watchdog plugin configuration.
  *
  *

+ 9 - 1
packages/ckeditor5-watchdog/src/watchdog.js

@@ -41,6 +41,14 @@ export default class Watchdog {
 		 */
 		 */
 		this.crashes = [];
 		this.crashes = [];
 
 
+		/**
+		 * TODO
+		 *
+		 * @abstract
+		 * @protected
+		 * @method #_restart
+		 */
+
 		/**
 		/**
 		 * Specifies the state of the editor handled by the watchdog. The state can be one of the following values:
 		 * Specifies the state of the editor handled by the watchdog. The state can be one of the following values:
 		 *
 		 *
@@ -176,7 +184,7 @@ export default class Watchdog {
 			this.state = 'crashed';
 			this.state = 'crashed';
 
 
 			if ( this._shouldRestart() ) {
 			if ( this._shouldRestart() ) {
-				this.restart();
+				this._restart();
 			} else {
 			} else {
 				this.state = 'crashedPermanently';
 				this.state = 'crashedPermanently';
 			}
 			}

+ 167 - 0
packages/ckeditor5-watchdog/tests/contextwatchdog.js

@@ -0,0 +1,167 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ContextWatchdog from '../src/contextwatchdog';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Context from '@ckeditor/ckeditor5-core/src/context';
+import sinon from 'sinon';
+import { expect } from 'chai';
+
+describe( 'ContextWatchdog', () => {
+	let element1, element2;
+	const contextOptions = {};
+	let mainWatchdog;
+
+	beforeEach( () => {
+		element1 = document.createElement( 'div' );
+		element2 = document.createElement( 'div' );
+
+		document.body.appendChild( element1 );
+		document.body.appendChild( element2 );
+	} );
+
+	afterEach( () => {
+		element1.remove();
+		element2.remove();
+
+		sinon.restore();
+	} );
+
+	it.skip( 'case: editor, contextItem', async () => {
+		mainWatchdog = ContextWatchdog.for( Context, contextOptions );
+
+		mainWatchdog.add( {
+			editor1: {
+				type: 'editor',
+				creator: ( el, config ) => ClassicTestEditor.create( el, config ),
+				sourceElementOrData: element1,
+				config: {}
+			},
+			annotatedInput: {
+				type: 'contextItem',
+				creator: () => { }
+			}
+		} );
+
+		await mainWatchdog.waitForReady();
+
+		await mainWatchdog.destroy();
+	} );
+
+	describe( 'case: no editors and contextItems', () => {
+		it( 'should create only context', async () => {
+			mainWatchdog = ContextWatchdog.for( Context, contextOptions );
+
+			await mainWatchdog.waitForReady();
+
+			expect( mainWatchdog.context ).to.be.instanceOf( Context );
+
+			await mainWatchdog.destroy();
+		} );
+
+		it( 'should have proper states', async () => {
+			mainWatchdog = ContextWatchdog.for( Context, contextOptions );
+
+			expect( mainWatchdog.state ).to.equal( 'initializing' );
+
+			await mainWatchdog.waitForReady();
+
+			expect( mainWatchdog.state ).to.equal( 'ready' );
+
+			await mainWatchdog.destroy();
+
+			expect( mainWatchdog.state ).to.equal( 'destroyed' );
+		} );
+	} );
+
+	describe( 'case: multiple editors', () => {
+		it( 'should allow adding multiple items without waiting', async () => {
+			mainWatchdog = ContextWatchdog.for( Context, contextOptions );
+
+			mainWatchdog.add( {
+				editor1: {
+					type: 'editor',
+					creator: ( el, config ) => ClassicTestEditor.create( el, config ),
+					sourceElementOrData: element1,
+					config: {}
+				},
+			} );
+
+			mainWatchdog.add( {
+				editor2: {
+					type: 'editor',
+					creator: ( el, config ) => ClassicTestEditor.create( el, config ),
+					sourceElementOrData: element2,
+					config: {}
+				},
+			} );
+
+			await mainWatchdog.waitForReady();
+
+			await mainWatchdog.destroy();
+		} );
+
+		it( 'should allow adding and removing items without waiting', async () => {
+			mainWatchdog = ContextWatchdog.for( Context, contextOptions );
+
+			mainWatchdog.add( {
+				editor1: {
+					type: 'editor',
+					creator: ( el, config ) => ClassicTestEditor.create( el, config ),
+					sourceElementOrData: element1,
+					config: {}
+				},
+			} );
+
+			mainWatchdog.add( {
+				editor2: {
+					type: 'editor',
+					creator: ( el, config ) => ClassicTestEditor.create( el, config ),
+					sourceElementOrData: element2,
+					config: {}
+				},
+			} );
+
+			await mainWatchdog.waitForReady();
+
+			expect( mainWatchdog.state ).to.equal( 'ready' );
+
+			mainWatchdog.remove( [ 'editor1' ] );
+
+			await mainWatchdog.waitForReady();
+
+			mainWatchdog.remove( [ 'editor2' ] );
+
+			await mainWatchdog.waitForReady();
+
+			await mainWatchdog.destroy();
+		} );
+	} );
+
+	it( 'case: recreating watchdog', async () => {
+		mainWatchdog = ContextWatchdog.for( Context, contextOptions );
+
+		await mainWatchdog.destroy();
+		let err;
+
+		try {
+			await mainWatchdog.add( {
+				editor2: {
+					type: 'editor',
+					creator: ( el, config ) => ClassicTestEditor.create( el, config ),
+					sourceElementOrData: element1,
+					config: {}
+				},
+			} );
+		} catch ( _err ) {
+			err = _err;
+		}
+
+		expect( err ).to.be.instanceOf( Error );
+		expect( err.message ).to.match( /Cannot add items do destroyed watchdog\./ );
+	} );
+} );