Parcourir la source

Fix: Prevent infinite loops on `.once()`.

Szymon Cofalik il y a 6 ans
Parent
commit
3b54a06152

+ 12 - 4
packages/ckeditor5-utils/src/emittermixin.js

@@ -32,12 +32,20 @@ const EmitterMixin = {
 	 * @inheritDoc
 	 */
 	once( event, callback, options ) {
+		let wasFired = false;
+
 		const onceCallback = function( event, ...args ) {
-			// Go off() at the first call.
-			event.off();
+			// Ensure the callback is called only once even if the callback itself leads to re-firing the event
+			// (which would call the callback again).
+			if ( !wasFired ) {
+				wasFired = true;
+
+				// Go off() at the first call.
+				event.off();
 
-			// Go with the original callback.
-			callback.call( this, event, ...args );
+				// Go with the original callback.
+				callback.call( this, event, ...args );
+			}
 		};
 
 		// Make a similar on() call, simply replacing the callback.

+ 17 - 8
packages/ckeditor5-utils/tests/emittermixin.js

@@ -324,20 +324,29 @@ describe( 'EmitterMixin', () => {
 			sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 1, 2, 3 );
 		} );
 
-		it( 'should be removed only after exact event fired', () => {
-			const spy1 = sinon.spy();
-			const spy2 = sinon.spy();
+		it( 'should be removed also when fired through namespaced event', () => {
+			const spy = sinon.spy();
 
-			emitter.on( 'foo', spy1 );
-			emitter.once( 'foo', spy2 );
+			emitter.once( 'foo', spy );
 
 			emitter.fire( 'foo:bar' );
 			emitter.fire( 'foo' );
-			emitter.fire( 'foo:bar' );
+
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should be called only once and have infinite loop protection', () => {
+			const spy = sinon.spy();
+
+			emitter.once( 'foo', () => {
+				spy();
+
+				emitter.fire( 'foo' );
+			} );
+
 			emitter.fire( 'foo' );
 
-			sinon.assert.callCount( spy1, 4 );
-			sinon.assert.calledTwice( spy2 );
+			sinon.assert.calledOnce( spy );
 		} );
 	} );