Skip to content

Commit

Permalink
feat(vowTools): add asVow helper (#9577)
Browse files Browse the repository at this point in the history
refs: #9449

## Description

- Add `asVow` helper to `VowTools` to ensure we always return vows, even in the event of early synchronous errors.
- Implement the helper in `ChainAccountKit` against #9562
  • Loading branch information
mergify[bot] authored Jun 25, 2024
2 parents 3aa5d66 + 0cdcd5f commit 0ad10c6
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 3 deletions.
4 changes: 3 additions & 1 deletion packages/vow/src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' */
Expand All @@ -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.
Expand 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);

Expand Down
32 changes: 30 additions & 2 deletions packages/vow/src/vow-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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<T>> | Awaited<T>} fn
* @returns {Vow<Awaited<T>>}
*/
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);
1 change: 1 addition & 0 deletions packages/vow/src/vow.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,6 @@ export const prepareVowKit = zone => {

return makeVowKit;
};
/** @typedef {ReturnType<typeof prepareVowKit>} MakeVowKit */

harden(prepareVowKit);
54 changes: 54 additions & 0 deletions packages/vow/test/asVow.test.js
Original file line number Diff line number Diff line change
@@ -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');
});

0 comments on commit 0ad10c6

Please sign in to comment.