Reputation: 33607
I have a situation like this:
async function thirdPartyCode(a) {
if (a == ...) {
return myPromiser(...) // can allow and act on this
}
let b = await someoneElsesPromiserB(...)
if (b == ...) {
let c = await myPromiser(...) // must error on this
...
}
let d = await someoneElsesPromiserD(...)
let e = myPromiser(...) // note no await
return e // can allow and act on this
}
As the author of myPromiser() and caller of this thirdPartyCode(), I'd like to detect whether the myPromiser()'s promise is used as the returning promise of the async function. This is the only legal way to use it in this particular kind of async function's calling context. It cannot be awaited on, or have .then() clauses attached to it while it is inside this function.
If there were a way to know "When is the body of an async function actually finished", that would be a wedge to solving it.
(Note: The strange limitations in this question are a by-product of using the Emscripten Emterpreter. The limits may (?) need not apply when simulated pthreads are available via WebAssembly workers / SharedArrayBuffer / etc. But those bleeding-edge browser features aren't enabled by default at time of writing...so this unusual desire comes from wanting a compatible subset of code to be legal.)
Upvotes: 4
Views: 510
Reputation: 33607
UPDATE This approach can work mechanically, but can't directly throw a custom errors when they use
then()
,catch()
, orawait
. They'll just get a more cryptic error likeobject has no method .then()
. See comments from @Bergi suggesting there's no way to give something a "promise like appearance" and still be able to tell from the result where the promise originated. But leaving some of the initial notes in the answer to help illustrate what the actual desire was...
RE: "If there were a way to know 'When is the body of an async function actually finished'"
Async functions are "actually finished" when their returning promise resolves. If you control the calling context and myPromiser(), then you (er, me) could choose to make myPromiser() not return a promise directly -but- a Promise-like object which memoizes the work you intend to do once the call is finished.
Making the memoization an Error subclass seems like it could be a good thing--so it identifies the calling stack and can implicate offending callsites like the await myPromiser(...)
from the example.
class MyFakePromise extends Error {
memo // capture of whatever MyPromiser()'s args were for
constructor(memo) {
super("You can only use `return myPromiser()` in this context")
this.memo = memo
}
errorAndCleanup() {
/* this.memo.cleanup() */ // if necessary
throw this // will implicate the offending `myPromiser(...)` callsite
}
// "Fake promise interface with .then() and .catch()
// clauses...but you can still recognize it with `instanceof`
// in the handler that called thirdPartyCode() and treat it
// as an instruction to do the work." -- nope, doesn't work
//
then(handler) { // !!! See UPDATE note, can't improve errors via .then()
this.errorAndCleanup()
}
catch(handler) { // !!! See UPDATE note, can't improve errors via .catch()
this.errorAndCleanup()
}
}
This gives the desired property of erroring for anyone who tried to actually use it:
> let x = new MyFakePromise(1020)
> await x
** Uncaught (in promise) Error: You can only use `return myPromiser()` in this context
But if it's not used and just passed on, you can treat it like data. So then you'd do something like this in the calling context where fake promises must be used:
fake_promise_mode = true
thirdPartyCode(...)
.then(function(result_or_fake_promise) {
fake_promise_mode = false
if (result_or_fake_promise instanceof MyFakePromise) {
handleRealResultMadeFromMemo(result_or_fake_promise.memo)
else
handleRealResult(result_or_fake_promise)
})
.catch(function(error)) {
fake_promise_mode = false
if (error instanceof MyFakePromise)
error.errorAndCleanup()
throw error
})
And myPromiser() would heed the flag to know if it had to give the fake promise:
function myPromiser(...) {
if (fake_promise_mode) {
return new MyFakePromise(...memoize args...)
return new Promise(function(resolve, reject) {
...safe context for ordinary promising...
})
}
Upvotes: 1
Reputation: 1041
Your question is quite complex and I might get it wrong in some aspects. But here are 3-4 ideas that might help.
Idea 1
From 'then' you can call 'handler' immediately with a Proxy, which forbids mostly every operation with it. After this done you just watch for function to exit or to throw your error. This way you can track if the returned value is actually used in any way.
However if the returned value isn't used - you won't see it. So this allows this kind of uses:
... some code ...
await myPromiser(); // << notice the return value is ignored
... some more code ...
If this is an issue for you, then this method helps only partially. But if this is an issue than your last call (let e = myPromiser(...)) would also be useless, since "e" can be ignored after.
Below, at the end of this answer javascript code which successfully distinguish between your three cases
Idea 2
You can use Babel to instrument 'thirdPartyCode' code before calling it. Babel can also be used in runtime if needed. With it you can: 2.1 Just find all usages of myPromise and examine if its legitimate or not. 2.2 Add calls to some marker functions after each await or '.then' - this way you'll be able to detect all the cases with Option 1.
Answer 3
If you are seeking a way to know if Promise is yours or is resolved - then the answer is 'there is no such way'. Proof (execute in Chrome as an example):
let p = new Promise((resolve, reject)=>{
console.log('Code inside promise');
resolve(5);
});
p.then(()=>{
console.log('Code of then')
})
console.log('Code tail');
// Executed in Chrome:
// Code inside promise
// Code tail
// Code of then
This tells us that code of resolve is always executed outside of current calling context. I.e. we might have been expecting that calling 'resolve' from inside Promise would result in immediate call to all the subscribed functions, but it's not - v8 will wait until current function execution is over and only then execute then handler.
Idea 4 (partial)
If you want to intercept all calls to SystemPromise.then and decide if your Promiser or not was called - there is a way: you can override Promise.then with your implemetation.
Unfortunately this won't tell you if async function is over or not. I've tried experimenting with it - see comments in my code below.
Code for answer 1:
let mySymbol = Symbol();
let myPromiserRef = undefined;
const errorMsg = 'ANY CUSTOM MESSAGE HERE';
const allForbiddingHandler = {
getPrototypeOf: target => { throw new Error(errorMsg); },
setPrototypeOf: target => { throw new Error(errorMsg); },
isExtensible: target => { throw new Error(errorMsg); },
preventExtensions: target => { throw new Error(errorMsg); },
getOwnPropertyDescriptor: target => { throw new Error(errorMsg); },
defineProperty: target => { throw new Error(errorMsg); },
has: target => { throw new Error(errorMsg); },
get: target => { throw new Error(errorMsg); },
set: target => { throw new Error(errorMsg); },
deleteProperty: target => { throw new Error(errorMsg); },
ownKeys: target => { throw new Error(errorMsg); },
apply: target => { throw new Error(errorMsg); },
construct: target => { throw new Error(errorMsg); },
};
// We need to permit some get operations because V8 calls it for some props to know if the value is a Promise.
// We tell it's not to stop Promise resolution sequence.
// We also allow access to our Symbol prop to be able to read args data
const guardedHandler = Object.assign({}, allForbiddingHandler, {
get: (target, prop, receiver) => {
if(prop === mySymbol)
return target[prop];
if(prop === 'then' || typeof prop === 'symbol')
return undefined;
throw new Error(errorMsg);
},
})
let myPromiser = (...args)=> {
let vMyPromiser = {[mySymbol]:[...args] };
return new Proxy(vMyPromiser,guardedHandler);
// vMyPromiser.proxy = new Proxy(vMyPromiser,guardedHandler);
// vMyPromiser.then = ()=> {
// myPromiserRef = vMyPromiser;
// console.log('myPromiserThen - called!');
// return vMyPromiser.proxy;
// }
// return vMyPromiser;
};
let someArg = ['someArgs1', 'someArgs2'];
const someoneElsesPromiserB = async(a)=>{
return a;
}
const someoneElsesPromiserD = async(a)=>{
return a;
}
async function thirdPartyCode(a) {
console.log('CODE0001')
if (a == 1) {
console.log('CODE0002')
return myPromiser(a, someArg) // can allow and act on this
}
console.log('CODE0003')
let b = await someoneElsesPromiserB(a)
console.log('CODE0004')
if (b == 2) {
console.log('CODE0005')
let c = await myPromiser(a, someArg) // must error on this
console.log('CODE0006')
let x = c+5; // <= the value should be used in any way. If it's not - no matter if we did awaited it or not.
console.log('CODE0007')
}
console.log('CODE0008')
let d = await someoneElsesPromiserD(a);
console.log('CODE0009')
let e = myPromiser(a, someArg) // note no await
console.log('CODE0010')
return e // can allow and act on this
};
// let originalThen = Promise.prototype.then;
// class ReplacementForPromiseThen {
// then(resolve, reject) {
// // this[mySymbol]
// if(myPromiserRef) {
// console.log('Trapped then myPromiser - resolve immediately');
// resolve(myPromiserRef.proxy);
// myPromiserRef = undefined;
// } else {
// console.log('Trapped then other - use System Promise');
// originalThen.call(this, resolve, reject);
// }
// }
// }
//
// Promise.prototype.then = ReplacementForPromiseThen.prototype.then;
(async()=>{
let r;
console.log('Starting test 1');
r = await thirdPartyCode(1);
console.log('Test 1 finished - no error, args used in myPromiser = ', r[mySymbol]);
console.log("\n\n\n");
console.log('Starting test 3');
r = await thirdPartyCode(3);
console.log('Test 3 finished - no error, args used in myPromiser = ', r[mySymbol]);
console.log("\n\n\n");
console.log('Starting test 2 - should see an error below');
r = await thirdPartyCode(2);
})();
Upvotes: 1