reworking event.js to be more extensible...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2020-12-01 05:48:50 +03:00
parent 6e39dc9d3b
commit f8e3d94824
3 changed files with 225 additions and 32 deletions

169
event.js
View File

@ -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 },
})

View File

@ -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": {

View File

@ -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__() },
}))