From c8dbb2c7575b38f0ab66dafc34922ec3422dcd3c Mon Sep 17 00:00:00 2001 From: Aiosa Date: Tue, 17 Jan 2023 11:13:48 +0100 Subject: [PATCH 1/2] Implement support for async function and promise type recognition with $.type. Add $.Promise proxy. Implement support for promises in EventSource. Implement ability to abort events as well as prioritize events. --- package-lock.json | 4 +- src/eventsource.js | 59 ++++++++-- src/openseadragon.js | 35 ++++-- test/coverage.html | 1 + test/modules/event-source.js | 203 +++++++++++++++++++++++++++++++++++ test/test.html | 1 + 6 files changed, 282 insertions(+), 21 deletions(-) create mode 100644 test/modules/event-source.js diff --git a/package-lock.json b/package-lock.json index ca799e74..821ed8ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openseadragon", - "version": "3.1.0", + "version": "4.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openseadragon", - "version": "3.1.0", + "version": "4.0.0", "license": "BSD-3-Clause", "devDependencies": { "grunt": "^1.4.1", diff --git a/src/eventsource.js b/src/eventsource.js index 9bc29704..c367eb83 100644 --- a/src/eventsource.js +++ b/src/eventsource.js @@ -40,6 +40,7 @@ * @callback EventHandler * @memberof OpenSeadragon * @param {Object} event - See individual events for event-specific properties. + * @return {undefined|Promise} */ @@ -58,7 +59,7 @@ $.EventSource.prototype = { /** * Add an event handler to be triggered only once (or a given number of times) - * for a given event. + * for a given event. It is not removable with removeHandler(). * @function * @param {String} eventName - Name of event to register. * @param {OpenSeadragon.EventHandler} handler - Function to call when event @@ -67,8 +68,9 @@ $.EventSource.prototype = { * to the handler. * @param {Number} [times=1] - The number of times to handle the event * before removing it. + * @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority. */ - addOnceHandler: function(eventName, handler, userData, times) { + addOnceHandler: function(eventName, handler, userData, times, priority) { var self = this; times = times || 1; var count = 0; @@ -77,9 +79,9 @@ $.EventSource.prototype = { if (count === times) { self.removeHandler(eventName, onceHandler); } - handler(event); + return handler(event); }; - this.addHandler(eventName, onceHandler, userData); + this.addHandler(eventName, onceHandler, userData, priority); }, /** @@ -88,14 +90,22 @@ $.EventSource.prototype = { * @param {String} eventName - Name of event to register. * @param {OpenSeadragon.EventHandler} handler - Function to call when event is triggered. * @param {Object} [userData=null] - Arbitrary object to be passed unchanged to the handler. + * @param {Number} [priority=0] - Handler priority. By default, all priorities are 0. Higher number = priority. */ - addHandler: function ( eventName, handler, userData ) { + addHandler: function ( eventName, handler, userData, priority ) { var events = this.events[ eventName ]; if ( !events ) { this.events[ eventName ] = events = []; } if ( handler && $.isFunction( handler ) ) { - events[ events.length ] = { handler: handler, userData: userData || null }; + var index = events.length, + event = { handler: handler, userData: userData || null, priority: priority || 0 }; + events[ index ] = event; + while ( index > 0 && events[ index - 1 ].priority < events[ index ].priority ) { + events[ index ] = events[ index - 1 ]; + events[ index - 1 ] = event; + index--; + } } }, @@ -155,8 +165,10 @@ $.EventSource.prototype = { * Get a function which iterates the list of all handlers registered for a given event, calling the handler for each. * @function * @param {String} eventName - Name of event to get handlers for. + * @param {boolean} waitForPromiseHandlers - true to wait for asynchronous functions (promises are returned) + * or plain functions that return promise */ - getHandler: function ( eventName ) { + getHandler: function ( eventName, waitForPromiseHandlers) { var events = this.events[ eventName ]; if ( !events || !events.length ) { return null; @@ -164,11 +176,31 @@ $.EventSource.prototype = { events = events.length === 1 ? [ events[ 0 ] ] : Array.apply( null, events ); - return function ( source, args ) { + return waitForPromiseHandlers ? function ( source, args ) { + var length = events.length; + function loop(index) { + if ( index >= length || !events[ index ] ) { + return $.Promise.resolve(); + } + args.stopPropagation = function () { + index = length; + }; + args.eventSource = source; + args.userData = events[ index ].userData; + var result = events[ index ].handler( args ); + result = (!result || $.type(result) !== "promise") ? $.Promise.resolve() : result; + return result.then(function () { + loop(index + 1); + }); + } + return loop(0); + } : function ( source, args ) { var i, - length = events.length; + length = events.length, + stop = function () { i = length; }; for ( i = 0; i < length; i++ ) { if ( events[ i ] ) { + args.stopPropagation = stop; args.eventSource = source; args.userData = events[ i ].userData; events[ i ].handler( args ); @@ -182,19 +214,24 @@ $.EventSource.prototype = { * @function * @param {String} eventName - Name of event to register. * @param {Object} eventArgs - Event-specific data. + * @param {boolean} eventArgs.waitForPromiseHandlers - Synchronizes asynchronous-like handlers + * @return {undefined|Promise} - A promise is returned in case waitForPromiseHandlers = true */ raiseEvent: function( eventName, eventArgs ) { //uncomment if you want to get a log of all events //$.console.log( eventName ); - var handler = this.getHandler( eventName ); + + var awaits = (eventArgs && eventArgs.waitForPromiseHandlers) || false, + handler = this.getHandler( eventName, awaits ); if ( handler ) { if ( !eventArgs ) { eventArgs = {}; } - handler( this, eventArgs ); + return handler( this, eventArgs ); } + return undefined; } }; diff --git a/src/openseadragon.js b/src/openseadragon.js index 45e4c8e7..b3ee4dbb 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -826,14 +826,16 @@ function OpenSeadragon( options ){ * @private */ var class2type = { - '[object Boolean]': 'boolean', - '[object Number]': 'number', - '[object String]': 'string', - '[object Function]': 'function', - '[object Array]': 'array', - '[object Date]': 'date', - '[object RegExp]': 'regexp', - '[object Object]': 'object' + '[object Boolean]': 'boolean', + '[object Number]': 'number', + '[object String]': 'string', + '[object Function]': 'function', + '[object AsyncFunction]': 'function', + '[object Promise]': 'promise', + '[object Array]': 'array', + '[object Date]': 'date', + '[object RegExp]': 'regexp', + '[object Object]': 'object' }, // Save a reference to some core methods toString = Object.prototype.toString, @@ -849,6 +851,23 @@ function OpenSeadragon( options ){ return $.type(obj) === "function"; }; + /** + * Promise proxy in OpenSeadragon, can be removed once IE11 support is dropped + * @type {PromiseConstructor|(function())|*} + */ + $.Promise = (function () { + if (window.Promise) { + return window.Promise; + } + var promise = function () {}; + promise.prototype.then = function () { + throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises."; + }; + promise.prototype.resolve = function () { + throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises."; + }; + return promise; + })(); /** * Taken from jQuery 1.6.1 diff --git a/test/coverage.html b/test/coverage.html index 6d392209..68756461 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -65,6 +65,7 @@ + diff --git a/test/modules/event-source.js b/test/modules/event-source.js new file mode 100644 index 00000000..3eb23851 --- /dev/null +++ b/test/modules/event-source.js @@ -0,0 +1,203 @@ +/* global QUnit, $, TouchUtil, Util, testLog */ + +(function () { + var context, result=[], eName = "test", eventCounter = 0, finished = false; + + function evaluateTest(e) { + if (finished) return; + finished = true; + e.stopPropagation(); + e.assert.strictEqual(JSON.stringify(result), JSON.stringify(e.expected), e.message); + e.done(); + } + + function executor(i, ms, breaks=false) { + if (ms === undefined) return function (e) { + eventCounter++; + result.push(i); + if (breaks) { + e.stopPropagation(); + evaluateTest(e); + } else if (eventCounter === context.numberOfHandlers(eName)) { + evaluateTest(e); + } + }; + + return function (e) { + return new Promise(function (resolve) { + setTimeout(function () { + eventCounter++; + result.push(i); + if (breaks) { + e.stopPropagation(); + evaluateTest(e); + } else if (eventCounter === context.numberOfHandlers(eName)) { + evaluateTest(e); + } + resolve(); + }, ms); + }); + } + } + + function runTest(e) { + context.raiseEvent(eName, e); + } + + QUnit.module( 'EventSource', { + beforeEach: function () { + context = new OpenSeadragon.EventSource(); + eventCounter = 0; + result = []; + finished = false; + } + } ); + + // ---------- + QUnit.test('EventSource: no events', function(assert) { + context.addHandler(eName, evaluateTest); + runTest({ + assert: assert, + done: assert.async(), + expected: [], + message: 'No handlers registered - arrays should be empty.' + }); + }); + + QUnit.test('EventSource: simple callbacks order', function(assert) { + context.addHandler(eName, executor(1)); + context.addHandler(eName, executor(2)); + context.addHandler(eName, executor(3)); + runTest({ + assert: assert, + done: assert.async(), + expected: [1, 2, 3], + message: 'Simple callback order should follow [1,2,3].' + }); + }); + + QUnit.test('EventSource: simple callbacks order with break', function(assert) { + context.addHandler(eName, executor(1)); + context.addHandler(eName, executor(2, undefined, true)); + context.addHandler(eName, executor(3)); + runTest({ + assert: assert, + done: assert.async(), + expected: [1, 2], + message: 'Simple callback order should follow [1,2] since 2 breaks the event.' + }); + }); + + QUnit.test('EventSource: priority callbacks order', function(assert) { + context.addHandler(eName, executor(1), undefined, 20); + context.addHandler(eName, executor(2), undefined, 124); + context.addHandler(eName, executor(3), undefined, -5); + context.addHandler(eName, executor(4)); + context.addHandler(eName, executor(5), undefined, -2); + runTest({ + assert: assert, + done: assert.async(), + expected: [2, 1, 4, 5, 3], + message: 'Prioritized callback order should follow [2,1,4,5,3].' + }); + }); + + QUnit.test('EventSource: async non-synchronized order', function(assert) { + context.addHandler(eName, executor(1, 5)); + context.addHandler(eName, executor(2, 50)); + context.addHandler(eName, executor(3)); + context.addHandler(eName, executor(4)); + runTest({ + assert: assert, + done: assert.async(), + expected: [3, 4, 1, 2], + message: 'Async callback order should follow [3,4,1,2].' + }); + }); + + QUnit.test('EventSource: async non-synchronized priority order', function(assert) { + context.addHandler(eName, executor(1, 5)); + context.addHandler(eName, executor(2, 50), undefined, -100); + context.addHandler(eName, executor(3), undefined, -500); + context.addHandler(eName, executor(4), undefined, 675); + runTest({ + assert: assert, + done: assert.async(), + expected: [4, 3, 1, 2], + message: 'Async callback order with priority should follow [4,3,1,2]. Async functions do not respect priority.' + }); + }); + + QUnit.test('EventSource: async synchronized order', function(assert) { + context.addHandler(eName, executor(1, 5)); + context.addHandler(eName, executor(2, 50)); + context.addHandler(eName, executor(3)); + context.addHandler(eName, executor(4)); + runTest({ + waitForPromiseHandlers: true, + assert: assert, + done: assert.async(), + expected: [1, 2, 3, 4], + message: 'Async callback order should follow [1,2,3,4], since it is synchronized.' + }); + }); + + QUnit.test('EventSource: async synchronized priority order', function(assert) { + context.addHandler(eName, executor(1, 5)); + context.addHandler(eName, executor(2), undefined, -500); + context.addHandler(eName, executor(3, 50), undefined, -200); + context.addHandler(eName, executor(4), undefined, 675); + runTest({ + waitForPromiseHandlers: true, + assert: assert, + done: assert.async(), + expected: [4, 1, 3, 2], + message: 'Async callback order with priority should follow [4,1,3,2], since priority is respected when synchronized.' + }); + }); + + QUnit.test('EventSource: async non-synchronized with breaking', function(assert) { + context.addHandler(eName, executor(1, 5)); + context.addHandler(eName, executor(2, 50, true)); + context.addHandler(eName, executor(3, 80)); + context.addHandler(eName, executor(4)); + runTest({ + assert: assert, + done: assert.async(), + expected: [4, 1, 2], + message: 'Async breaking should follow [4,1,2,3]. Async functions do not necessarily respect breaking, but unit tests finish after 50 ms.' + }); + }); + + // These tests fail despite being 'correct' - inspection shows that callabacks are called with mixed + // data in closures or even twice one 'setTimeout' handler. No issues in isolated test run. Possibly + // an issue with Qunit. + // + + // QUnit.test('EventSource: async synchronized priority order with breaking', function(assert) { + // context.addHandler(eName, executor(1, 5)); + // context.addHandler(eName, executor(2, 50, true), undefined, -100); + // context.addHandler(eName, executor(3), undefined, -500); + // context.addHandler(eName, executor(4), undefined, 675) + // runTest({ + // waitForPromiseHandlers: true, + // assert: assert, + // done: assert.async(), + // expected: [4, 1, 2], + // message: 'Async callback order with synced priority should follow [4,1,2], since 2 stops execution.' + // }); + // }); + // QUnit.test('EventSource: async synchronized priority order with breaking', function(assert) { + // context.addHandler(eName, executor(1, 50)); + // context.addHandler(eName, executor(2, 5), undefined, -300); + // context.addHandler(eName, executor(3, 80, true), undefined, -70); + // context.addHandler(eName, executor(4), undefined, 675); + // runTest({ + // waitForPromiseHandlers: true, + // assert: assert, + // done: assert.async(), + // expected: [4, 1, 3], + // message: 'Async callback order with sync should follow [4,1,3]. Async break works when synchronized.' + // }); + // }); +} )(); diff --git a/test/test.html b/test/test.html index 8b85a123..761db1f2 100644 --- a/test/test.html +++ b/test/test.html @@ -22,6 +22,7 @@ + From de00939d8d23ce1fbfa5d21492217991aad84bcb Mon Sep 17 00:00:00 2001 From: Aiosa Date: Sat, 21 Jan 2023 09:00:24 +0100 Subject: [PATCH 2/2] Revert async support and event breaking support in EventSource. --- src/eventsource.js | 41 ++---------- src/openseadragon.js | 18 ----- test/modules/event-source.js | 124 +---------------------------------- 3 files changed, 8 insertions(+), 175 deletions(-) diff --git a/src/eventsource.js b/src/eventsource.js index c367eb83..f92adb67 100644 --- a/src/eventsource.js +++ b/src/eventsource.js @@ -40,7 +40,6 @@ * @callback EventHandler * @memberof OpenSeadragon * @param {Object} event - See individual events for event-specific properties. - * @return {undefined|Promise} */ @@ -165,10 +164,8 @@ $.EventSource.prototype = { * Get a function which iterates the list of all handlers registered for a given event, calling the handler for each. * @function * @param {String} eventName - Name of event to get handlers for. - * @param {boolean} waitForPromiseHandlers - true to wait for asynchronous functions (promises are returned) - * or plain functions that return promise */ - getHandler: function ( eventName, waitForPromiseHandlers) { + getHandler: function ( eventName) { var events = this.events[ eventName ]; if ( !events || !events.length ) { return null; @@ -176,31 +173,11 @@ $.EventSource.prototype = { events = events.length === 1 ? [ events[ 0 ] ] : Array.apply( null, events ); - return waitForPromiseHandlers ? function ( source, args ) { - var length = events.length; - function loop(index) { - if ( index >= length || !events[ index ] ) { - return $.Promise.resolve(); - } - args.stopPropagation = function () { - index = length; - }; - args.eventSource = source; - args.userData = events[ index ].userData; - var result = events[ index ].handler( args ); - result = (!result || $.type(result) !== "promise") ? $.Promise.resolve() : result; - return result.then(function () { - loop(index + 1); - }); - } - return loop(0); - } : function ( source, args ) { + return function ( source, args ) { var i, - length = events.length, - stop = function () { i = length; }; + length = events.length; for ( i = 0; i < length; i++ ) { if ( events[ i ] ) { - args.stopPropagation = stop; args.eventSource = source; args.userData = events[ i ].userData; events[ i ].handler( args ); @@ -214,22 +191,14 @@ $.EventSource.prototype = { * @function * @param {String} eventName - Name of event to register. * @param {Object} eventArgs - Event-specific data. - * @param {boolean} eventArgs.waitForPromiseHandlers - Synchronizes asynchronous-like handlers - * @return {undefined|Promise} - A promise is returned in case waitForPromiseHandlers = true */ raiseEvent: function( eventName, eventArgs ) { //uncomment if you want to get a log of all events //$.console.log( eventName ); - var awaits = (eventArgs && eventArgs.waitForPromiseHandlers) || false, - handler = this.getHandler( eventName, awaits ); - + var handler = this.getHandler( eventName ); if ( handler ) { - if ( !eventArgs ) { - eventArgs = {}; - } - - return handler( this, eventArgs ); + return handler( this, eventArgs || {} ); } return undefined; } diff --git a/src/openseadragon.js b/src/openseadragon.js index b3ee4dbb..cb111195 100644 --- a/src/openseadragon.js +++ b/src/openseadragon.js @@ -851,24 +851,6 @@ function OpenSeadragon( options ){ return $.type(obj) === "function"; }; - /** - * Promise proxy in OpenSeadragon, can be removed once IE11 support is dropped - * @type {PromiseConstructor|(function())|*} - */ - $.Promise = (function () { - if (window.Promise) { - return window.Promise; - } - var promise = function () {}; - promise.prototype.then = function () { - throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises."; - }; - promise.prototype.resolve = function () { - throw "OpenSeadragon needs promises API. Your browser do not support promises. You can add polyfill.js to import promises."; - }; - return promise; - })(); - /** * Taken from jQuery 1.6.1 * @function isArray diff --git a/test/modules/event-source.js b/test/modules/event-source.js index 3eb23851..2f61e5ee 100644 --- a/test/modules/event-source.js +++ b/test/modules/event-source.js @@ -6,19 +6,15 @@ function evaluateTest(e) { if (finished) return; finished = true; - e.stopPropagation(); e.assert.strictEqual(JSON.stringify(result), JSON.stringify(e.expected), e.message); e.done(); } - function executor(i, ms, breaks=false) { + function executor(i, ms) { if (ms === undefined) return function (e) { eventCounter++; result.push(i); - if (breaks) { - e.stopPropagation(); - evaluateTest(e); - } else if (eventCounter === context.numberOfHandlers(eName)) { + if (eventCounter === context.numberOfHandlers(eName)) { evaluateTest(e); } }; @@ -28,10 +24,7 @@ setTimeout(function () { eventCounter++; result.push(i); - if (breaks) { - e.stopPropagation(); - evaluateTest(e); - } else if (eventCounter === context.numberOfHandlers(eName)) { + if (eventCounter === context.numberOfHandlers(eName)) { evaluateTest(e); } resolve(); @@ -76,18 +69,6 @@ }); }); - QUnit.test('EventSource: simple callbacks order with break', function(assert) { - context.addHandler(eName, executor(1)); - context.addHandler(eName, executor(2, undefined, true)); - context.addHandler(eName, executor(3)); - runTest({ - assert: assert, - done: assert.async(), - expected: [1, 2], - message: 'Simple callback order should follow [1,2] since 2 breaks the event.' - }); - }); - QUnit.test('EventSource: priority callbacks order', function(assert) { context.addHandler(eName, executor(1), undefined, 20); context.addHandler(eName, executor(2), undefined, 124); @@ -101,103 +82,4 @@ message: 'Prioritized callback order should follow [2,1,4,5,3].' }); }); - - QUnit.test('EventSource: async non-synchronized order', function(assert) { - context.addHandler(eName, executor(1, 5)); - context.addHandler(eName, executor(2, 50)); - context.addHandler(eName, executor(3)); - context.addHandler(eName, executor(4)); - runTest({ - assert: assert, - done: assert.async(), - expected: [3, 4, 1, 2], - message: 'Async callback order should follow [3,4,1,2].' - }); - }); - - QUnit.test('EventSource: async non-synchronized priority order', function(assert) { - context.addHandler(eName, executor(1, 5)); - context.addHandler(eName, executor(2, 50), undefined, -100); - context.addHandler(eName, executor(3), undefined, -500); - context.addHandler(eName, executor(4), undefined, 675); - runTest({ - assert: assert, - done: assert.async(), - expected: [4, 3, 1, 2], - message: 'Async callback order with priority should follow [4,3,1,2]. Async functions do not respect priority.' - }); - }); - - QUnit.test('EventSource: async synchronized order', function(assert) { - context.addHandler(eName, executor(1, 5)); - context.addHandler(eName, executor(2, 50)); - context.addHandler(eName, executor(3)); - context.addHandler(eName, executor(4)); - runTest({ - waitForPromiseHandlers: true, - assert: assert, - done: assert.async(), - expected: [1, 2, 3, 4], - message: 'Async callback order should follow [1,2,3,4], since it is synchronized.' - }); - }); - - QUnit.test('EventSource: async synchronized priority order', function(assert) { - context.addHandler(eName, executor(1, 5)); - context.addHandler(eName, executor(2), undefined, -500); - context.addHandler(eName, executor(3, 50), undefined, -200); - context.addHandler(eName, executor(4), undefined, 675); - runTest({ - waitForPromiseHandlers: true, - assert: assert, - done: assert.async(), - expected: [4, 1, 3, 2], - message: 'Async callback order with priority should follow [4,1,3,2], since priority is respected when synchronized.' - }); - }); - - QUnit.test('EventSource: async non-synchronized with breaking', function(assert) { - context.addHandler(eName, executor(1, 5)); - context.addHandler(eName, executor(2, 50, true)); - context.addHandler(eName, executor(3, 80)); - context.addHandler(eName, executor(4)); - runTest({ - assert: assert, - done: assert.async(), - expected: [4, 1, 2], - message: 'Async breaking should follow [4,1,2,3]. Async functions do not necessarily respect breaking, but unit tests finish after 50 ms.' - }); - }); - - // These tests fail despite being 'correct' - inspection shows that callabacks are called with mixed - // data in closures or even twice one 'setTimeout' handler. No issues in isolated test run. Possibly - // an issue with Qunit. - // - - // QUnit.test('EventSource: async synchronized priority order with breaking', function(assert) { - // context.addHandler(eName, executor(1, 5)); - // context.addHandler(eName, executor(2, 50, true), undefined, -100); - // context.addHandler(eName, executor(3), undefined, -500); - // context.addHandler(eName, executor(4), undefined, 675) - // runTest({ - // waitForPromiseHandlers: true, - // assert: assert, - // done: assert.async(), - // expected: [4, 1, 2], - // message: 'Async callback order with synced priority should follow [4,1,2], since 2 stops execution.' - // }); - // }); - // QUnit.test('EventSource: async synchronized priority order with breaking', function(assert) { - // context.addHandler(eName, executor(1, 50)); - // context.addHandler(eName, executor(2, 5), undefined, -300); - // context.addHandler(eName, executor(3, 80, true), undefined, -70); - // context.addHandler(eName, executor(4), undefined, 675); - // runTest({ - // waitForPromiseHandlers: true, - // assert: assert, - // done: assert.async(), - // expected: [4, 1, 3], - // message: 'Async callback order with sync should follow [4,1,3]. Async break works when synchronized.' - // }); - // }); } )();