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(vows): improve handling of ephemeral values #9620

Merged
merged 7 commits into from
Jul 1, 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
2 changes: 1 addition & 1 deletion packages/base-zone/src/watch-promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { apply } = Reflect;
/**
* A PromiseWatcher method guard callable with or more arguments, returning void.
*/
export const PromiseWatcherHandler = M.call(M.any()).rest(M.any()).returns();
export const PromiseWatcherHandler = M.call(M.raw()).rest(M.raw()).returns();

/**
* A PromiseWatcher interface that has both onFulfilled and onRejected handlers.
Expand Down
23 changes: 17 additions & 6 deletions packages/vow/src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ 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 {Zone} from '@agoric/base-zone';
* @import {IsRetryableReason, AsPromiseFunction, EVow} from './types.js';
*/

/**
* @param {Zone} zone
Expand All @@ -19,18 +21,27 @@ export const prepareVowTools = (zone, powers = {}) => {
const makeVowKit = prepareVowKit(zone);
const when = makeWhen(isRetryableReason);
const watch = prepareWatch(zone, makeVowKit, isRetryableReason);
const makeWatchUtils = prepareWatchUtils(zone, watch, makeVowKit);
const makeWatchUtils = prepareWatchUtils(zone, {
watch,
when,
makeVowKit,
isRetryableReason,
});
const watchUtils = makeWatchUtils();
const asVow = makeAsVow(makeVowKit);

/**
* Vow-tolerant implementation of Promise.all.
*
* @param {unknown[]} vows
* @param {EVow<unknown>[]} maybeVows
*/
const allVows = vows => watchUtils.all(vows);
const allVows = maybeVows => watchUtils.all(maybeVows);

/** @type {AsPromiseFunction} */
const asPromise = (specimenP, ...watcherArgs) =>
watchUtils.asPromise(specimenP, ...watcherArgs);

return harden({ when, watch, makeVowKit, allVows, asVow });
return harden({ when, watch, makeVowKit, allVows, asVow, asPromise });
};
harden(prepareVowTools);

Expand Down
20 changes: 20 additions & 0 deletions packages/vow/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export {};
* @typedef {T | PromiseLike<T>} ERef
*/

/**
* Eventually a value T or Vow for it.
* @template T
* @typedef {ERef<T | Vow<T>>} EVow
*/

/**
* Follow the chain of vow shortening to the end, returning the final value.
* This is used within E, so we must narrow the type to its remote form.
Expand Down Expand Up @@ -86,3 +92,17 @@ export {};
* @property {(value: T, ...args: C) => Vow<TResult1> | PromiseVow<TResult1> | TResult1} [onFulfilled]
* @property {(reason: any, ...args: C) => Vow<TResult2> | PromiseVow<TResult2> | TResult2} [onRejected]
*/

/**
* Converts a vow or promise to a promise, ensuring proper handling of ephemeral promises.
*
* @template [T=any]
* @template [TResult1=T]
* @template [TResult2=never]
* @template {any[]} [C=any[]]
* @callback AsPromiseFunction
* @param {ERef<T | Vow<T>>} specimenP
* @param {Watcher<T, TResult1, TResult2, C>} [watcher]
* @param {C} [watcherArgs]
* @returns {Promise<TResult1 | TResult2>}
*/
Copy link
Member Author

@0xpatrickdev 0xpatrickdev Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is identical to the type for watch, minus the return line. We might consider extracting that logic and extending it, and/or using something like the PromiseToVow helper from @agoric/orchestration that should maybe live here instead:

/**
* Converts a function type that returns a Promise to a function type that
* returns a Vow. If the input is not a function returning a Promise, it
* preserves the original type.
*
* @template T - The type to transform
*/
export type PromiseToVow<T> = T extends (
...args: infer Args
) => Promise<infer R>
? (...args: Args) => Vow<R>
: T extends (...args: infer Args) => infer R
? (...args: Args) => R
: T;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't lines 14-15 above be removed since they're redundant?

78 changes: 66 additions & 12 deletions packages/vow/src/vow.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { M } from '@endo/patterns';
import { makeTagged } from '@endo/pass-style';
import { PromiseWatcherI } from '@agoric/base-zone';

const { details: X } = assert;

/**
* @import {PromiseKit} from '@endo/promise-kit'
* @import {Zone} from '@agoric/base-zone'
* @import {VowResolver, VowKit} from './types.js'
* @import {PromiseKit} from '@endo/promise-kit';
* @import {Zone} from '@agoric/base-zone';
* @import {MapStore} from '@agoric/store';
* @import {VowResolver, VowKit} from './types.js';
*/

const sink = () => {};
Expand All @@ -25,6 +28,9 @@ export const prepareVowKit = zone => {
/** @type {WeakMap<VowResolver, VowEphemera>} */
const resolverToEphemera = new WeakMap();

/** @type {WeakMap<VowResolver, any>} */
const resolverToNonStoredValue = new WeakMap();

/**
* Get the current incarnation's promise kit associated with a vowV0.
*
Expand Down Expand Up @@ -61,30 +67,55 @@ export const prepareVowKit = zone => {
shorten: M.call().returns(M.promise()),
}),
resolver: M.interface('VowResolver', {
resolve: M.call().optional(M.any()).returns(),
reject: M.call().optional(M.any()).returns(),
resolve: M.call().optional(M.raw()).returns(),
reject: M.call().optional(M.raw()).returns(),
}),
watchNextStep: PromiseWatcherI,
},
() => ({
value: undefined,
value: /** @type {any} */ (undefined),
// The stepStatus is null if the promise step hasn't settled yet.
stepStatus: /** @type {null | 'pending' | 'fulfilled' | 'rejected'} */ (
null
),
isStoredValue: /** @type {boolean} */ (false),
/**
* Map for future properties that aren't in the schema.
* UNTIL https://github.com/Agoric/agoric-sdk/issues/7407
* @type {MapStore<any, any> | undefined}
*/
extra: undefined,
}),
{
vowV0: {
/**
* @returns {Promise<any>}
*/
async shorten() {
const { stepStatus, value } = this.state;
const { stepStatus, isStoredValue, value } = this.state;
const { resolver } = this.facets;

switch (stepStatus) {
case 'fulfilled':
return value;
case 'rejected':
case 'fulfilled': {
if (isStoredValue) {
// Always return a stored fulfilled value.
return value;
} else if (resolverToNonStoredValue.has(resolver)) {
// Non-stored value is available.
return resolverToNonStoredValue.get(resolver);
}
// We can't recover the non-stored value, so throw the
// explanation.
throw value;
}
case 'rejected': {
if (!isStoredValue && resolverToNonStoredValue.has(resolver)) {
// Non-stored reason is available.
throw resolverToNonStoredValue.get(resolver);
}
// Always throw a stored rejection reason.
throw value;
}
case null:
case 'pending':
return provideCurrentKit(this.facets.resolver).promise;
Expand Down Expand Up @@ -131,15 +162,38 @@ export const prepareVowKit = zone => {
onFulfilled(value) {
const { resolver } = this.facets;
const { resolve } = getPromiseKitForResolution(resolver);
harden(value);
if (resolve) {
0xpatrickdev marked this conversation as resolved.
Show resolved Hide resolved
resolve(value);
}
this.state.stepStatus = 'fulfilled';
this.state.value = value;
this.state.isStoredValue = zone.isStorable(value);
if (this.state.isStoredValue) {
this.state.value = value;
} else {
resolverToNonStoredValue.set(resolver, value);
this.state.value = assert.error(
X`Vow fulfillment value was not stored: ${value}`,
);
}
},
onRejected(reason) {
const { resolver } = this.facets;
const { reject } = getPromiseKitForResolution(resolver);
harden(reason);
if (reject) {
reject(reason);
}
this.state.stepStatus = 'rejected';
0xpatrickdev marked this conversation as resolved.
Show resolved Hide resolved
this.state.value = reason;
this.state.isStoredValue = zone.isStorable(reason);
if (this.state.isStoredValue) {
this.state.value = reason;
} else {
resolverToNonStoredValue.set(resolver, reason);
this.state.value = assert.error(
X`Vow rejection reason was not stored: ${reason}`,
);
}
},
},
},
Expand Down
Loading
Loading