From f8e3d94824048dd7a3e624cd18612b6d5fc8f03e Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Tue, 1 Dec 2020 05:48:50 +0300 Subject: [PATCH] reworking event.js to be more extensible... Signed-off-by: Alex A. Naanou --- event.js | 169 ++++++++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- runner.js | 86 +++++++++++++++++++++----- 3 files changed, 225 insertions(+), 32 deletions(-) diff --git a/event.js b/event.js index e9657ca..6f81e6a 100644 --- a/event.js +++ b/event.js @@ -29,6 +29,146 @@ object.Constructor('EventCommand', { Object.assign(this, data, {name}) }, }) + +module.TRIGGER = module.EventCommand('TRIGGER') + + + +// XXX would be nice to have an event object... +var _Eventfull = +module._Eventfull = +object.Constructor('Eventfull', { + + handlerLocation: 'context', + + name: null, + func: null, + + toString: function(){ + return this.func ? + `${this.constructor.name} ` + +(this.func.toString() + .replace(/^(function[^(]*\()[^,)]*, ?/, '$1')) + : `${this.constructor.name} function ${this.name}(){}` }, + + __event_handlers__: null, + bind: function(context, handler){ + var handlers = + // local... + options.handlerLocation == 'method' ? + (this.__event_handlers__ = this.__event_handlers__ || []) + // context (default)... + : (context.__event_handlers__ == null ? + Object.defineProperty(context, '__event_handlers__', { + value: {[this.name]: (handlers = [])}, + enumerable: false, + }) + && handlers + : (context.__event_handlers__[this.name] = + context.__event_handlers__[this.name] || [])) + // add handler... + handlers.push(func) + return this }, + unbind: function(context, handler){ + var handlers = + (options.handlerLocation == 'method' ? + method.__event_handlers__ + : (context.__event_handlers__ || {})[this.name]) || [] + handlers.splice(0, handlers.length, + ...handlers.filter(function(h){ + return h !== this.func + && h.__event_original_handler__ !== func })) + return this }, + + __call__: function(context, ...args){ + var handlers = + this.handlerLocation == 'method' ? + (this.__event_handlers__ || []) + : [] + // context (default)... + // NOTE: these are allways called... + handlers = handlers + .concat((context.__event_handlers__ || {})[this.name] || []) + + // NOTE: this will stop event handling if one of the handlers + // explicitly returns false... + // NOTE: if the user does not call handle() it will be called + // after the event action is done but before it returns... + // NOTE: to explicitly disable calling the handlers func must + // call handle(false) + var did_handle = false + var handle = function(run=true){ + did_handle = run === false + var a = args[0] instanceof EventCommand ? + args.slice(1) + : args + return run ? + handlers + .reduce(function(res, handler){ + return res === true + && handler(this.name, ...a) !== false }, true) + : undefined } + + // call... + var res = this.func ? + this.func.call(context, handle, ...args) + : undefined + + // call the handlers if the user either didn't call handle() + // or explicitly called handle(false)... + !did_handle + && handle() + return res }, + + __init__: function(name, func, options={}){ + options = func && typeof(func) != 'function' ? + func + : options + Object.assign(this, options) + Object.defineProperty(this, 'name', { value: name }) + func && typeof(func) == 'function' + && (this.func = func) }, +}) + + +// XXX test... +// XXX rename??? +var _Event = +module._Event = +object.Constructor('Event', _Eventfull, { + toString: function(){ + return this.orig_func ? + 'Event ' + +this.orig_func.toString() + .replace(/^(function[^(]*\()[^,)]*, ?/, '$1') + : `Event function ${this.name}(){}`}, + __init__: function(name, func, options={}){ + var that = this + this.orig_func = func + object.parentCall(_Event.prototype.__init__, this, + name, + function(handle, ...args){ + // NOTE: when the first arg is an event command this will + // fall through to calling the action... + typeof(args[0]) == 'function' ? + // add handler... + that.bind(this, args[0]) + // call the action... + : (func + && func.call(this, handle, ...args)) + return this }, + options) } +}) + + +var _PureEvent = +module._PureEvent = +object.Constructor('PureEvent', _Event, { + // XXX +}) + + + // Create an "eventfull" method... // // The resulting method can be either called directly or via .trigger(..). @@ -87,11 +227,9 @@ var Eventfull = module.Eventfull = function(name, func, options={}){ var hidden - options = func && typeof(func) != 'function' ? func : options - var method = object.mixin( function(...args){ var handlers = @@ -101,8 +239,11 @@ function(name, func, options={}){ // function... : options.handlerLocation == 'method' ? (method.__event_handlers__ || []) - // context (default)... - : ((this.__event_handlers__ || {})[name] || []) + : [] + // context (default)... + // NOTE: these are allways called... + handlers = handlers + .concat((this.__event_handlers__ || {})[name] || []) // NOTE: this will stop event handling if one of the handlers // explicitly returns false... @@ -187,8 +328,6 @@ function(name, func, options={}){ -module.TRIGGER = module.EventCommand('TRIGGER') - // Extends Eventfull(..) adding ability to bind events via the // resulting method directly by passing it a function... // @@ -259,8 +398,8 @@ function(name, func, options={}){ }) } -// Like Event(..) but produces an event that can only be triggered via -// .trigger(name, ...), calling this is a no-op... +// Like Event(..) but produces an event method that can only be triggered +// via .trigger(name, ...), calling this is a no-op... var PureEvent = module.PureEvent = function(name, options={}){ @@ -334,14 +473,14 @@ module.EventHandlerMixin = object.Mixin('EventHandlerMixin', { return h !== func && h.__event_original_handler__ !== func })) } return this }, + // XXX revise... trigger: function(evt, ...args){ - // local handler... - evt in this - && this[evt](module.TRIGGER, ...args) - // global events... - this.__event_handlers__ - && (this.__event_handlers__[evt] || []) - .forEach(function(h){ h(evt, ...args) }) + evt in this ? + // XXX add a better check... + this[evt](module.TRIGGER, ...args) + : this.__event_handlers__ + && (this.__event_handlers__[evt] || []) + .forEach(function(h){ h(evt, ...args) }) return this }, }) diff --git a/package.json b/package.json index 6516152..4117c6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ig-types", - "version": "5.0.22", + "version": "5.0.23", "description": "Generic JavaScript types and type extensions...", "main": "main.js", "scripts": { diff --git a/runner.js b/runner.js index 3f9ebee..1fb1c6e 100644 --- a/runner.js +++ b/runner.js @@ -44,6 +44,15 @@ module.STOP = object.STOP // release their spot in the pool. // // XXX need to configure to run a specific amount of jobs on each start... +// XXX triggering .tasksAdded(..)... +// there are two ways to go here: +// - wrap the queue in a proxy +// + transparent-ish +// - reported in a confusing way by node/chrome... +// - force the user to use specific API +// - not consistent -- a task can be added via .add(..) and +// the Array interface and only .add(..) will trigger the +// proper events... var Queue = module.Queue = object.Constructor('Queue', Array, { @@ -113,10 +122,12 @@ object.Constructor('Queue', Array, { // events... // + // .tasksAdded(func(evt, [task, ..])) // .taskStarting(func(evt, task)) // .taskCompleted(func(evt, task)) // .queueEmpty(func(evt)) // + tasksAdded: events.PureEvent('tasksAdded'), taskStarting: events.PureEvent('taskStarting'), taskCompleted: events.PureEvent('taskCompleted'), queueEmpty: events.PureEvent('queueEmpty'), @@ -124,22 +135,13 @@ object.Constructor('Queue', Array, { // NOTE: each handler will get called once when the next time the // queue is emptied... + // XXX should this trigger on empty or on stop??? then: function(func){ var that = this return new Promise(function(resolve, reject){ that.one('queueEmpty', function(){ resolve(func()) }) }) }, - - // helpers... - // - // move tasks to head/tail of queue resp. - prioritize: function(...tasks){ - return this.sortAs(tasks) }, - delay: function(...tasks){ - return this.sortAs(tasks, true) }, - - // Runner API... // // Run the given task type... @@ -210,9 +212,7 @@ object.Constructor('Queue', Array, { run() : setTimeout(run, 0)) return this }, - // run one task from queue... - // // NOTE: this does not care about .state... runTask: function(next){ var that = this @@ -246,8 +246,6 @@ object.Constructor('Queue', Array, { res = res instanceof module.STOP ? res.value : res - stop - && this.stop() // handle task results... // @@ -277,8 +275,7 @@ object.Constructor('Queue', Array, { // one post handler is enough... && !running.includes(res)){ running.push(res) - res.finally(function(){ - runningDone() }) + res.finally(runningDone) // re-queue tasks... } else if(typeof(res) == 'function'){ @@ -292,9 +289,64 @@ object.Constructor('Queue', Array, { this.length == 0 && this.trigger('queueEmpty') + stop + && this.stop() + return this }, + // helpers... + // + // move tasks to head/tail of queue resp. + prioritize: function(...tasks){ + return this.sortAs(tasks) }, + delay: function(...tasks){ + return this.sortAs(tasks, true) }, + + + // trigger .tasksAdded(..)... + // + // NOTE: adding tasks via the [..] notation will not trigger the + // event... + push: function(...tasks){ + res = object.parentCall(Queue.prototype.push, this, ...tasks) + this.trigger('tasksAdded', tasks) + return res }, + unsift: function(...tasks){ + res = object.parentCall(Queue.prototype.unshift, this, ...tasks) + this.trigger('tasksAdded', tasks) + return res }, + splice: function(...args){ + res = object.parentCall(Queue.prototype.splice, this, ...args) + var tasks = args.slise(2) + tasks.length > 0 + && this.trigger('tasksAdded', tasks) + return res }, + // convenience... + add: function(...tasks){ + this.push(...tasks) + return this }, + + + /*/ trigger .tasksAdded(..) when stuff is added to queue... + // XXX EXPERIMENTAL... + // XXX this messes up how devtools/node print this object... + // ...everything works as expected but looks ugly... + // XXX another potential issue here is the opposite of the overload + // approach, this is too strong and we will need to go around + // it if we need for instance to shift around around... + __new__: function(context, ...args){ + return new Proxy( + Reflect.construct(Queue.__proto__, args, Queue), + { + set: function(queue, prop, value, receiver){ + parseInt(prop) == prop + && queue.trigger('tasksAdded', [value]) + return Reflect.set(...arguments) }, + }) }, + //*/ + + // constructor argument handling... // // Queue() @@ -314,6 +366,8 @@ object.Constructor('Queue', Array, { && typeof(this[0]) != 'function' && typeof(this[0].finally) != 'function'){ Object.assign(this, this.shift()) } + this.length > 0 + && this.trigger('tasksAdded', [...this]) // see if we need to start... this.__run_tasks__() }, }))