diff --git a/Promise.js b/Promise.js index 77f8b0e..81dad1e 100644 --- a/Promise.js +++ b/Promise.js @@ -13,7 +13,6 @@ var object = require('ig-object') /*********************************************************************/ -// XXX should this be aborted on reject??? var IterablePromise = module.IterablePromise = object.Constructor('IterablePromise', Promise, { @@ -95,10 +94,10 @@ object.Constructor('IterablePromise', Promise, { // // NOTE: .catch(..) and .finally(..) are implemented through .then(..) // so we do not need to overload those... - then: function(onfulfilled, onrejected){ + then: function (onfulfilled, onrejected){ return new Promise( function(resolve, reject){ - object.parentCall(IterablePromise.prototype.then, this, + Promise.prototype.then.call(this, // NOTE: resolve(..) / reject(..) return undefined so // we can't pass them directly here... function(res){ @@ -108,7 +107,7 @@ object.Constructor('IterablePromise', Promise, { reject(res) return res }) }.bind(this)) .then(...arguments) }, - + // // Promise.iter([ .. ]) @@ -145,8 +144,9 @@ object.Constructor('IterablePromise', Promise, { var promise // instance... - var obj = Reflect.construct(IterablePromise.__proto__, [ - function(resolve, reject){ + var obj = Reflect.construct( + IterablePromise.__proto__, + [function(resolve, reject){ // NOTE: this is here for Promise compatibilty... if(typeof(list) == 'function'){ return list.call(this, ...arguments) } @@ -202,48 +202,112 @@ object.Constructor('IterablePromise', Promise, { +//--------------------------------------------------------------------- + +var InteractivePromise = +module.InteractivePromise = +object.Constructor('InteractivePromise', Promise, { + __message_handlers: null, + + send: function(...args){ + var that = this + ;(this.__message_handlers || []) + .forEach(function(h){ h.call(that, ...args) }) + return this }, + + then: IterablePromise.prototype.then, + + // + // Promise.interactive(handler) + // -> interacive-promise + // + // handler(resolve, reject, onmessage) + // + // onmessage(func) + // + // + __new__: function(_, handler){ + var handlers = [] + var onmessage = function(func){ + handlers.push(func) } + + var obj = Reflect.construct( + InteractivePromise.__proto__, + !handler ? + [] + : [function(resolve, reject){ + return handler(resolve, reject, onmessage) }], + InteractivePromise) + + Object.defineProperty(obj, '__message_handlers', { + value: handlers, + enumerable: false, + }) + return obj }, +}) + + + +//--------------------------------------------------------------------- + +var CooperativePromise = +module.CooperativePromise = +object.Constructor('CooperativePromise', Promise, { + __handlers: null, + + get isSet(){ + return this.__handlers === false }, + + set: function(value, resolve=true){ + // can't set twice... + if(this.isSet){ + throw new Error('.set(..): can not set twice') } + // bind to promise... + if(value && value.then && value.catch){ + value.then(handlers.resolve) + value.catch(handlers.reject) + // resolve with value... + } else { + resolve ? + this.__handlers.resolve(value) + : this.__handlers.reject(value) } + // cleanup and prevent setting twice... + this.__handlers = false + return this }, + + then: IterablePromise.prototype.then, + + __new__: function(){ + var handlers + var resolver = arguments[1] + + var obj = Reflect.construct( + CooperativePromise.__proto__, + [function(resolve, reject){ + handlers = {resolve, reject} + // NOTE: this is here to support builtin .then(..) + resolver + && resolver(resolve, reject) }], + CooperativePromise) + + Object.defineProperty(obj, '__handlers', { + value: handlers, + enumerable: false, + writable: true, + }) + return obj }, +}) + + + //--------------------------------------------------------------------- var PromiseMixin = module.PromiseMixin = object.Mixin('PromiseMixin', 'soft', { - // XXX does this need to be a distinct object/constructor??? - cooperative: function(){ - var handlers - return object.mixinFlat( - new Promise(function(resolve, reject){ - handlers = { resolve, reject, } }), - { - get isSet(){ - return handlers === false }, - // - // Resolve promise with value... - // .set(value) - // -> this - // - // Reject promise with value... - // .set(value, false) - // -> this - // - set: function(value, resolve=true){ - // can't set twice... - if(this.isSet){ - throw new Error('Promise.cooperative().set(..): can not set twice') } - // bind to promise... - if(value && value.then && value.catch){ - value.then(handlers.resolve) - value.catch(handlers.reject) - // resolve with value... - } else { - resolve ? - handlers.resolve(value) - : handlers.reject(value) } - // cleanup and prevent setting twice... - handlers = false - return this }, - }) }, - iter: IterablePromise, + interactive: InteractivePromise, + cooperative: CooperativePromise, }) diff --git a/event.js b/event.js index 2100455..fcaf790 100644 --- a/event.js +++ b/event.js @@ -46,14 +46,13 @@ var Eventfull = module.Eventfull = function(name, func, options={}){ var hidden - var method options = func && typeof(func) != 'function' ? func : options - return object.mixinFlat( - method = function(...args){ + var method = object.mixin( + function(...args){ var handlers = // hidden... options.handlerLocation == 'hidden' ? @@ -107,8 +106,14 @@ function(name, func, options={}){ : options.handlerLocation == 'method' ? (method.__event_handlers__ = method.__event_handlers__ || []) // context (default)... - : ((context.__event_handlers__ = context.__event_handlers__ || {})[name] = - context.__event_handlers__[name] || []) + : (context.__event_handlers__ == null ? + Object.defineProperty(context, '__event_handlers__', { + value: {[name]: (handlers = [])}, + enumerable: false, + }) + && handlers + : (context.__event_handlers__[name] = + context.__event_handlers__[name] || [])) // add handler... handlers.push(func) return this }, @@ -128,7 +133,13 @@ function(name, func, options={}){ toString: function(){ return func.toString() .replace(/^(function[^(]*\()[^,)]*, ?/, '$1') }, - }) } + }) + + Object.defineProperty(method, 'name', { + value: name, + }) + + return method } module.TRIGGER = {doc: 'force event method to trigger'} @@ -169,6 +180,8 @@ module.TRIGGER = {doc: 'force event method to trigger'} // arg is a function or not... // // +// XXX might be a good idea to adde an event that can't be triggered by +// calling... var Event = module.Event = function(name, func, options={}){ @@ -177,7 +190,8 @@ function(name, func, options={}){ func : options - return Object.assign( + //return Object.assign( + return object.mixin( method = Eventfull(name, function(handle, ...args){ // add handler... @@ -199,8 +213,10 @@ function(name, func, options={}){ // NOTE: this is a copy of Eventfull's .toString() as we // still need to base the doc on the user's func... toString: function(){ - return func.toString() - .replace(/^(function[^(]*\()[^,)]*, ?/, '$1') }, + return func ? + func.toString() + .replace(/^(function[^(]*\()[^,)]*, ?/, '$1') + : `function ${name}(){}`}, }) } @@ -213,7 +229,7 @@ function(name, func, options={}){ // XXX do we need to be able to force global handler??? var EventHandlerMixin = module.EventHandlerMixin = object.Mixin('EventHandlerMixin', { - __event_handlers__: null, + //__event_handlers__: null, on: function(evt, func){ // event... @@ -222,7 +238,12 @@ module.EventHandlerMixin = object.Mixin('EventHandlerMixin', { this[evt].__event_handler_add__(this, func) // non-event... } else { - ;((this.__event_handlers__ = this.__event_handlers__ || {})[evt] = + this.__event_handlers__ == null + && Object.defineProperty(this, '__event_handlers__', { + value: {}, + enumerable: false, + }) + ;(this.__event_handlers__[evt] = this.__event_handlers__[evt] || []) .push(func) } return this }, diff --git a/package-lock.json b/package-lock.json index 9571a84..7989c6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ig-types", - "version": "3.7.14", + "version": "5.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -265,9 +265,9 @@ } }, "ig-object": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/ig-object/-/ig-object-5.4.11.tgz", - "integrity": "sha512-WPPQ5C41c6q3tPfa2fBbWE2xcLF7LoGRu2E6Wr/aoA5oxAyl8lAuE7Kqt4TyPwfW9jVI0+ifBztg9e1tR5mG1Q==" + "version": "5.4.12", + "resolved": "https://registry.npmjs.org/ig-object/-/ig-object-5.4.12.tgz", + "integrity": "sha512-9kZM80Js9/eTwXN9VXwLDC1wDJ7gIAdYU9GIzb5KJmNcLAMaW+zhgFrwFFMrcSfggUuadgnqSrS41E4XLe8JZw==" }, "ig-test": { "version": "1.4.8", diff --git a/package.json b/package.json index f82cdd1..8287353 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ig-types", - "version": "5.0.0", + "version": "5.0.2", "description": "Generic JavaScript types and type extensions...", "main": "main.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/flynx/types.js#readme", "dependencies": { - "ig-object": "^5.4.11", + "ig-object": "^5.4.12", "object-run": "^1.0.1" }, "devDependencies": { diff --git a/runner.js b/runner.js index 4e2bbff..74dd9f9 100644 --- a/runner.js +++ b/runner.js @@ -21,6 +21,7 @@ var object = require('ig-object') require('./Array') +require('./Promise') var events = require('./event') @@ -38,7 +39,8 @@ module.STOP = object.STOP // XXX do we need an async mode -- exec .__run_tasks__(..) in a // setTimeout(.., 0)??? var Queue = -module.Queue = object.Constructor('Queue', Array, { +module.Queue = +object.Constructor('Queue', Array, { // create a running queue... runTasks: function(...tasks){ return this({ state: 'running' }, ...tasks) }, @@ -106,6 +108,16 @@ module.Queue = object.Constructor('Queue', Array, { queueEmpty: events.Event('queueEmpty'), + // NOTE: each handler will get called once when the next time the + // queue is emptied... + // XXX revise... + 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. @@ -142,7 +154,7 @@ module.Queue = object.Constructor('Queue', Array, { task.start() : task }, // - // Hanlde 'running' state... + // Hanlde 'running' state (async)... // .__run_tasks__() // -> this // @@ -152,36 +164,33 @@ module.Queue = object.Constructor('Queue', Array, { __running: null, __run_tasks__: function(){ var that = this + this.state == 'running' + && setTimeout(function(){ + // handle queue... + while(this.length > 0 + && this.state == 'running' + && (this.__running || []).length < (this.pool_size || Infinity) ){ + this.runTask(this.__run_tasks__.bind(this)) } - // if we are not running stop immidiately... - if(this.state != 'running'){ - return this } - - // handle queue... - while(this.length > 0 - && this.state == 'running' - && (this.__running || []).length < (this.pool_size || Infinity) ){ - this.runTask(this.__run_tasks__.bind(this)) } - - // empty queue -> pole or stop... - // - // NOTE: we endup here in two cases: - // - the pool is full - // - the queue is empty - // NOTE: we do not care about stopping the timer when changing - // state as .__run_tasks__() will stop itself... - // - // XXX will this be collected by the GC if it is polling??? - if(this.length == 0 - && this.state == 'running'){ - this.auto_stop ? - // auto-stop... - this.stop() - // pole... - : (this.poling_delay - && setTimeout( - this.__run_tasks__.bind(this), - this.poling_delay || 200)) } + // empty queue -> pole or stop... + // + // NOTE: we endup here in two cases: + // - the pool is full + // - the queue is empty + // NOTE: we do not care about stopping the timer when changing + // state as .__run_tasks__() will stop itself... + // + // XXX will this be collected by the GC if it is polling??? + if(this.length == 0 + && this.state == 'running'){ + this.auto_stop ? + // auto-stop... + this.stop() + // pole... + : (this.poling_delay + && setTimeout( + this.__run_tasks__.bind(this), + this.poling_delay || 200)) } }.bind(this), 0) return this }, // run one task from queue... @@ -291,6 +300,86 @@ module.Queue = object.Constructor('Queue', Array, { })) +//--------------------------------------------------------------------- +// Task manager... +// +// goal: +// externally manage long running functions/promises/etc +// +// enteties: +// task +// - wrap a function/promise +// - pass the function/promise a reciver +// - return a controller (store in manager) +// manager +// - container for tasks +// - multiplex actions to tasks +// + + +var TaskMixin = +object.Mixin('TaskMixin', 'soft', { + stop: function(){ + this.send('stop', ...arguments) }, +}) + + +// XXX should this be a Queue??? +var TaskManager = +module.TaskManager = +object.Constructor('TaskManager', Array, events.EventMixin('flat', { + + // XXX each task should also trigger this when stopping and this + // should not result in this and tasks infinitely playing + // ping-pong... + stop: events.Event('stop', + function(task='all'){ + this.forEach(function(task){ + ;(task == 'all' + || task == '*' + || task === task) + && task.stop() }) }), + done: events.Event('done'), + + + Task: function(task, ...args){ + var that = this + + // normalize handler... + var handler = + // queue... + // NOTE: queue is task-compatible... + task instanceof Queue ? + task.start() + // interactive... + : task && task.then && task.stop ? + task + : TaskMixin( + // dumb promise -- will ignore all the messages... + // XXX should we complain about this??? + task instanceof Promise ? + Promise.interactive( + function(resolve, reject, onmsg){ + task.then(resolve, reject) }) + // function... + : Promise.interactive( + function(resolve, reject, onmsg){ + resolve(task(onmsg, ...args)) })) + + this.push(handler) + + // handle task done... + handler + .then(function(res){ + that.splice(that.indexOf(handler), 1) + that.trigger('done', task, res) + that.length == 0 + && that.done('all') }) + + // XXX or should we return this??? + return handler }, +})) +