diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index a632dda4c81..2f0edad6bf6 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -3,6 +3,7 @@ import { makeWhen } from './when.js'; import { prepareVowKit } from './vow.js'; import { prepareWatch } from './watch.js'; import { prepareWatchUtils } from './watch-utils.js'; +import { makeAsVow } from './vow-utils.js'; /** @import {Zone} from '@agoric/base-zone' */ /** @import {IsRetryableReason} from './types.js' */ @@ -20,6 +21,7 @@ export const prepareVowTools = (zone, powers = {}) => { const watch = prepareWatch(zone, makeVowKit, isRetryableReason); const makeWatchUtils = prepareWatchUtils(zone, watch, makeVowKit); const watchUtils = makeWatchUtils(); + const asVow = makeAsVow(makeVowKit); /** * Vow-tolerant implementation of Promise.all. @@ -28,7 +30,7 @@ export const prepareVowTools = (zone, powers = {}) => { */ const allVows = vows => watchUtils.all(vows); - return harden({ when, watch, makeVowKit, allVows }); + return harden({ when, watch, makeVowKit, allVows, asVow }); }; harden(prepareVowTools); diff --git a/packages/vow/src/vow-utils.js b/packages/vow/src/vow-utils.js index 63ba2ecf6aa..2da8ccda1c5 100644 --- a/packages/vow/src/vow-utils.js +++ b/packages/vow/src/vow-utils.js @@ -4,8 +4,9 @@ import { isPassable } from '@endo/pass-style'; import { M, matches } from '@endo/patterns'; /** - * @import {PassableCap} from '@endo/pass-style' - * @import {VowPayload, Vow} from './types.js' + * @import {PassableCap} from '@endo/pass-style'; + * @import {VowPayload, Vow} from './types.js'; + * @import {MakeVowKit} from './vow.js'; */ export { basicE }; @@ -73,3 +74,30 @@ export const toPassableCap = k => { return vowV0; }; harden(toPassableCap); + +/** @param {MakeVowKit} makeVowKit */ +export const makeAsVow = makeVowKit => { + /** + * Helper function that coerces the result of a function to a Vow. Helpful + * for scenarios like a synchronously thrown error. + * @template {any} T + * @param {(...args: any[]) => Vow> | Awaited} fn + * @returns {Vow>} + */ + const asVow = fn => { + let result; + try { + result = fn(); + } catch (e) { + result = Promise.reject(e); + } + if (isVow(result)) { + return result; + } + const kit = makeVowKit(); + kit.resolver.resolve(result); + return kit.vow; + }; + return harden(asVow); +}; +harden(makeAsVow); diff --git a/packages/vow/src/vow.js b/packages/vow/src/vow.js index 742a9c2e737..1a1ca4a472e 100644 --- a/packages/vow/src/vow.js +++ b/packages/vow/src/vow.js @@ -157,5 +157,6 @@ export const prepareVowKit = zone => { return makeVowKit; }; +/** @typedef {ReturnType} MakeVowKit */ harden(prepareVowKit); diff --git a/packages/vow/test/asVow.test.js b/packages/vow/test/asVow.test.js new file mode 100644 index 00000000000..3109c74e027 --- /dev/null +++ b/packages/vow/test/asVow.test.js @@ -0,0 +1,54 @@ +// @ts-check +import test from 'ava'; + +import { E } from '@endo/far'; +import { makeHeapZone } from '@agoric/base-zone/heap.js'; + +import { prepareVowTools } from '../src/tools.js'; +import { getVowPayload, isVow } from '../src/vow-utils.js'; + +test('asVow takes a function that throws/returns synchronously and returns a vow', async t => { + const { watch, when, asVow } = prepareVowTools(makeHeapZone()); + + const fnThatThrows = () => { + throw Error('fail'); + }; + + const vowWithRejection = asVow(fnThatThrows); + t.true(isVow(vowWithRejection)); + await t.throwsAsync( + when(vowWithRejection), + { message: 'fail' }, + 'error should propogate as promise rejection', + ); + + const isWatchAble = watch(asVow(fnThatThrows)); + t.true(isVow(vowWithRejection)); + await t.throwsAsync(when(isWatchAble), { message: 'fail' }); + + const fnThatReturns = () => { + return 'early return'; + }; + const vowWithReturn = asVow(fnThatReturns); + t.true(isVow(vowWithReturn)); + t.is(await when(vowWithReturn), 'early return'); + t.is(await when(watch(vowWithReturn)), 'early return'); +}); + +test('asVow does not resolve a vow to a vow', async t => { + const { watch, when, asVow } = prepareVowTools(makeHeapZone()); + + const testVow = watch(Promise.resolve('payload')); + const testVowAsVow = asVow(() => testVow); + + const vowPayload = getVowPayload(testVowAsVow); + assert(vowPayload?.vowV0, 'testVowAsVow is a vow'); + const unwrappedOnce = await E(vowPayload.vowV0).shorten(); + t.false( + isVow(unwrappedOnce), + 'vows passed to asVow are not rewrapped as vows', + ); + t.is(unwrappedOnce, 'payload'); + + t.is(await when(testVow), await when(testVowAsVow), 'result is preserved'); +});