From ada94d40058dba746d500de2edf69364d75fa9cc Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 24 Jun 2024 18:35:17 -0400 Subject: [PATCH] feat(vowTools): add asVow helper - adds asVow() helper function that coerces the result of a function to a Vow - see comment from @mhofman for inspiration: https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 --- packages/vow/src/tools.js | 6 ++++-- packages/vow/src/vow-utils.js | 27 +++++++++++++++++++++++++-- packages/vow/src/vow.js | 1 + packages/vow/test/asVow.test.js | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 packages/vow/test/asVow.test.js diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 7351457b6a5..595dad161f5 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -3,9 +3,10 @@ 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' */ +/** @import {IsRetryableReason, Vow} from './types.js' */ /** * @param {Zone} zone @@ -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,6 +30,6 @@ 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..664bfa0698d 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,25 @@ 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 => { + const kit = makeVowKit(); + try { + kit.resolver.resolve(fn()); + } catch (e) { + kit.resolver.reject(e); + } + 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..25d22005584 --- /dev/null +++ b/packages/vow/test/asVow.test.js @@ -0,0 +1,32 @@ +// @ts-check +import test from 'ava'; + +import { makeHeapZone } from '@agoric/base-zone/heap.js'; + +import { prepareVowTools } from '../src/tools.js'; +import { isVow } from '../src/vow-utils.js'; + +test('asVow takes a function that throws/returns synchronously and returns a vow', async t => { + const zone = makeHeapZone(); + const { watch, when, asVow } = prepareVowTools(zone); + + const fnThatThrows = () => { + throw Error('fail'); + }; + + const vowWithRejection = asVow(fnThatThrows); + t.true(isVow(vowWithRejection)); + await t.throwsAsync(when(vowWithRejection), { message: 'fail' }, 'failure '); + + const isWatchAble = watch(asVow(fnThatThrows)); + t.true(isVow(vowWithRejection)); + await t.throwsAsync(when(isWatchAble), { message: 'fail' }, 'failure '); + + 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'); +});