Skip to content

Commit

Permalink
Upgrade-16 cherry-picks round 3 (#9640)
Browse files Browse the repository at this point in the history
git rebase-todo
```
# Branch pc/as-vow
label base-pc-as-vow
pick b6b5f5f feat(vowTools): add asVow helper
pick 0cdcd5f feat(vowTools): asVow should not wrap a vow as a vow
label pc-as-vow
reset base-pc-as-vow
merge -C 0ad10c6 pc-as-vow # feat(vowTools): add asVow helper (#9577)

# Branch pc/watch-utils-as-promise
label base-pc-watch-utils-as-promise
pick bf430a1 feat(watchUtils): add asPromise helper
# To resolve conflicts in the following commit:
# * In packages/vow/src/types.js, keep inbound changes and (after that) the
#   definition of VowTools.
# * In packages/vow/test/watch-utils.test.js, keep only inbound changes and
#   limit them to start at "asPromise converts a vow to a promise" (i.e.,
#   just the additions of
#   https://github.com/Agoric/agoric-sdk/pull/9620/files#diff-fd7cabcc7d1097036e2981c3262aa20612bec349f6e418d53324fc33b1b26946 ).
pick c940d5c feat(vowTools): asPromise helper for unwrapping vows
pick 8c27c67 feat(watchUtils): handle non-storables
pick 3d5a3f3 feat(types): EVow
pick 1c0b964 refactor: don't re-use index
pick 274df18 fix(vow): clearer stored/non-stored values
pick ff92211 refactor: 'extra' field for future properties
label pc-watch-utils-as-promise
reset base-pc-watch-utils-as-promise
merge -C 8edf902 pc-watch-utils-as-promise # feat(vows): improve handling of ephemeral values (#9620)
```
  • Loading branch information
gibson042 authored Jul 2, 2024
2 parents b7c2f02 + cb81030 commit 91eb8f4
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 44 deletions.
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
25 changes: 19 additions & 6 deletions packages/vow/src/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ 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 {Zone} from '@agoric/base-zone';
* @import {IsRetryableReason, AsPromiseFunction, EVow} from './types.js';
*/

/**
* @param {Zone} zone
Expand All @@ -18,16 +21,26 @@ 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 });
return harden({ when, watch, makeVowKit, allVows, asVow, asPromise });
};
harden(prepareVowTools);
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 @@ -87,4 +93,18 @@ export {};
* @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>}
*/

/** @typedef {ReturnType<typeof prepareVowTools>} VowTools */
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);
79 changes: 67 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) {
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';
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 All @@ -157,5 +211,6 @@ export const prepareVowKit = zone => {

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

harden(prepareVowKit);
Loading

0 comments on commit 91eb8f4

Please sign in to comment.