I need to create a JavaScript Promise that will not resolve until a specific condition is true. Let's say I have a 3rd party library, and I need to wait until a certain data condition exists within that library.
The scenario I am interested in is one where there is no way to know when this condition is satisfied other than by simply polling.
I can create a promise that waits on it - and this code works, but is there a better or more concise approach to this problem?
function ensureFooIsSet() {
return new Promise(function (resolve, reject) {
waitForFoo(resolve);
});
}
function waitForFoo(resolve) {
if (!lib.foo) {
setTimeout(waitForFoo.bind(this, resolve), 30);
} else {
resolve();
}
}
Usage:
ensureFooIsSet().then(function(){
...
});
I would normally implement a max poll time, but didn't want that to cloud the issue here.
A small variation would be to use a named IIFE so that your code is a little more concise and avoids polluting the external scope:
function ensureFooIsSet() {
return new Promise(function (resolve, reject) {
(function waitForFoo(){
if (lib.foo) return resolve();
setTimeout(waitForFoo, 30);
})();
});
}
Here's a waitFor function that I use quite a bit. You pass it a function, and it checks and waits until the function returns a truthy value, or until it times out.
This is a simple version which illustrates what the function does, but you might want to use the full version, added further in the answer
let sleep = ms => new Promise(r => setTimeout(r, ms));
let waitFor = async function waitFor(f){
while(!f()) await sleep(1000);
return f();
};
Example usages:
wait for an element to exist, then assign it to a variable
let bed = await waitFor(() => document.getElementById('bedId'))
if(!bed) doSomeErrorHandling();
wait for a variable to be truthy
await waitFor(() => el.loaded)
wait for some test to be true
await waitFor(() => video.currentTime > 21)
add a specific timeout to stop waiting
await waitFor(() => video.currentTime > 21, 60*1000)
pass it some other test function
if(await waitFor(someTest)) console.log('test passed')
else console.log("test didn't pass after 20 seconds")
Full Version:
This version takes cares of more cases than the simple version, null, undefined, empty array, etc., has a timeout, a frequency can be passed as an argument, and logs to the console what it is doing with some nice colors
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms));}
/**
* Waits for the test function to return a truthy value
* example usage:
* wait for an element to exist, then save it to a variable
* let el = await waitFor(() => document.querySelector('#el_id')))
* timeout_ms and frequency are optional parameters
*/
async function waitFor(test, timeout_ms = 20 * 1000, frequency = 200) {
if (typeof (test) != "function") throw new Error("test should be a function in waitFor(test, [timeout_ms], [frequency])")
if (typeof (timeout_ms) != "number") throw new Error("timeout argument should be a number in waitFor(test, [timeout_ms], [frequency])");
if (typeof (frequency) != "number") throw new Error("frequency argument should be a number in waitFor(test, [timeout_ms], [frequency])");
let logPassed = () => console.log('Passed: ', test);
let logTimedout = () => console.log('%c' + 'Timeout : ' + test, 'color:#cc2900');
let last = Date.now();
let logWaiting = () => {
if(Date.now() - last > 1000) {
last = Date.now();
console.log('%c' + 'waiting for: ' + test, 'color:#809fff');
}
}
let endTime = Date.now() + timeout_ms;
let isNotTruthy = (val) => val === undefined || val === false || val === null || val.length === 0; // for non arrays, length is undefined, so != 0
let result = test();
while (isNotTruthy(result)) {
if (Date.now() > endTime) {
logTimedout();
return false;
}
logWaiting();
await sleep(frequency);
result = test();
}
logPassed();
return result;
}
Is there a more concise approach to this problem?
Well, with that waitForFoo function you don't need an anonymous function in your constructor at all:
function ensureFooIsSet() {
return new Promise(waitForFoo);
}
To avoid polluting the scope, I would recommend to either wrap both in an IIFE or to move the waitForFoo function inside the ensureFooIsSet scope:
function ensureFooIsSet(timeout) {
var start = Date.now();
return new Promise(waitForFoo);
function waitForFoo(resolve, reject) {
if (window.lib && window.lib.foo)
resolve(window.lib.foo);
else if (timeout && (Date.now() - start) >= timeout)
reject(new Error("timeout"));
else
setTimeout(waitForFoo.bind(this, resolve, reject), 30);
}
}
Alternatively, to avoid the binding that is needed to pass around resolve and reject you could move it inside the Promise constructor callback like #DenysSéguret suggested.
Is there a better approach?
Like #BenjaminGruenbaum commented, you could watch the .foo property to be assigned, e.g. using a setter:
function waitFor(obj, prop, timeout, expected) {
if (!obj) return Promise.reject(new TypeError("waitFor expects an object"));
if (!expected) expected = Boolean;
var value = obj[prop];
if (expected(value)) return Promise.resolve(value);
return new Promise(function(resolve, reject) {
if (timeout)
timeout = setTimeout(function() {
Object.defineProperty(obj, prop, {value: value, writable:true});
reject(new Error("waitFor timed out"));
}, timeout);
Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
get: function() { return value; },
set: function(v) {
if (expected(v)) {
if (timeout) cancelTimeout(timeout);
Object.defineProperty(obj, prop, {value: v, writable:true});
resolve(v);
} else {
value = v;
}
}
});
});
// could be shortened a bit using "native" .finally and .timeout Promise methods
}
You can use it like waitFor(lib, "foo", 5000).
Here's a utility function using async/await and default ES6 promises. The promiseFunction is an async function (or just a function that returns a promise) that returns a truthy value if the requirement is fulfilled (example below).
const promisePoll = (promiseFunction, { pollIntervalMs = 2000 } = {}) => {
const startPoll = async resolve => {
const startTime = new Date()
const result = await promiseFunction()
if (result) return resolve()
const timeUntilNext = Math.max(pollIntervalMs - (new Date() - startTime), 0)
setTimeout(() => startPoll(resolve), timeUntilNext)
}
return new Promise(startPoll)
}
Example usage:
// async function which returns truthy if done
const checkIfOrderDoneAsync = async (orderID) => {
const order = await axios.get(`/order/${orderID}`)
return order.isDone
}
// can also use a sync function if you return a resolved promise
const checkIfOrderDoneSync = order => {
return Promise.resolve(order.isDone)
}
const doStuff = () => {
await promisePoll(() => checkIfOrderDone(orderID))
// will wait until the poll result is truthy before
// continuing to execute code
somethingElse()
}
function getReportURL(reportID) {
return () => viewReportsStatus(reportID)
.then(res => JSON.parse(res.body).d.url);
}
function pollForUrl(pollFnThatReturnsAPromise, target) {
if (target) return P.resolve(target);
return pollFnThatReturnsAPromise().then(someOrNone => pollForUrl(pollFnThatReturnsAPromise, someOrNone));
}
pollForUrl(getReportURL(id), null);
Related
I'm trying to create a typewriter effect with setTimeout().
My code for the typewriter function is as follows:
function typeWriter(toWrite,isDelete=false){
if(!isDelete && i<toWrite.length){
document.getElementById("typewrite").innerHTML+=toWrite.charAt(i);
i++;
speed=Math.random()*100+100;
setTimeout(typeWriter,speed,toWrite,false);
}
else if(isDelete && i<toWrite.length){
var typewrite=document.getElementById("typewrite");
typewrite.innerHTML=typewrite.innerHTML.slice(0,-1);
i++;
speed=100;
setTimeout(typeWriter,speed,toWrite,true);
}
}
And I want to call the code twice, once to write a string, and then a second time to delete a part of it.
My grasp on promises is still very shaky, and my attempt (below) didn't really change anything:
const intro=new Promise((resolve,reject)=>{
resolve();
})
intro
.then(typeWriter("hello world"))
.then(typeWriter("world",true))
When I run the code, instead of having "Hello world" get typed and then delete the "world". Both functions start going synchronously and the final output is "world".
I've been banging my head on this for longer than I'm comfortable admitting, I would appreciate any help.
to be chainable, typeWriter has to return a function.
e.g.
function typeWriter(toWrite,isDelete=false) {
return new Promise((resolve, reject) => {
})
}
If you were to use a setTimout, and you want to wait for the function to be called before going to the next of the chain, place the resolve in the settimeout.
e.g.
function typeWriter(toWrite,isDelete=false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hello i am done")
}, 1000)
})
}
Note, I'm using the same syntax as you - e.g
.then(typeWriter("hello world"))
But in this case, typeWriter returns a function, so it's legit
const typeWriter = (toWrite, isDelete) => {
const typewrite=document.getElementById("typewrite");
if (!isDelete) {
const fn = async (position = 0) => {
if (position < toWrite.length) {
typewrite.innerHTML += toWrite.charAt(position);
await new Promise(resolve => setTimeout(resolve, Math.random()*100 + 100));
return fn(position+1);
}
};
return () => fn(0);
} else {
const fn = async (position) => {
if (position < toWrite.length) {
typewrite.innerHTML = typewrite.innerHTML.slice(0,-1);
await new Promise(resolve => setTimeout(resolve, 100));
return fn(position + 1);
}
};
return () => fn(0);
}
};
Promise.resolve()
.then(typeWriter("hello world"))
.then(typeWriter("world",true))
<div id="typewrite"></div>
You're calling typeWriter immediately, while you really intended to pass that call to the then method, and have that only called when the relevant promise resolves. You can do that by passing () => typeWriter(......) as anonymous function to then.
Secondly, typeWriter will then have to return a promise, so the next chained then callback will only be called when that promise resolves.
I would also change the signature of your function a bit, so that it first deletes a given number of characters (can be 0) and then inserts the given string.
Here is how that looks:
// Promisify setTimeout:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// Make async, so it returns a promise:
const typeWriter = async (numDelete, toWrite) => {
const typewrite = document.getElementById("typewrite");
// First delete a given number of characters
while (numDelete-- > 0) {
typewrite.textContent = typewrite.textContent.slice(0, -1);
await delay(100);
}
// Then append new characters
for (const letter of toWrite) {
typewrite.textContent += letter;
await delay(Math.random()*100 + 100);
}
};
Promise.resolve()
// Pass a callback -- don't call typeWriter yourself
.then(() => typeWriter(0, "hello world"))
.then(() => typeWriter(5, ""));
<div id="typewrite"></div>
I'm trying to implement a debounce function that works with a promise in javascript. That way, each caller can consume the result of the "debounced" function using a Promise. Here is the best I have been able to come up with so far:
function debounce(inner, ms = 0) {
let timer = null;
let promise = null;
const events = new EventEmitter(); // do I really need this?
return function (...args) {
if (timer == null) {
promise = new Promise(resolve => {
events.once('done', resolve);
});
} else {
clearTimeout(timer);
}
timer = setTimeout(() => {
events.emit('done', inner(...args));
timer = null;
}, ms);
return promise;
};
}
Ideally, I would like to implement this utility function without introducing a dependency on EventEmitter (or implementing my own basic version of EventEmitter), but I can't think of a way to do it. Any thoughts?
I found a better way to implement this with promises:
function debounce(inner, ms = 0) {
let timer = null;
let resolves = [];
return function (...args) {
// Run the function after a certain amount of time
clearTimeout(timer);
timer = setTimeout(() => {
// Get the result of the inner function, then apply it to the resolve function of
// each promise that has been created since the last time the inner function was run
let result = inner(...args);
resolves.forEach(r => r(result));
resolves = [];
}, ms);
return new Promise(r => resolves.push(r));
};
}
I still welcome suggestions, but the new implementation answers my original question about how to implement this function without a dependency on EventEmitter (or something like it).
In Chris's solution all calls will be resolved with delay between them, which is good, but sometimes we need resolve only last call.
In my implementation, only last call in interval will be resolved.
function debounce(f, interval) {
let timer = null;
return (...args) => {
clearTimeout(timer);
return new Promise((resolve) => {
timer = setTimeout(
() => resolve(f(...args)),
interval,
);
});
};
}
And the following typescript(>=4.5) implementation supports aborted features:
Support aborting promise via reject(). If we don't abort it, it cannot execute finally function.
Support custom reject abortValue.
If we catch error, we may need to determine if the error type is Aborted
/**
*
* #param f callback
* #param wait milliseconds
* #param abortValue if has abortValue, promise will reject it if
* #returns Promise
*/
export function debouncePromise<T extends (...args: any[]) => any>(
fn: T,
wait: number,
abortValue: any = undefined,
) {
let cancel = () => { };
// type Awaited<T> = T extends PromiseLike<infer U> ? U : T
type ReturnT = Awaited<ReturnType<T>>;
const wrapFunc = (...args: Parameters<T>): Promise<ReturnT> => {
cancel();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve(fn(...args)), wait);
cancel = () => {
clearTimeout(timer);
if (abortValue!==undefined) {
reject(abortValue);
}
};
});
};
return wrapFunc;
}
/**
// deno run src/utils/perf.ts
function add(a: number) {
return Promise.resolve(a + 1);
}
const wrapFn= debouncePromise(add, 500, 'Aborted');
wrapFn(2).then(console.log).catch(console.log).finally(()=>console.log('final-clean')); // Aborted + final-clean
wrapFn(3).then(console.log).catch(console.log).finally(()=>console.log('final-clean')); // 4 + final_clean
Note:
I had done some memory benchmarks, huge number of pending promises won't cause memory leak. It seems that V8 engine GC will clean unused promises.
I landed here because I wanted to get the return value of the promise, but debounce in underscore.js was returning undefined instead. I ended up using lodash version with leading=true. It works for my case because I don't care if the execution is leading or trailing.
https://lodash.com/docs/4.17.4#debounce
_.debounce(somethingThatReturnsAPromise, 300, {
leading: true,
trailing: false
})
resolve one promise, cancel the others
Many implementations I've seen over-complicate the problem or have other hygiene issues. In this post we will write our own debounce. This implementation will -
have at most one promise pending at any given time (per debounced task)
stop memory leaks by properly cancelling pending promises
resolve only the latest promise
demonstrate proper behaviour with live code demos
We write debounce with its two parameters, the task to debounce, and the amount of milliseconds to delay, ms. We introduce a single local binding for its local state, t -
function debounce (task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return async (...args) => {
try {
t.cancel()
t = deferred()
await t.promise
await task(...args)
}
catch (_) { /* prevent memory leak */ }
}
}
We depend on a reusable deferred function, which creates a new promise that resolves in ms milliseconds. It introduces two local bindings, the promise itself, an the ability to cancel it -
function deferred (ms) {
let cancel, promise = new Promise((resolve, reject) => {
cancel = reject
setTimeout(resolve, ms)
})
return { promise, cancel }
}
click counter example
In this first example, we have a button that counts the user's clicks. The event listener is attached using debounce, so the counter is only incremented after a specified duration -
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const mycounter = myform.mycounter
// event handler
function clickCounter (event) {
mycounter.value = Number(mycounter.value) + 1
}
// debounced listener
myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))
<form id="myform">
<input name="myclicker" type="button" value="click" />
<output name="mycounter">0</output>
</form>
live query example, "autocomplete"
In this second example, we have a form with a text input. Our search query is attached using debounce -
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const myresult = myform.myresult
// event handler
function search (event) {
myresult.value = `Searching for: ${event.target.value}`
}
// debounced listener
myform.myquery.addEventListener("keypress", debounce(search, 1000))
<form id="myform">
<input name="myquery" placeholder="Enter a query..." />
<output name="myresult"></output>
</form>
Here's my version in typescript (mostly based on Chris one), if someone need it 😉
function promiseDebounce (exec: (...args: any[]) => Promise<any>, interval: number): () => ReturnType<typeof exec> {
let handle: number | undefined;
let resolves: Array<(value?: unknown) => void> = [];
return async (...args: unknown[]) => {
clearTimeout(handle);
handle = setTimeout(
() => {
const result = exec(...args);
resolves.forEach(resolve => resolve(result));
resolves = [];
},
interval
);
return new Promise(resolve => resolves.push(resolve));
};
}
No clue what you are trying to accomplish as it vastly depends on what your needs are. Below is something somewhat generic though. Without a solid grasp of what is going on in the code below, you really might not want to use it though.
// Debounce state constructor
function debounce(f) {
this._f = f;
return this.run.bind(this)
}
// Debounce execution function
debounce.prototype.run = function() {
console.log('before check');
if (this._promise)
return this._promise;
console.log('after check');
return this._promise = this._f(arguments).then(function(r) {
console.log('clearing');
delete this._promise; // remove deletion to prevent new execution (or remove after timeout?)
return r;
}.bind(this)).catch(function(r) {
console.log('clearing after rejection');
delete this._promise; // Remove deletion here for as needed as noted above
return Promise.reject(r); // rethrow rejection
})
}
// Some function which returns a promise needing debouncing
function test(str) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('test' + str);
resolve();
}, 1000);
});
}
a = new debounce(test); // Create debounced version of function
console.log("p1: ", p1 = a(1));
console.log("p2: ", p2 = a(2));
console.log("p1 = p2", p1 === p2);
setTimeout(function() {
console.log("p3: ", p3 = a(3));
console.log("p1 = p3 ", p1 === p3, " - p2 = p3 ", p2 === p3);
}, 2100)
View the console when running the code above. I put a few messages to show a bit about what is going on. First some function which returns a promise is passed as an argument to new debounce(). This creates a debounced version of the function.
When you run the debounced function as the code above does (a(1), a(2), and a(3)) you will notice during processing it returns the same promise instead of starting a new one. Once the promise is complete it removes the old promise. In code above I wait for the timeout manually with setTimeout before running a(3).
You can clear the promise in other ways as well, like adding a reset or clear function on debounce.prototype to clear the promise at a different time. You could also set it to timeout. The tests in the console log should show p1 and p2 get the same promise (reference comparison "===" is true) and that p3 is different.
Here is what I came up with to solve this issue. All calls to the debounced function batched to the same invocation all return the same Promise that resolves to the result of the future invocation.
function makeFuture() {
let resolve;
let reject;
let promise = new Promise((d, e) => {
resolve = d;
reject = e;
});
return [promise, resolve, reject];
}
function debounceAsync(asyncFunction, delayMs) {
let timeout;
let [promise, resolve, reject] = makeFuture();
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(async () => {
const [prevResolve, prevReject] = [resolve, reject];
[promise, resolve, reject] = makeFuture();
try {
prevResolve(await asyncFunction.apply(this, args));
} catch (error) {
prevReject(error);
}
}, delayMs);
return promise;
}
}
const start = Date.now();
const dog = {
sound: 'woof',
bark() {
const delay = Date.now() - start;
console.log(`dog says ${this.sound} after ${delay} ms`);
return delay;
},
};
dog.bark = debounceAsync(dog.bark, 50);
Promise.all([dog.bark(), dog.bark()]).then(([delay1, delay2]) => {
console.log(`Delay1: ${delay1}, Delay2: ${delay2}`);
});
Both Chris and Николай Гордеев have good solutions. The first will resolve all of them. The problem is that they all be resolved, but usually you wouldn't want all of them to run.
The second solution solved that but created a new problem - now you will have multiple awaits. If it's a function that is called a lot (like search typing) you might have a memory issue. I fixed it by creating the following asyncDebounce that will resolve the last one and reject (and the awaiting call will get an exception that they can just catch).
const debounceWithRejection = (
inner,
ms = 0,
reject = false,
rejectionBuilder
) => {
let timer = null;
let resolves = [];
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
const resolvesLocal = resolves;
resolves = [];
if (reject) {
const resolve = resolvesLocal.pop();
resolve.res(inner(...args));
resolvesLocal.forEach((r, i) => {
!!rejectionBuilder ? r.rej(rejectionBuilder(r.args)) : r.rej(r.args);
});
} else {
resolvesLocal.forEach((r) => r.res(inner(...args)));
}
resolves = [];
}, ms);
return new Promise((res, rej) =>
resolves.push({ res, rej, args: [...args] })
);
};
};
The rejection logic is optional, and so is the rejectionBuilder. It's an option to reject with specific builder so you will know to catch it.
You can see runing example.
This may not what you want, but can provide you some clue:
/**
* Call a function asynchronously, as soon as possible. Makes
* use of HTML Promise to schedule the callback if available,
* otherwise falling back to `setTimeout` (mainly for IE<11).
* #type {(callback: function) => void}
*/
export const defer = typeof Promise=='function' ?
Promise.resolve().then.bind(Promise.resolve()) : setTimeout;
Sometimes code would like to know if a particular function (or children) are running or not. For instance, node.js has domains which works for async stuff as well (not sure if this includes async functions).
Some simple code to explain what I need would by like this:
inUpdate = true;
try {
doUpdate();
} finally {
inUpdate = false;
}
This could then be used something like:
function modifyThings() {
if (inUpdate) throw new Error("Can't modify while updating");
}
With the advent of async this code breaks if the doUpdate() function is asynchronous. This was of course already true using callback-style functions.
The doUpdate function could of course be patched to maintain the variable around every await, but even if you have control over the code, this is cumbersome and error prone and this breaks when trying to track async function calls inside doUpdate.
I tried monkey-patching Promise.prototype:
const origThen = Promise.prototype.then;
Promise.prototype.then = function(resolve, reject) {
const isInUpdate = inUpdate;
origThen.call(this, function myResolve(value) {
inUpdate = isInUpdate;
try {
return resolve(value);
} finally {
inUpdate = false;
}
}, reject);
}
Unfortunately this doesn't work. I'm not sure why, but the async continuation code ends up running outside of the resolve call stack (probably using a microtask).
Note that it's not enough to simply do:
function runUpdate(doUpdate) {
inUpdate = true;
doUpdate.then(() => inUpdate = false).catch(() => inUpdate = false);
}
The reason is:
runUpdate(longAsyncFunction);
console.log(inUpdate); // incorrectly returns true
Is there any way to track something from outside an async function so it's possible to tell if the function called, or any of its descendant calls are running?
I know that it's possible to simulate async functions with generators and yield, in which case we have control over the call stack (since we can call gen.next()) but this is a kludge which the advent of async functions just got around to solving, so I'm specifically looking for a solution that works with native (not Babel-generated) async functions.
Edit: To clarify the question: Is there's a way for outside code to know if a particular invocation of an async function is running or if it is suspended, assuming that this code is the caller of the async function. Whether it's running or not would be determined by a function that ultimately is called by the async function (somewhere in the stack).
Edit: To clarify some more: The intended functionality would be the same as domains in node.js, but also for the browser. Domains already work with Promises, so async functions probably work as well (not tested).
This code allows me to do what I want to a certain extent:
function installAsyncTrack() {
/* global Promise: true */
if (Promise.isAsyncTracker) throw new Error('Only one tracker can be installed');
const RootPromise = Promise.isAsyncTracker ? Promise.rootPromise : Promise;
let active = true;
const tracker = {
track(f, o, ...args) {
const prevObj = tracker.trackObj;
tracker.trackObj = o;
try {
return f.apply(this, args);
} finally {
tracker.trackObj = prevObj;
}
},
trackObj: undefined,
uninstall() {
active = false;
if (Promise === AsyncTrackPromise.prevPromise) return;
if (Promise !== AsyncTrackPromise) return;
Promise = AsyncTrackPromise.prevPromise;
}
};
AsyncTrackPromise.prototype = Object.create(Promise);
AsyncTrackPromise.rootPromise = RootPromise;
AsyncTrackPromise.prevPromise = Promise;
Promise = AsyncTrackPromise;
AsyncTrackPromise.resolve = value => {
return new AsyncTrackPromise(resolve => resolve(value));
};
AsyncTrackPromise.reject = val => {
return new AsyncTrackPromise((resolve, reject) => reject(value));
};
AsyncTrackPromise.all = iterable => {
const promises = Array.from(iterable);
if (!promises.length) return AsyncTrackPromise.resolve();
return new AsyncTrackPromise((resolve, reject) => {
let rejected = false;
let results = new Array(promises.length);
let done = 0;
const allPromises = promises.map(promise => {
if (promise && typeof promise.then === 'function') {
return promise;
}
return new AsyncTrackPromise.resolve(promise);
});
allPromises.forEach((promise, ix) => {
promise.then(value => {
if (rejected) return;
results[ix] = value;
done++;
if (done === results.length) {
resolve(results);
}
}, reason => {
if (rejected) return;
rejected = true;
reject(reason);
});
});
});
};
AsyncTrackPromise.race = iterable => {
const promises = Array.from(iterable);
if (!promises.length) return new AsyncTrackPromise(() => {});
return new AsyncTrackPromise((resolve, reject) => {
let resolved = false;
if (promises.some(promise => {
if (!promise || typeof promise.then !== 'function') {
resolve(promise);
return true;
}
})) return;
promises.forEach((promise, ix) => {
promise.then(value => {
if (resolved) return;
resolved = true;
resolve(value);
}, reason => {
if (resolved) return;
resolved = true;
reject(reason);
});
});
});
};
function AsyncTrackPromise(handler) {
const promise = new RootPromise(handler);
promise.trackObj = tracker.trackObj;
promise.origThen = promise.then;
promise.then = thenOverride;
promise.origCatch = promise.catch;
promise.catch = catchOverride;
if (promise.finally) {
promise.origFinally = promise.finally;
promise.finally = finallyOverride;
}
return promise;
}
AsyncTrackPromise.isAsyncTracker = true;
function thenOverride(resolve, reject) {
const trackObj = this.trackObj;
if (!active || trackObj === undefined) return this.origThen.apply(this, arguments);
return this.origThen.call(
this,
myResolver(trackObj, resolve),
reject && myResolver(trackObj, reject)
);
}
function catchOverride(reject) {
const trackObj = this.trackObj;
if (!active || trackObj === undefined) return this.origCatch.catch.apply(this, arguments);
return this.origCatch.call(
this,
myResolver(trackObj, reject)
);
}
function finallyOverride(callback) {
const trackObj = this.trackObj;
if (!active || trackObj === undefined) return this.origCatch.catch.apply(this, arguments);
return this.origCatch.call(
this,
myResolver(trackObj, reject)
);
}
return tracker;
function myResolver(trackObj, resolve) {
return function myResolve(val) {
if (trackObj === undefined) {
return resolve(val);
}
RootPromise.resolve().then(() => {
const prevObj = tracker.trackObj;
tracker.trackObj = trackObj;
RootPromise.resolve().then(() => {
tracker.trackObj = prevObj;
});
});
const prevObj = tracker.trackObj;
tracker.trackObj = trackObj;
try {
return resolve(val);
} finally {
tracker.trackObj = prevObj;
}
};
}
}
tracker = installAsyncTrack();
function track(func, value, ...args) {
return tracker.track(func, { value }, value, ...args);
}
function show(where, which) {
console.log('At call', where, 'from', which, 'the value is: ', tracker.trackObj && tracker.trackObj.value);
}
async function test(which, sub) {
show(1, which);
await delay(Math.random() * 100);
show(2, which);
if (sub === 'resolve') {
await Promise.resolve(test('sub'));
show(3, which);
}
if (sub === 'call') {
await test(which + ' sub');
show(3, which);
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
track(test, 'test1');
track(test, 'test2');
track(test, 'test3', 'resolve');
track(test, 'test4', 'call');
It replaces the native Promise with my own. This promise stores the current context (taskObj) on the promise.
When the .then callback or its ilk are called, it does the following:
It creates a new native promise that immediately resolves. This adds a new microtask to the queue (according to spec, so should be reliable).
It calls the original resolve or reject. At least in Chrome and Firefox, this generates another microtask onto the queue that will run next part of the async function. Not sure what the spec has to say about this yet. It also restores the context around the call so that if it's not await that uses it, no microtask gets added here.
The first microtask gets executed, which is my first (native) promise being resolved. This code restores the current context (taskObj). It also creates a new resolved promise that queues another microtask
The second microtask (if any) gets executed, running the JS in the async function to until it hits the next await or returns.
The microtask queued by the first microtask gets executed, which restores the context to what it was before the Promise resolved/rejected (should always be undefined, unless set outside a tracker.track(...) call).
If the intercepted promise is not native (e.g. bluebird), it still works because it restores the state during the resolve(...) (and ilk) call.
There's one situation which I can't seem to find a solution for:
tracker.track(async () => {
console.log(tracker.taskObj); // 'test'
await (async () => {})(); //This breaks because the promise generated is native
console.log(tracker.taskObj); // undefined
}, 'test')
A workaround is to wrap the promise in Promise.resolve():
tracker.track(async () => {
console.log(tracker.taskObj); // 'test'
await Promise.resolve((async () => {})());
console.log(tracker.taskObj); // undefined
}, 'test')
Obviously, a lot of testing for all the different environments is needed and the fact that a workaround for sub-calls is needed is painful. Also, all Promises used need to either be wrapped in Promise.resolve() or use the global Promise.
[is it] possible to tell if the function called, or any of its descendant calls are running?
Yes. The answer is always no. Cause there is only one piece of code running at a time. Javascript is single threaded per definition.
Don't make it any more complicated than it needs to be. If doUpdate returns a promise (like when it is an async function), just wait for that:
inUpdate = true;
try {
await doUpdate();
//^^^^^
} finally {
inUpdate = false;
}
You can also use the finally Promise method:
var inUpdate = true;
doUpdate().finally(() => {
inUpdate = false;
});
That'll do just like like your synchronous code, having inUpdate == true while the function call or any of its descendants are running. Of course that only works if the asynchronous function doesn't settle the promise before it is finished doing its thing. And if you feel like the inUpdate flag should only be set during some specific parts of the doUpdate function, then yes the function will need to maintain the flag itself - just like it is the case with synchronous code.
I'm trying to convert some of my code to promises, but I can't figure out how to chain a new promise inside a promise.
My promise function should check the content of an array every second or so, and if there is any item inside it should resolve. Otherwise it should wait 1s and check again and so on.
function get(){
return new Promise((resolve) => {
if(c.length > 0){
resolve(c.shift());
}else{
setTimeout(get.bind(this), 1000);
}
});
}
let c = [];
setTimeout(function(){
c.push('test');
}, 2000);
This is how I expect my get() promise function to work, it should print "test" after 2 or 3 seconds max:
get().then((value) => {
console.log(value);
});
Obviously it doesn't work, nothing is ever printed
setTimeout has terrible chaining and error-handling characteristics on its own, so always wrap it:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
function get(c) {
if (c.length) {
return Promise.resolve(c.shift());
}
return wait(1000).then(() => get(c)); // try again
}
let c = [];
get(c).then(val => console.log(val));
wait(2000).then(() => c.push('test'));
While you didn't ask, for the benefit of others, this is a great case where async/await shines:
const wait = ms => new Promise(r => setTimeout(r, ms));
async function get(c) {
while (!c.length) {
await wait(1000);
}
return c.shift();
}
let c = [];
get(c).then(val => console.log(val));
wait(2000).then(() => c.push('test'));
Note how we didn't need Promise.resolve() this time, since async functions do this implicitly.
The problem is that your recursive call doesn't pass the resolve function along, so the else branch can never call resolve.
One way to fix this would be to create a closure inside the promise's callback so that the recursive call will have access to the same resolve variable as the initial call to get.
function get() {
return new Promise((resolve) => {
function loop() {
if (c.length > 0) {
resolve(c.shift());
} else {
setTimeout(loop, 1000);
}
}
loop();
});
}
let c = [];
setTimeout(function() {
c.push('test');
}, 2000);
get().then(val => console.log(val));
In the else case, you never resolve that promise. get might create another one, but it is returned to nowhere.
You should promisify your asynchronous function (setTimeout) on the lowest level, and then only chain your promises. By returning the result of the recursive call from a then callback, the resulting promise will resolve with the same result:
function delayAsync(time) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
function get(c) {
if (c.length > 0){
return Promise.resolve(c.shift());
} else {
return delay(1000).then(() => {
return get(c); // try again
});
}
}
What you need is a polling service, which checks periodically for specific condition prior proceeding with promise resolution. Currently when you run setTimeout(get.bind(this), 1000); you are creating a new instance of the promise without actually resolving the initial promise, because you don't reference to the initial resolve function that you created.
Solution:
Create a new callback function that you can reference to it inside the promise
Pass the resolve & reject as params in the setTimeout invocation e.g. setTimeout(HandlePromise, 1000, resolve, reject, param3, param4 ..); setTimeout API
function get() {
var handlerFunction = resolve => {
if (c.length > 0) {
resolve(c.shift());
} else {
setTimeout(handlerFunction, 1000, resolve);
}
};
return new Promise(handlerFunction);
}
let c = [];
setTimeout(function() {
c.push("test");
}, 2000);
get().then(value => {
console.log(value);
});
For more information look into javascript polling article
You could try this solution. Since JS needs to free itself to download the images, I use await within an asynchronous function and an asynchronous call to wake up JS after a delay
private async onBeforeDoingSomething() : void {
await this.delay(1000);
console.log("All images are loaded");
}
private delay (ms : number = 500) : Promise<number> {
return new Promise((resolve,reject) => {
const t = setTimeout( () => this.areImgsLoaded(resolve), ms);
});
}
private async areImgsLoaded (resolve) {
let reload = false;
const img = document.querySelectorAll('img');
console.log("total of images: ",img.length);
for (let i = 0; i < img.length; i++){
if (!img[i]["complete"]) {
console.log("img not load yet");
reload = true;
break;
}
}
if (reload) {
await this.delay();
}
resolve();
}
Use setInterval to check every second. Run this script to understand.
let c = [];
function get(){
return new Promise((resolve) => {
var i = setInterval(function(){
if(c.length > 0){
resolve(c.shift());
clearInterval(i);
}
}, 1000);
});
}
setTimeout(function(){
c.push('test');
}, 2000);
get().then((value) => {
console.log(value);
});
I am learning promises, so I decided to play around with extending them. Everything is working except I am having a hard time figuring out how to make a value persist across all functions.
My goal is to find the count of the called functions from this promise, but each call creates a new one and I am having trouble finding a way to pass the value in. I tried adding a constructor which will have a value passed in, but it doesn't seem to work as I expect. I assume this is due to me misunderstanding the scope of "this".
to sum it up, each of my functions "init", "add" and "commit" should all up front add 1 to the "total number of steps" variables, which right now in this example is "i". With that I want to be able to say, I am on step 1 of 3, step 2 of 3 etc...
class Repo {
constructor(setup) {
this.s = {};
this.s._progress = { "total":0, "count":0 };
this.s._logging = { "enabled":true, "location":"console", "pretty":true, "verbose":false, "tabCount":0 };
this.s._remoteInfo = { "name":"", "url":"" };
this.s._localInfo = { "path":"" };
this.s._logReset = () => {
this.s._logging.tabCount = 0;
};
this.s._log = (message, tabCount) => {
if(this.s._logging.enabled) {
let tabs = '';
if(this.s._logging.pretty) {
for(let i = 0; i < tabCount; i++) { tabs = tabs + '\t' };
}
if(this.s._logging.location == 'console') { console.log(tabs, message); }
else {
//TODO: implement the file location to output
}
}
};
this.s._progressReset = () => {
this.s._progress.total = 0;
this.s._progress.count = 0;
};
this.s._addProgressTotal = () => {
this.s._progress.total++;
console.log(this.s._progress.total)
}
this.s._addProgress = () => {
this.s._progress.count++;
console.log('Progress is ' + this.s._progress.count + ' out of ' + this.s._progress.total)
}
}
//Starts the promise chain and passes in the settings to be used.
start() {
this.s._logReset();
this.s._progressReset();
return new RepoPromise((resolve, reject) => {
this.s._log('Start Log: <time>',0)
resolve(this.s);
});
}
}
class RepoPromise extends Promise {
constructor(executor, val) {
let e = executor || function (res, rej) { res('')};
super((resolve, reject) => {
return e(resolve, reject);
});
this.i = val || 0;
}
end() {
const returnValue = super.then((s) => {
return new RepoPromise((resolve, reject) => {
s._log('End Log: <time>',0)
resolve(s);
}, this.i);
});
return returnValue;
}
init() {
//I know I need to add 1 to "i" here, but it won't work
const returnValue = super.then((s) => {
return new RepoPromise((resolve, reject) => {
s._log('git init',1);
s._addProgress();
resolve(s, '')
}, ++this.i);
});
return returnValue;
};
add() {
//I know I need to add 1 to "i" here, but it won't work
const returnValue = super.then((s) => {
return new RepoPromise((resolve, reject) => {
setTimeout(() => {
s._log('git add',1);
s._addProgress();
resolve(s,'');
//reject('Add Failed')
}, Math.random() * (10000 - 1000) + 1000);
},++this.i);
});
return returnValue;
}
commit() {
//I know I need to add 1 to "i" here, but it won't work
const returnValue = super.then((s) => {
return new RepoPromise((resolve, reject) => {
setTimeout(() => {
s._log('git commit -m "message"',1);
s._addProgress();
resolve(s, 'Finished');
}, Math.random() * (5000 - 1000) + 1000);
}, ++this.i);
});
return returnValue;
}
then(onFulfilled, onRejected) {
const returnValue = super.then(onFulfilled, onRejected);
return returnValue;
}
}
usage:
var p = new Repo('')
.start()
.init()
.add()
.commit()
.end()
.catch(
x => {console.log('it broke: ' + x)}
);
As you've pointed out, there isn't one promise in the chain, every then and catch returns a new promise. So don't try to retain the state in the RepoPromise, retain it in the object that you pass through the chain as the resolution value: s.
Re your second parameter to RepoPromise constructor: You can't reliably do that, because you aren't in control of every time that constructor is called. Remember, that constructor is called when you call then or catch. Which is another reason to pass the value around on s instead. :-) Just for completeness, here's an illustration of the fact that the constructor gets called within Promise:
class MyPromise extends Promise {
constructor(...args) {
super(...args);
console.log("MyPromise constructor called");
}
}
MyPromise.resolve()
.then(val => val)
.then(val => val)
.then(val => val);
A couple of side notes:
This:
super((resolve, reject) => {
return e(resolve, reject);
});
can be written simply as:
super(e);
This doesn't do anything and can just be removed:
then(onFulfilled, onRejected) {
const returnValue = super.then(onFulfilled, onRejected);
return returnValue;
}
I was a bit dense understanding the question, but now I get it: You want to increase s._progress.total for each call to init/add/commit, and increase s._progress.count when each then/catch callback is called.
Here's a simplified exmaple that just uses then and catch rather than adding init, add, and commit but you can easily apply the pattern to add those if you like.
The solution was to keep the status tracker (s) on the promise object, and insert ourselves into the various ways new promises are created (then, catch) so we copy the tracker from the old promise to the new one. We share the tracker across all of these promises, e.g., the tracker from the root promise tracks everything from there forward. See comments:
"use strict";
// For tracking our status
class Status {
constructor(total = 0, count = 0) {
this.id = ++Status.id;
this.total = total;
this.count = count;
}
addCall() {
++this.total;
return this;
}
addProgress() {
++this.count;
return this;
}
toString() {
return `[S${this.id}]: Total: ${this.total}, Count: ${this.count}`;
}
}
Status.id = 0;
// The promise subclass
class RepoPromise extends Promise {
constructor(executor) {
super(executor);
this.s = new Status();
}
// Utility method to wrap `then`/`catch` callbacks so we hook into when they're called
_wrapCallbacks(...callbacks) {
return callbacks.filter(c => c).map(c => value => this._handleCallback(c, value));
}
// Utility method for when the callback should be called: We track that we've seen
// the call then execute the callback
_handleCallback(callback, value) {
this.s.addProgress();
console.log("Progress: " + this.s);
return callback(value);
}
// Standard `then`, but overridden so we track what's going on, including copying
// our status object to the new promise before returning it
then(onResolved, onRejected) {
this.s.addCall();
console.log("Added: " + this.s);
const newPromise = super.then(...this._wrapCallbacks(onResolved, onRejected));
newPromise.s = this.s;
return newPromise;
}
// Standard `catch`, doing the same things as `then`
catch(onRejected) {
this.s.addCall();
console.log("Added: " + this.s);
const newPromise = super.catch(...this._wrapCallbacks(onRejected));
newPromise.s = this.s;
return newPromise;
}
}
// Create a promise we'll resolve after a random timeout
function delayedGratification() {
return new Promise(resolve => {
setTimeout(_ => {
resolve();
}, Math.random() * 1000);
});
}
// Run! Note we follow both kinds of paths: Chain and diverge:
const rp = RepoPromise.resolve();
rp.then(delayedGratification) // First chain
.then(delayedGratification)
.then(delayedGratification);
rp.catch(delayedGratification) // Second chain
.then(delayedGratification);
.as-console-wrapper {
max-height: 100% !important;
}