diff --git a/asynctimeout.js b/asynctimeout.js index dab865c..1024d29 100644 --- a/asynctimeout.js +++ b/asynctimeout.js @@ -1,3 +1,13 @@ + +const is_promise = obj => !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; + +const fire_and_forget = (a, on_error) => { + (async () => { + try { + await a(); + } catch(e) { if(on_error) on_error(e); else throw(e); } + })(); +}; /// An async wrapper around `setTimeout()` and `setInterval()`. /// /// # `setTimeout()` @@ -15,43 +25,128 @@ /// See `examples` below. function AsyncTimeout(thing, interval) { - function isPromise(obj) { - return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; - } - - async function _call(t) - { - if(isPromise(t)) return await t(arguments); - else await (async () => {}); - return t(arguments); - } - this.timeout = () => new Promise((resolve, reject) => { setTimeout(() => { - try { - _call(thing).then(resolve); //XXX: This design doesn't work, we'll need to go back to the drawing board with this function for this to work... The exception won't propagate here, so... - } catch(e) { reject(e); } + const eval_thing = () => { + try { + return thing(); + } catch(e) { reject(e); } + }; + const rv = eval_thing(); + if(is_promise(rv)) fire_and_forget(async () => resolve(await rv), reject); + else resolve(rv); }, interval); }); this.interval = async function*() { while(true) { yield await new Promise((resolve, reject) => { + const eval_thing = () => { + try { + return thing(); + } catch(e) { reject(e); } + }; setTimeout(() => { - try { _call(thing).then(resolve); } catch(e) { reject(e); } + const irv = eval_thing(); + if(is_promise(irv)) fire_and_forget(async() => resolve(await irv), reject); + else resolve(irv); }, interval); }); } }; } -const example = async (_timeout) => { +/// Return a promise that will resolve when the timeout `i` has completed. There is no value for this resolution. +AsyncTimeout.await_timeout = (i) => new AsyncTimeout(() => {}, i).timeout(); +/// Return an async iterator that will resolve the next iteration every `i`. There is no value for these yielded resolutions. +AsyncTimeout.await_interval = (i) => new AsyncTimeout(() => {}, i).interval(); + +/// Cancellation token source for `timeout_example` +function Cancellation() { + let tokens = []; + + this.cancel_after_interrupt = (sig) => new Promise(resolve => { + sig = sig || 'SIGINT'; + process.on(sig, () => { + //console.log(`>>> Received signal ${sig}`); + resolve(sig || ""); + }); + }); + + + this.cancel_after_invoke = () => new Promise(resolve => { + tokens.push(resolve); + }); + + this.cancel_invoke = (value) => tokens.shift()(value || ""); +} + +/// Run the timeout example +const timeout_example = async (_timeout, cancelAfter) => { _timeout = _timeout || 100; + let cancel = false; + + const canceller = new Promise(resolve => { + if(is_promise(cancelAfter)) (async () => { + resolve(cancel = ((await cancelAfter) || true)); + })(); + else if(cancelAfter && cancelAfter > 0) setTimeout(() => resolve(cancel = true), cancelAfter); + else resolve(false); + }); - // Wait 100 then alert "hi" + // Wait _timeout then alert "hi" console.log(await (new AsyncTimeout(() => "hi", _timeout)).timeout()); - // Continuously wait 100 then alert "hi" - for await (const value of new AsyncTimeout(() => "hi forever", _timeout).interval()) { console.log(value); } + // Wait _timeout then call then await this async lambda, then print its result. + console.log(await (new AsyncTimeout(async () => { + console.log("> Hi starting promise..."); + await AsyncTimeout.await_timeout(_timeout); + console.log(">> Hi inside promise!"); + await AsyncTimeout.await_timeout(_timeout); + console.log("> Hi ending promise..."); + return "hi, from async lambda!"; + }, _timeout)).timeout()); + + // Wait _timeout as with this async lambda which uses `.interval()` producing the result `(_timeout / 10) || 10` times. + console.log(await (new AsyncTimeout(async () => { + let i=0; + const len = (_timeout / 10) || 10; + for await (const value of AsyncTimeout.await_interval(_timeout)) { + console.log(`[al]: iteration ${i}`); + i+=1; + if(cancel) throw "Operation cancelled."; + else if(i == len) return `Hi from async lambda containing iterator!, Completed ${i} iterations`; + } + }, _timeout)).timeout()); + + // Wait _timeout as interval running at most `(_interval / 10) || 10` times + console.log(await (async () => { + let i=0; + const len = (_timeout / 10) || 10; + for await (const value of new AsyncTimeout(async () => { + console.log(`i > Waiting ${_timeout * 2}`); + await AsyncTimeout.await_timeout(_timeout * 2); + console.log(`i > Returning ${i} as iteration value.`); + return `${i}`; + }, _timeout).interval()) { + console.log(`!i >>> Iteration ${i}`); + i += 1; + if(cancel) throw "Operation cancelled."; + else if(i == len) return value; + } + })()); + + // Continuously wait _timeout then alert "hi forever", until `cancelAfter` is reached (if it is) + for await (const value of new AsyncTimeout(() => "hi forever", _timeout).interval()) { if(cancel) break; console.log(value); } + + // Return `true` if cancelled (or, if `cancelAfter` was a promise that returned a truthy value, that resolved value); `false` if not (should never happen) + return await canceller; }; +/// Run the timeout example, catching an error if there is one thrown (usually on cancellation.) +const try_example = async (time) => { + try { return { value: await timeout_example(null, time) }; } + catch(e) { console.log(`Error! ${e}`); return {error: e}; } +}; +//try_example(2500); // Error! Operation cancelled. -> `{ error: "Operation cancelled." }` +//try_example(5000); // -> `{ value: true }`