/********************************************************************** * * This defines the following extensions to Promise: * * Promise.iter(seq) * .iter() * Iterable promise object. * Similar to Promise.all(..) but adds basic iterator/generator * API and will resolve the items as they are ready (resolved). * * Promise.interactive(handler) * Interactive promise object. * This adds a basic message passing API to the promise. * * Promise.cooperative() * Cooperative promise object. * Exposes the API to resolve/reject the promise object * externally. * * .as(obj) * Promise proxy. * Proxies the methods available from obj to promise value. * * * * **********************************************/ /* c8 ignore next 2 */ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var object = require('ig-object') //--------------------------------------------------------------------- // Iterable promise... // // Like Promise.all(..) but adds ability to iterate through results // via generators .map(..)/.reduce(..) and friends... // // NOTE: it would be nice to support throwing STOP (XXX) from the iterable // promise but... // - this is more complicated than simply using .smap(..) instead // of .map(..) because the list can contain async promises... // ...would need to wrap each .then(..) call in try-catch and // manually handle the stop... // ...then there's a question of derivative iterators etc. // - another issue here is that the stop would happen in order of // execution and not order of elements... // XXX though stopping within a single level might be useful... // NOTE: the following can not be implemented here: // .splice(..) - can't both modify and return // a result... // .pop() / .shift() - can't modify the promise, use // .first() / .last() instead. // [Symbol.iterator]() - needs to be sync and we can't // know the number of elements to // return promises before the whole // iterable promise is resolved. // NOTE: we are not using async/await here as we need to control the // type of promise returned in cases where we know we are returning // an array... // NOTE: there is no point in implementing a 1:1 version of this that // would not support element expansion/contraction as it would only // simplify a couple of methods that are 1:1 (like .map(..) and // .some(..)) while methods like .filter(..) will throw everything // back to the complex IterablePromise... // // XXX how do we handle errors/rejections??? // // XXX should these be exported??? var iterPromiseProxy = //module.iterPromiseProxy = function(name){ return function(...args){ return this.constructor( this.then(function(lst){ return lst[name](...args) })) } } var promiseProxy = //module.promiseProxy = function(name){ return async function(...args){ return (await this)[name](...args) } } var IterablePromise = module.IterablePromise = object.Constructor('IterablePromise', Promise, { // packed array... // // Holds promise state. // // Format: // [ // , // [ ], // , // ... // ] // // This format has several useful features: // - concatenating packed lists results in a packed list // - adding an iterable promise (as-is) into a packed list results // in a packed list // // NOTE: in general iterable promises are implicitly immutable, so // it is not recomended to ever edit this in-place... // NOTE: we are not isolating or "protecting" any internals to // enable users to responsibly extend the code. __packed: null, // low-level .__packed handlers/helpers... // // NOTE: these can be useful for debugging and extending... // // pack and oprionally transform/handle an array (sync)... __pack: function(list, handler=undefined){ var that = this // handle iterable promise list... if(list instanceof IterablePromise){ return this.__handle(list.__packed, handler) } // handle promise list... if(list instanceof Promise){ return list.then(function(list){ return that.__pack(list, handler) }) } // do the work... // NOTE: packing and handling are mixed here because it's faster // to do them both on a single list traverse... var handle = !!handler handler = handler ?? function(elem){ return [elem] } // XXX EXPEREMENTAL... var stoppable = !!Array.STOP var stop = false var map = stoppable ? 'smap' : 'map' var _handler = handler handler = !handler.stoppable ? Object.assign( function(){ try{ console.log('####', stop) return !stop ? _handler(...arguments) : [] }catch(err){ stop = err if(err === Array.STOP || err instanceof Array.STOP){ return 'value' in err ? err.value : [] } throw err } }, // prevent double+ wrapping... // XXX revise nameing... { stoppable: true }) : handler return [list].flat() // XXX supporting STOP here would require both using .smap(..) // and tracking STOPs thrown in promises -- this could // throw things out of sync for us not to be able to // guarantee a full stop if a throw happened in a delayed // handler effectively making the handlers race for the // throw... // ...not sure if this is good or not... //.map(function(elem){ [map](function(elem){ return elem && elem.then ? //that.__pack(elem, handler) //elem.then(handler) elem .then(handler) .catch(function(err){ stop = err if(err === Array.STOP || err instanceof Array.STOP){ return 'value' in err ? err.value : [] } throw err }) : elem instanceof Array ? handler(elem) // NOTE: we keep things that do not need protecting // from .flat() as-is... : !handle ? elem : handler(elem) }) }, // transform/handle packed array (sync)... __handle: function(list, handler=undefined){ var that = this if(typeof(list) == 'function'){ handler = list list = this.__packed } if(!handler){ return list } // handle promise list... if(list instanceof Promise){ return list.then(function(list){ return that.__handle(list, handler) }) } // do the work... // NOTE: since each section of the packed .__array is the same // structure as the input we'll use .__pack(..) to handle // them, this also keeps all the handling code in one place. // XXX EXPEREMENTAL... var map = !!Array.STOP ? 'smap' : 'map' //return list.map(function(elem){ return list[map](function(elem){ return elem instanceof Array ? that.__pack(elem, handler) : elem instanceof Promise ? that.__pack(elem, handler) .then(function(elem){ return elem.flat() }) : [handler(elem)] }) .flat() }, // unpack array (async)... __unpack: async function(list){ list = list ?? this.__packed // handle promise list... return list instanceof Promise ? this.__unpack(await list) // do the work... : (await Promise.all(list)) .flat() }, // iterator methods... // // These will return a new IterablePromise instance... // // NOTE: these are different to Array's equivalents in that the handler // is called not in the order of the elements but rather in order // of promise resolution... // NOTE: index of items is unknowable because items can expand and // contract depending on handlers (e.g. .filter(..) can remove // items)... map: function(func){ return this.constructor(this, function(e){ var res = func(e) return res instanceof Promise ? res.then(function(e){ return [e] }) : [res] }) }, filter: function(func){ return this.constructor(this, function(e){ var res = func(e) var _filter = function(elem){ return res ? [elem] : [] } return res instanceof Promise ? res.then(_filter) : _filter(e) }) }, // NOTE: this does not return an iterable promise as we can't know // what the user reduces to... // NOTE: the items can be handled out of order because the nested // promises can resolve in any order... // NOTE: since order of execution can not be guaranteed there is no // point in implementing .reduceRight(..) in the same way // (see below)... reduce: function(func, res){ return this.constructor(this, function(e){ res = func(res, e) return [] }) .then(function(){ return res }) }, flat: function(depth=1){ return this.constructor(this, function(e){ return (depth > 1 && e != null && e.flat) ? e.flat(depth-1) : depth != 0 ? e : [e] }) }, reverse: function(){ var lst = this.__packed return this.constructor( lst instanceof Promise ? lst.then(function(elems){ return elems instanceof Array ? elems.slice() .reverse() : elems }) : lst .map(function(elems){ return elems instanceof Array ? elems.slice() .reverse() : elems instanceof Promise ? elems.then(function(elems){ return elems.reverse() }) : elems }) .reverse(), 'raw') }, // NOTE: the following methods can create an unresolved promise from // a resolved promise... concat: function(other){ var that = this var cur = this.__pack(this) var other = this.__pack(other) return this.constructor( // NOTE: we need to keep things as exposed as possible, this // is why we're not blanketing all the cases with // Promise.all(..)... (cur instanceof Promise && other instanceof Promise) ? [cur, other] : cur instanceof Promise ? [cur, ...other] : other instanceof Promise ? [...cur, other] : [...cur, ...other], 'raw') }, push: function(elem){ return this.concat([elem]) }, unshift: function(elem){ return this.constructor([elem]) .concat(this) }, // XXX thses need to stop once an element is found so we cant simply // use .map(..) or .reduce(..)... // XXX .find(func) / .findIndex(func) // proxy methods... // // These require the whole promise to resolve to trigger. // // An exception to this would be .at(0)/.first() and .at(-1)/.last() // that can get the target element if it's accessible. // // NOTE: methods that are guaranteed to return an array will return // an iterable promise (created with iterPromiseProxy(..))... // at: async function(i){ var list = this.__packed return ((i != 0 && i != -1) || list instanceof Promise || list.at(i) instanceof Promise) ? (await this).at(i) // NOTE: we can only reason about first/last explicit elements, // anything else is non-deterministic... : list.at(i) instanceof Promise ? [await list.at(i)].flat().at(i) : list.at(i) instanceof Array ? list.at(i).at(i) : list.at(i) }, first: function(){ return this.at(0) }, last: function(){ return this.at(-1) }, // NOTE: unlike .reduce(..) this needs the parent fully resolved // to be able to iterate from the end. // XXX ??? reduceRight: promiseProxy('reduceRight'), // NOTE: there is no way we can do a sync generator returning // promises for values because any promise in .__packed makes the // value count/index non-deterministic... sort: iterPromiseProxy('sort'), // XXX we could have a special-case here for .slice()/slice(0, -1) // and possibly othets, should we??? slice: iterPromiseProxy('slice'), entries: iterPromiseProxy('entries'), keys: iterPromiseProxy('keys'), // XXX we could possibly make this better via .map(..) values: iterPromiseProxy('values'), indexOf: promiseProxy('indexOf'), lastIndexOf: promiseProxy('lastIndexOf'), includes: promiseProxy('includes'), every: promiseProxy('every'), // XXX this can be lazy... (???) some: promiseProxy('some'), // promise api... // // Overload .then(..), .catch(..) and .finally(..) to return a plain // Promise instnace... // // NOTE: .catch(..) and .finally(..) are implemented through .then(..) // so we do not need to overload those... // NOTE: this is slightly different from .then(..) in that it can be // called without arguments and return a promise wrapper. This can // be useful to hide special promise functionality... then: function (onfulfilled, onrejected){ var p = new Promise( function(resolve, reject){ Promise.prototype.then.call(this, // NOTE: resolve(..) / reject(..) return undefined so // we can't pass them directly here... function(res){ resolve(res) return res }, function(res){ reject(res) return res }) }.bind(this)) return arguments.length > 0 ? p.then(...arguments) : p }, // this is defined globally as Promise.prototype.iter(.,) // // for details see: PromiseMixin(..) below... //iter: function(handler=undefined){ ... }, // constructor... // // Promise.iter([ .. ]) // -> iterable-promise // // Promise.iter([ .. ], handler) // -> iterable-promise // // // handler(e) // -> [value, ..] // -> [] // -> // // // NOTE: element index is unknowable until the full list is expanded // as handler(..)'s return value can expand to any number of // items... // XXX we can make the index a promise, then if the client needs // the value they can wait for it... // // // Special cases useful for extending this constructor... // // Set raw .__packed without any pre-processing... // Promise.iter([ .. ], 'raw') // -> iterable-promise // // Create a rejected iterator... // Promise.iter(false) // -> iterable-promise // __new__: function(_, list, handler){ // instance... var promise var obj = Reflect.construct( IterablePromise.__proto__, [function(resolve, reject){ // NOTE: this is here for Promise compatibility... if(typeof(list) == 'function'){ return list.call(this, ...arguments) } // initial reject... if(list === false){ return reject() } promise = {resolve, reject} }], IterablePromise) // populate new instance... if(promise){ // handle/pack input data... if(handler != 'raw'){ list = list instanceof IterablePromise ? this.__handle(list.__packed, handler) : this.__pack(list, handler) } Object.defineProperty(obj, '__packed', { value: list, enumerable: false, }) // handle promise state... this.__unpack(list) .then(function(list){ promise.resolve(list) }) .catch(promise.reject) } return obj }, }) //--------------------------------------------------------------------- // Interactive promise... // // Adds ability to send messages to the running promise. // var InteractivePromise = module.InteractivePromise = object.Constructor('InteractivePromise', Promise, { // XXX do we need a way to remove handlers??? __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){ // remove all handlers... if(func === false){ var h = (obj == null ? handlers : (obj.__message_handlers || [])) h.splice(0, handlers.length) // remove a specific handler... } else if(arguments[1] === false){ var h = (obj == null ? handlers : (obj.__message_handlers || [])) h.splice(h.indexOf(func), 1) // register a handler... } else { var h = obj == null ? // NOTE: we need to get the handlers from // .__message_handlers unless we are not // fully defined yet, then use the bootstrap // container (handlers)... // ...since we can call onmessage(..) while // the promise is still defined there is no // way to .send(..) until it returns a promise // object, this races here are highly unlikely... handlers : (obj.__message_handlers = obj.__message_handlers ?? []) 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, // XXX should this be .configurable??? configurable: true, }) return obj }, }) //--------------------------------------------------------------------- // Cooperative promise... // // A promise that can be resolved/rejected externally. // NOTE: normally this has no internal resolver logic... // 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 }, }) //--------------------------------------------------------------------- // XXX EXPEREMENTAL... var ProxyPromise = module.ProxyPromise = object.Constructor('ProxyPromise', Promise, { then: IterablePromise.prototype.then, __new__: function(context, other, nooverride=false){ var proto = 'prototype' in other ? other.prototype : other var obj = Reflect.construct( ProxyPromise.__proto__, [function(resolve, reject){ context.then(resolve) context.catch(reject) }], ProxyPromise) // populate... // NOTE: we are not using object.deepKeys(..) here as we need // the key origin not to trigger property getters... var seen = new Set() nooverride = nooverride instanceof Array ? new Set(nooverride) : nooverride while(proto != null){ Object.entries(Object.getOwnPropertyDescriptors(proto)) .forEach(function([key, value]){ // skip overloaded keys... if(seen.has(key)){ return } // skip non-functions... if(typeof(value.value) != 'function'){ return } // skip non-enumerable except for Object.prototype.run(..)... if(!(key == 'run' && Object.prototype.run === value.value) && !value.enumerable){ return } // do not override existing methods... if(nooverride === true ? key in obj : nooverride instanceof Set ? nooverride.has(key) : nooverride){ return } // proxy... obj[key] = promiseProxy(key) }) proto = proto.__proto__ } return obj }, }) //--------------------------------------------------------------------- var PromiseMixin = module.PromiseMixin = object.Mixin('PromiseMixin', 'soft', { iter: IterablePromise, interactive: InteractivePromise, cooperative: CooperativePromise, }) PromiseMixin(Promise) var PromiseProtoMixin = module.PromiseProtoMixin = object.Mixin('PromiseProtoMixin', 'soft', { as: ProxyPromise, iter: function(handler=undefined){ return IterablePromise(this, handler) }, }) PromiseProtoMixin(Promise.prototype) /********************************************************************** * vim:set ts=4 sw=4 nowrap : */ return module })