Определение статуса асинхронной функции «только возвращенное обещание»

#javascript #promise #async-await #es6-promise

#javascript #обещание #async-ожидание #es6-обещание

Вопрос:

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

 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() и вызывающий этот thirdPartyCode(), я хотел бы определить, используется ли обещание myPromiser () в качестве возвращаемого обещания асинхронной функции. Это единственный законный способ использовать его в контексте вызова этого конкретного вида асинхронной функции. Его нельзя ожидать или к нему не могут быть присоединены предложения .then(), пока оно находится внутри этой функции.

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

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

Комментарии:

1. Между return myPromiser() и return await myPromiser() нет большой семантической разницы. В чем именно заключается ограничение, что именно не должно происходить после myPromiser() ? Я думаю, что единственная возможность — это ошибка при этом, а не при вызове myPromiser() .

2. «Нет большой семантической разницы между return myPromiser() и return await myPromiser()» Я согласен — и, как я уже сказал, в идеале оба варианта должны работать — но это попытка усовершенствовать совместимое подмножество стиля кодирования для клиентов библиотеки WASM как в новых, так и в старых браузерах. В чем именно заключается ограничение. Это было бы длинное объяснение, требующее досконального понимания emscripten_sleep_with_yield() , а затем понимания очень сложного его использования.

3. Попробуйте меня 🙂 Каков ваш вариант использования?

4. Хорошо, позвольте мне резюмировать: существует библиотека, которая позволяет запускать синхронные сценарии Rebol в браузере, используя webworkers или асинхронный интерпретатор. Он может использоваться синхронно ( reb.Spell ) или асинхронно ( reb.Promise ) в зависимости от того, что делает скрипт Rebol. Теперь вы пишете расширение для среды выполнения, которое позволяет вам вызывать обратный вызов в javascript (в основном для ввода-вывода) из синхронного сценария Rebol, и этот js может быть асинхронным (возвращающим обещание), что делает необходимым приостановку среды выполнения. Пока ничего себе. Я правильно понял?

5. И теперь вы хотите разрешить это внутреннее JavaScript, чтобы использовать во время выполнения (например reb.Spell , reb.ArgR , reb.Text , reb.Buffer ) а на самом деле это условно, так только синхронные функции будут работать. (Я в замешательстве, что какие-либо функции вообще работают). Вы все еще хотите разрешить возврат одного результата от асинхронного вызова, для которого ваш текущий обходной путь возвращает ту функцию, которая выполняет вызов?

Ответ №1:

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

Идея 1

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

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

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

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

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

Идея 2

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

Ответ 3

Если вы ищете способ узнать, является ли обещание вашим или разрешено — тогда ответ «такого способа нет». Доказательство (выполняется в 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
  

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

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

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

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


Код для ответа 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("nnn");

        console.log('Starting test 3');
        r = await thirdPartyCode(3);
        console.log('Test 3 finished - no error, args used in myPromiser = ', r[mySymbol]);
        console.log("nnn");

        console.log('Starting test 2 - should see an error below');
        r = await thirdPartyCode(2);
    })();
  

Комментарии:

1. Эй, насколько я могу судить, глядя на это прямо сейчас, метод # 1, похоже, работает так, как вы описываете! Возможно, @Bergi нашел бы это интересным или у него были бы комментарии. Учитывая, что некоторые из моих функций не возвращают results…in в целом, возможно, было бы лучше отказаться от специализированных ошибок для .then() и await для того, чтобы получить ошибку в случае await myPromiser() … а также использование более простого объекта с «меньшим количеством движущихся частей». Я немного подумаю над этим, но вы определенно предоставили подробный ответ и заслуживаете награды за это! Спасибо!

Ответ №2:

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

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

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

Кажется, что превращение memoization в подкласс ошибок может быть хорошей вещью — таким образом, он идентифицирует вызывающий стек и может привести к нарушению callsites, подобных 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...
    })
}
  

Комментарии:

1. Я сомневаюсь, что это работает. Когда вы return это MyFakePromise сделаете в async function , оно все равно попытается разрешить возвращенное обещание с его помощью и вызовет then метод, точно так же, как когда вы await это.

2. @Bergi Ах, хорошее наблюдение. :-/ Я моделировал .then () в своей голове как нечто, доступное только для обещаний. Но вы прочитали это достаточно внимательно, чтобы понять это, так можно ли это изменить в соответствии с желанием? Одна вещь .then() может знать идентификатор обработчика, который передает специальный сайт вызова под моим контролем, и разрешить эту функцию только каким-то образом … какой-то метод белого списка в обработчике.

3. Нет, async function всегда возвращается собственное Promise , и я не могу придумать способ идентифицировать сайт вызова. Также then идентификатор обратного вызова не может быть использован, ваше поддельное обещание никогда этого не увидит.

4. Однако, поскольку вы вызываете thirdPartyCode , ваш общий подход довольно хорош: просто заставьте myPromiser() возвращать какое-то значение прокси-сервера — это вообще не обязательно должно быть обещание — и когда thirdPartyCode() обещание выполняется с этим значением прокси-сервера, вы запускаете то, что хотите. Это также гарантирует, что существует максимум одно значение — третья сторона может вызывать myPromiser() так часто, как захочет, но это бессмысленно, поскольку она может вернуть только одно из них.

5. @Bergi Но что let good = function(x) {} тогда class Fake extends Error { constructor() { super("bad") } then(f) { if (f == good) {console.log("good")} else { throw this }}} ? Если вы скажете, let p = new Fake; p.then(good) что все в порядке, в то время как все остальные предложения then(function(x) {…}) выдадут ошибку. Поскольку мой callsite является единственным, который выполняет эту обработку, похоже, что это работает, и вы получаете ошибки (с объяснением и надлежащим значением callsite) в await / then для всех остальных — как и предполагалось.