Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vowTools): add asVow helper #9577

Merged
merged 2 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => {
mhofman marked this conversation as resolved.
Show resolved Hide resolved
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');
});
Loading