Обнаружение состояния «только возвращенное обещание» асинхронной функции - PullRequest
4 голосов
/ 15 марта 2019

У меня такая ситуация:

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
}

Как автор myPromiser () и вызывающая сторона этого третьегоPartyCode (), я хотел бы определить, используется ли обещание myPromiser () в качестве возвращаемого обещания асинхронной функции. Это единственный допустимый способ использовать его в контексте вызова этого типа асинхронной функции . Его нельзя ожидать, или к нему не могут быть добавлены предложения .then (), пока он находится внутри этой функции.

Если бы был способ узнать "Когда тело асинхронной функции фактически закончено", это было бы клином для ее решения.

(Примечание: странные ограничения в этом вопросе являются побочным продуктом использования Emscripten Emterpreter . Ограничения могут (?) Не применяться, если имитированные pthreads доступны через WebAssembly работников / SharedArrayBuffer / и т. д. Но эти самые передовые функции браузера не включены по умолчанию на момент написания ... поэтому это необычное желание возникает из-за того, что совместимый поднабор кода допустим.)

Ответы [ 2 ]

1 голос
/ 29 марта 2019

Ваш вопрос довольно сложный, и я могу ошибаться в некоторых аспектах.Но вот 3-4 идеи, которые могут помочь.

Идея 1

Из 'then' вы можете немедленно вызвать 'handler' с прокси, который запрещает в большинстве случаев каждыйоперация с ним.После того, как это сделано, вы просто наблюдаете за функцией, чтобы выйти или выдать ошибку.Таким образом, вы можете отслеживать, действительно ли возвращенное значение используется каким-либо образом.

Однако, если возвращенное значение не используется - вы его не увидите.Так что это позволяет использовать этот вид:

    ... some code ...
    await myPromiser();         // << notice the return value is ignored
    ... some more code ...

Если это проблема для вас, то этот метод помогает только частично.Но если это проблема, то ваш последний вызов (пусть e = myPromiser (...)) также будет бесполезен, так как «е» можно игнорировать после.

Ниже, вконец этого ответа: JavaScript-код, который успешно различает ваши три случая

Идея 2

Вы можете использовать Babel для инструментов кода третьегоPartyCodeпрежде чем позвонить.Babel также может быть использован во время выполнения при необходимости.С его помощью вы можете: 2.1 Просто найти все способы использования myPromise и проверить, является ли он законным или нет.2.2 Добавляйте вызовы к некоторым функциям маркера после каждого ожидания или «.then» - таким образом вы сможете обнаружить все случаи с помощью Варианта 1.

Ответ 3

Если вы ищете способ узнать, является ли «Обещание» вашим или решенным - тогда ответ «нет такого пути».Proof (выполнить в Chrome в качестве примера):

    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

Это говорит нам о том, что код разрешения всегда выполняется вне текущего контекста вызова.Т.е. мы могли ожидать, что вызов функции 'resol' из Promise приведет к немедленному вызову всех подписанных функций, но это не так - v8 будет ждать окончания текущего выполнения функции и только затем выполнит затем обработчик.

Идея 4 (частичная)

Если вы хотите перехватить все вызовы SystemPromise.then и решить, был ли вызван ваш Promiser или нет - есть способ: вы можете переопределить Promise.thenс вашей реализацией.

К сожалению, это не скажет вам, закончена ли асинхронная функция или нет.Я попытался поэкспериментировать с ним - см. Комментарии в моем коде ниже.


Код для ответа 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);
    })();
1 голос
/ 15 марта 2019

ОБНОВЛЕНИЕ Этот подход может работать механически, но не может напрямую генерировать пользовательские ошибки, когда они используют then(), catch() или await. Они просто получат более загадочную ошибку, такую ​​как object has no method .then(). Посмотрите комментарии @Bergi, в которых говорится, что нет способа придать что-то «внешнему виду, подобному обещанию», и все же быть в состоянии определить результат, из которого возникло обещание. Но оставив несколько начальных примечаний в ответе, чтобы проиллюстрировать, каким было фактическое желание ...

RE: «Если бы был способ узнать« Когда тело асинхронной функции на самом деле закончено »»

Асинхронные функции «фактически завершены», когда их возвращаемое обещание разрешается. Если вы управляете вызывающим контекстом и myPromiser (), то вы (э, я) могли бы сделать так, чтобы myPromiser () не возвращал обещание напрямую - но - объект типа Promise, который запоминает работу, которую вы намереваетесь делать после завершения вызова.

Делать памятку подклассом Error кажется хорошей вещью, поэтому он идентифицирует стек вызовов и может включать вызывающие вызовы сайты, такие как await myPromiser(...) из примера.

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()
   }
}

Это дает желаемое свойство ошибки для любого, кто пытался фактически использовать его:

 > let x = new MyFakePromise(1020)
 > await x
 ** Uncaught (in promise) Error: You can only use `return myPromiser()` in this context

Но если он не используется и просто передается, вы можете обращаться с ним как с данными. И тогда вы будете делать что-то подобное в контексте вызова, где должны использоваться ложные обещания:

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
   })

И myPromiser () прислушается к флагу, чтобы узнать, должен ли он дать ложное обещание:

function myPromiser(...) {
    if (fake_promise_mode) {
        return new MyFakePromise(...memoize args...)
    return new Promise(function(resolve, reject) {
        ...safe context for ordinary promising...
    })
}
...