From bf430a12afa853b332fd6cfdcb77781d544b0e7c Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Sun, 30 Jun 2024 15:48:36 -0400 Subject: [PATCH 1/7] feat(watchUtils): add asPromise helper Co-authored-by: Michael FIG --- packages/vow/src/tools.js | 7 +++++- packages/vow/src/watch-utils.js | 38 ++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 2f0edad6bf6..621159f0d06 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -19,7 +19,12 @@ 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); diff --git a/packages/vow/src/watch-utils.js b/packages/vow/src/watch-utils.js index a198906555d..a77f82ae71b 100644 --- a/packages/vow/src/watch-utils.js +++ b/packages/vow/src/watch-utils.js @@ -1,12 +1,17 @@ // @ts-check import { M } from '@endo/patterns'; +import { PromiseWatcherI } from '@agoric/base-zone'; + +const { Fail, bare } = assert; /** * @import {MapStore} from '@agoric/store/src/types.js' * @import { Zone } from '@agoric/base-zone' * @import { Watch } from './watch.js' + * @import { When } from './when.js' * @import {VowKit} from './types.js' + * @import {IsRetryableReason} from './types.js' */ const VowShape = M.tagged( @@ -18,21 +23,29 @@ const VowShape = M.tagged( /** * @param {Zone} zone - * @param {Watch} watch - * @param {() => VowKit} makeVowKit + * @param {object} powers + * @param {Watch} powers.watch + * @param {When} powers.when + * @param {() => VowKit} powers.makeVowKit + * @param {IsRetryableReason} powers.isRetryableReason */ -export const prepareWatchUtils = (zone, watch, makeVowKit) => { +export const prepareWatchUtils = ( + zone, + { watch, when, makeVowKit, isRetryableReason }, +) => { const detached = zone.detached(); const makeWatchUtilsKit = zone.exoClassKit( 'WatchUtils', { utils: M.interface('Utils', { all: M.call(M.arrayOf(M.any())).returns(VowShape), + asPromise: M.call(M.raw()).rest(M.raw()).returns(M.promise()), }), watcher: M.interface('Watcher', { onFulfilled: M.call(M.any()).rest(M.any()).returns(M.any()), onRejected: M.call(M.any()).rest(M.any()).returns(M.any()), }), + retryRejectionPromiseWatcher: PromiseWatcherI, }, () => { /** @@ -83,6 +96,17 @@ export const prepareWatchUtils = (zone, watch, makeVowKit) => { } return kit.vow; }, + asPromise(specimenP, ...watcherArgs) { + // Watch the specimen in case it is an ephemeral promise. + const vow = watch(specimenP, ...watcherArgs); + const promise = when(vow); + // Watch the ephemeral result promise to ensure that if its settlement is + // lost due to upgrade of this incarnation, we will at least cause an + // unhandled rejection in the new incarnation. + zone.watchPromise(promise, this.facets.retryRejectionPromiseWatcher); + + return promise; + }, }, watcher: { onFulfilled(value, { id, index }) { @@ -122,6 +146,14 @@ export const prepareWatchUtils = (zone, watch, makeVowKit) => { resolver.reject(value); }, }, + retryRejectionPromiseWatcher: { + onFulfilled(_result) {}, + onRejected(reason, failedOp) { + if (isRetryableReason(reason, undefined)) { + Fail`Pending ${bare(failedOp)} could not retry; {reason}`; + } + }, + }, }, ); From c940d5ca7356428d2bda78af17942dc76fef59dc Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Sun, 30 Jun 2024 17:32:45 -0400 Subject: [PATCH 2/7] feat(vowTools): asPromise helper for unwrapping vows --- packages/vow/src/tools.js | 8 +++-- packages/vow/src/types.js | 14 ++++++++ packages/vow/src/watch-utils.js | 3 +- packages/vow/test/watch-utils.test.js | 52 +++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 621159f0d06..0698731942f 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -6,7 +6,7 @@ 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, AsPromiseFunction} from './types.js' */ /** * @param {Zone} zone @@ -35,7 +35,11 @@ export const prepareVowTools = (zone, powers = {}) => { */ const allVows = vows => watchUtils.all(vows); - return harden({ when, watch, makeVowKit, allVows, asVow }); + /** @type {AsPromiseFunction} */ + const asPromise = (specimenP, ...watcherArgs) => + watchUtils.asPromise(specimenP, ...watcherArgs); + + return harden({ when, watch, makeVowKit, allVows, asVow, asPromise }); }; harden(prepareVowTools); diff --git a/packages/vow/src/types.js b/packages/vow/src/types.js index e70011277ce..f81bf55f9a3 100644 --- a/packages/vow/src/types.js +++ b/packages/vow/src/types.js @@ -86,3 +86,17 @@ export {}; * @property {(value: T, ...args: C) => Vow | PromiseVow | TResult1} [onFulfilled] * @property {(reason: any, ...args: C) => Vow | PromiseVow | 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>} specimenP + * @param {Watcher} [watcher] + * @param {C} [watcherArgs] + * @returns {Promise} + */ diff --git a/packages/vow/src/watch-utils.js b/packages/vow/src/watch-utils.js index a77f82ae71b..d7068033102 100644 --- a/packages/vow/src/watch-utils.js +++ b/packages/vow/src/watch-utils.js @@ -10,7 +10,7 @@ const { Fail, bare } = assert; * @import { Zone } from '@agoric/base-zone' * @import { Watch } from './watch.js' * @import { When } from './when.js' - * @import {VowKit} from './types.js' + * @import {VowKit, AsPromiseFunction} from './types.js' * @import {IsRetryableReason} from './types.js' */ @@ -96,6 +96,7 @@ export const prepareWatchUtils = ( } return kit.vow; }, + /** @type {AsPromiseFunction} */ asPromise(specimenP, ...watcherArgs) { // Watch the specimen in case it is an ephemeral promise. const vow = watch(specimenP, ...watcherArgs); diff --git a/packages/vow/test/watch-utils.test.js b/packages/vow/test/watch-utils.test.js index d0abc41abac..701e2d64eae 100644 --- a/packages/vow/test/watch-utils.test.js +++ b/packages/vow/test/watch-utils.test.js @@ -199,3 +199,55 @@ test('allVows does NOT support Vow pipelining', async t => { message: 'target has no method "getAddress", has []', }); }); + +test('asPromise converts a vow to a promise', async t => { + const zone = makeHeapZone(); + const { watch, asPromise } = prepareVowTools(zone); + + const testPromiseP = Promise.resolve('test value'); + const vow = watch(testPromiseP); + + const result = await asPromise(vow); + t.is(result, 'test value'); +}); + +test('asPromise handles vow rejection', async t => { + const zone = makeHeapZone(); + const { watch, asPromise } = prepareVowTools(zone); + + const testPromiseP = Promise.reject(new Error('test error')); + const vow = watch(testPromiseP); + + await t.throwsAsync(asPromise(vow), { message: 'test error' }); +}); + +test('asPromise accepts and resolves promises', async t => { + const zone = makeHeapZone(); + const { asPromise } = prepareVowTools(zone); + + const p = Promise.resolve('a promise'); + const result = await asPromise(p); + t.is(result, 'a promise'); +}); + +test('asPromise handles watcher arguments', async t => { + const zone = makeHeapZone(); + const { watch, asPromise } = prepareVowTools(zone); + + const testPromiseP = Promise.resolve('watcher test'); + const vow = watch(testPromiseP); + + let watcherCalled = false; + const watcher = { + onFulfilled(value, ctx) { + watcherCalled = true; + t.is(value, 'watcher test'); + t.deepEqual(ctx, ['ctx']); + return value; + }, + }; + + const result = await asPromise(vow, watcher, ['ctx']); + t.is(result, 'watcher test'); + t.true(watcherCalled); +}); From 8c27c6725ba7ef4b71d3ab0ccfdbddd755bcd926 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Sun, 30 Jun 2024 20:20:48 -0400 Subject: [PATCH 3/7] feat(watchUtils): handle non-storables Co-authored-by: Michael FIG --- packages/base-zone/src/watch-promise.js | 2 +- packages/vow/src/tools.js | 6 +- packages/vow/src/vow.js | 50 +++++++++++++---- packages/vow/src/watch-utils.js | 74 +++++++++++++++++++++---- 4 files changed, 108 insertions(+), 24 deletions(-) diff --git a/packages/base-zone/src/watch-promise.js b/packages/base-zone/src/watch-promise.js index 4693bd98377..4efec17195f 100644 --- a/packages/base-zone/src/watch-promise.js +++ b/packages/base-zone/src/watch-promise.js @@ -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. diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 0698731942f..a318415ee12 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -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, AsPromiseFunction} from './types.js' */ +/** + * @import {Zone} from '@agoric/base-zone'; + * @import {IsRetryableReason, AsPromiseFunction} from './types.js'; + */ /** * @param {Zone} zone diff --git a/packages/vow/src/vow.js b/packages/vow/src/vow.js index 1a1ca4a472e..7566b2cb3a1 100644 --- a/packages/vow/src/vow.js +++ b/packages/vow/src/vow.js @@ -4,10 +4,12 @@ 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 {VowResolver, VowKit} from './types.js'; */ const sink = () => {}; @@ -61,13 +63,13 @@ 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 @@ -80,11 +82,18 @@ export const prepareVowKit = zone => { */ async shorten() { const { stepStatus, value } = this.state; + const { resolver } = this.facets; + const ephemera = resolverToEphemera.get(resolver); + switch (stepStatus) { - case 'fulfilled': + case 'fulfilled': { + if (ephemera) return ephemera.promise; return value; - case 'rejected': + } + case 'rejected': { + if (ephemera) return ephemera.promise; throw value; + } case null: case 'pending': return provideCurrentKit(this.facets.resolver).promise; @@ -129,17 +138,36 @@ export const prepareVowKit = zone => { }, watchNextStep: { onFulfilled(value) { - const { resolver } = this.facets; + const { resolver, watchNextStep } = this.facets; const { resolve } = getPromiseKitForResolution(resolver); + harden(value); if (resolve) { resolve(value); } this.state.stepStatus = 'fulfilled'; - this.state.value = value; + if (zone.isStorable(value)) { + this.state.value = value; + } else { + watchNextStep.onRejected( + assert.error(X`Vow fulfillment value is not storable: ${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; + if (zone.isStorable(reason)) { + this.state.value = reason; + } else { + this.state.value = assert.error( + X`Vow rejection reason is not storable: ${reason}`, + ); + } }, }, }, diff --git a/packages/vow/src/watch-utils.js b/packages/vow/src/watch-utils.js index d7068033102..c314883a301 100644 --- a/packages/vow/src/watch-utils.js +++ b/packages/vow/src/watch-utils.js @@ -3,7 +3,7 @@ import { M } from '@endo/patterns'; import { PromiseWatcherI } from '@agoric/base-zone'; -const { Fail, bare } = assert; +const { Fail, bare, details: X } = assert; /** * @import {MapStore} from '@agoric/store/src/types.js' @@ -21,6 +21,20 @@ const VowShape = M.tagged( }), ); +/** + * Like `provideLazy`, but accepts non-Passable values. + * + * @param {WeakMap} map + * @param {any} key + * @param {(key: any) => any} makeValue + */ +const provideLazyMap = (map, key, makeValue) => { + if (!map.has(key)) { + map.set(key, makeValue(key)); + } + return map.get(key); +}; + /** * @param {Zone} zone * @param {object} powers @@ -34,6 +48,8 @@ export const prepareWatchUtils = ( { watch, when, makeVowKit, isRetryableReason }, ) => { const detached = zone.detached(); + const utilsToNonStorableResults = new WeakMap(); + const makeWatchUtilsKit = zone.exoClassKit( 'WatchUtils', { @@ -75,7 +91,11 @@ export const prepareWatchUtils = ( // Preserve the order of the vow results. let index = 0; for (const vow of vows) { - watch(vow, this.facets.watcher, { id, index }); + watch(vow, this.facets.watcher, { + id, + index, + numResults: vows.length, + }); index += 1; } @@ -90,6 +110,12 @@ export const prepareWatchUtils = ( resultsMap: detached.mapStore('resultsMap'), }), ); + const idToNonStorableResults = provideLazyMap( + utilsToNonStorableResults, + this.facets.utils, + () => new Map(), + ); + idToNonStorableResults.set(id, new Map()); } else { // Base case: nothing to wait for. kit.resolver.resolve(harden([])); @@ -110,15 +136,30 @@ export const prepareWatchUtils = ( }, }, watcher: { - onFulfilled(value, { id, index }) { + onFulfilled(value, { id, index, numResults }) { const { idToVowState } = this.state; if (!idToVowState.has(id)) { // Resolution of the returned vow happened already. return; } const { remaining, resultsMap, resolver } = idToVowState.get(id); + const idToNonStorableResults = provideLazyMap( + utilsToNonStorableResults, + this.facets.utils, + () => new Map(), + ); + const nonStorableResults = provideLazyMap( + idToNonStorableResults, + id, + () => new Map(), + ); + // Capture the fulfilled value. - resultsMap.init(index, value); + if (zone.isStorable(value)) { + resultsMap.init(index, value); + } else { + nonStorableResults.set(index, value); + } const vowState = harden({ remaining: remaining - 1, resultsMap, @@ -130,13 +171,26 @@ export const prepareWatchUtils = ( } // We're done! Extract the array. idToVowState.delete(id); - const results = new Array(resultsMap.getSize()); - for (const [i, val] of resultsMap.entries()) { - results[i] = val; + const results = new Array(numResults); + let numLost = 0; + for (let i = 0; i < numResults; i += 1) { + if (nonStorableResults.has(i)) { + results[i] = nonStorableResults.get(i); + } else if (resultsMap.has(i)) { + results[i] = resultsMap.get(i); + } else { + numLost += 1; + } + } + if (numLost > 0) { + resolver.reject( + assert.error(X`${numLost} unstorable results were lost`), + ); + } else { + resolver.resolve(harden(results)); } - resolver.resolve(harden(results)); }, - onRejected(value, { id, index: _index }) { + onRejected(value, { id, index: _index, numResults: _numResults }) { const { idToVowState } = this.state; if (!idToVowState.has(id)) { // First rejection wins. @@ -151,7 +205,7 @@ export const prepareWatchUtils = ( onFulfilled(_result) {}, onRejected(reason, failedOp) { if (isRetryableReason(reason, undefined)) { - Fail`Pending ${bare(failedOp)} could not retry; {reason}`; + Fail`Pending ${bare(failedOp)} could not retry; ${reason}`; } }, }, From 3d5a3f3e44e328e102d7db197c0b06b18a5c63fe Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 1 Jul 2024 11:51:40 -0700 Subject: [PATCH 4/7] feat(types): EVow --- packages/vow/src/tools.js | 6 +++--- packages/vow/src/types.js | 6 ++++++ packages/vow/src/watch-utils.js | 13 ++++++------- packages/vow/src/watch.js | 4 ++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index a318415ee12..c93cab4b5a3 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -7,7 +7,7 @@ import { makeAsVow } from './vow-utils.js'; /** * @import {Zone} from '@agoric/base-zone'; - * @import {IsRetryableReason, AsPromiseFunction} from './types.js'; + * @import {IsRetryableReason, AsPromiseFunction, Vow, EVow} from './types.js'; */ /** @@ -33,9 +33,9 @@ export const prepareVowTools = (zone, powers = {}) => { /** * Vow-tolerant implementation of Promise.all. * - * @param {unknown[]} vows + * @param {EVow[]} maybeVows */ - const allVows = vows => watchUtils.all(vows); + const allVows = maybeVows => watchUtils.all(maybeVows); /** @type {AsPromiseFunction} */ const asPromise = (specimenP, ...watcherArgs) => diff --git a/packages/vow/src/types.js b/packages/vow/src/types.js index f81bf55f9a3..15b894b244e 100644 --- a/packages/vow/src/types.js +++ b/packages/vow/src/types.js @@ -29,6 +29,12 @@ export {}; * @typedef {T | PromiseLike} ERef */ +/** + * Eventually a value T or Vow for it. + * @template T + * @typedef {ERef>} 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. diff --git a/packages/vow/src/watch-utils.js b/packages/vow/src/watch-utils.js index c314883a301..9af06f3ab6a 100644 --- a/packages/vow/src/watch-utils.js +++ b/packages/vow/src/watch-utils.js @@ -6,12 +6,11 @@ import { PromiseWatcherI } from '@agoric/base-zone'; const { Fail, bare, details: X } = assert; /** - * @import {MapStore} from '@agoric/store/src/types.js' - * @import { Zone } from '@agoric/base-zone' - * @import { Watch } from './watch.js' - * @import { When } from './when.js' - * @import {VowKit, AsPromiseFunction} from './types.js' - * @import {IsRetryableReason} from './types.js' + * @import {MapStore} from '@agoric/store/src/types.js'; + * @import {Zone} from '@agoric/base-zone'; + * @import {Watch} from './watch.js'; + * @import {When} from './when.js'; + * @import {VowKit, AsPromiseFunction, IsRetryableReason, Vow, EVow} from './types.js'; */ const VowShape = M.tagged( @@ -81,7 +80,7 @@ export const prepareWatchUtils = ( { utils: { /** - * @param {unknown[]} vows + * @param {EVow[]} vows */ all(vows) { const { nextId: id, idToVowState } = this.state; diff --git a/packages/vow/src/watch.js b/packages/vow/src/watch.js index 9391dfeefb7..fb5a793500b 100644 --- a/packages/vow/src/watch.js +++ b/packages/vow/src/watch.js @@ -6,7 +6,7 @@ const { apply } = Reflect; /** * @import { PromiseWatcher, Zone } from '@agoric/base-zone'; - * @import { ERef, IsRetryableReason, Vow, VowKit, VowResolver, Watcher } from './types.js'; + * @import { ERef, EVow, IsRetryableReason, Vow, VowKit, VowResolver, Watcher } from './types.js'; */ /** @@ -170,7 +170,7 @@ export const prepareWatch = ( * @template [TResult1=T] * @template [TResult2=never] * @template {any[]} [C=any[]] watcher args - * @param {ERef>} specimenP + * @param {EVow} specimenP * @param {Watcher} [watcher] * @param {C} watcherArgs */ From 1c0b96468c6199db2a66ec5fb617ec022520ceba Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 1 Jul 2024 12:29:37 -0700 Subject: [PATCH 5/7] refactor: don't re-use index --- packages/vow/src/watch-utils.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/vow/src/watch-utils.js b/packages/vow/src/watch-utils.js index 9af06f3ab6a..24e70909649 100644 --- a/packages/vow/src/watch-utils.js +++ b/packages/vow/src/watch-utils.js @@ -88,24 +88,22 @@ export const prepareWatchUtils = ( const kit = makeVowKit(); // Preserve the order of the vow results. - let index = 0; - for (const vow of vows) { - watch(vow, this.facets.watcher, { + for (let index = 0; index < vows.length; index += 1) { + watch(vows[index], this.facets.watcher, { id, index, numResults: vows.length, }); - index += 1; } - if (index > 0) { + if (vows.length > 0) { // Save the state until rejection or all fulfilled. this.state.nextId += 1n; idToVowState.init( id, harden({ resolver: kit.resolver, - remaining: index, + remaining: vows.length, resultsMap: detached.mapStore('resultsMap'), }), ); From 274df1833f000af9971d2015a25afd89d89fdbf6 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sun, 30 Jun 2024 21:02:48 -0600 Subject: [PATCH 6/7] fix(vow): clearer stored/non-stored values --- packages/vow/src/vow.js | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/vow/src/vow.js b/packages/vow/src/vow.js index 7566b2cb3a1..4612cc68878 100644 --- a/packages/vow/src/vow.js +++ b/packages/vow/src/vow.js @@ -27,6 +27,9 @@ export const prepareVowKit = zone => { /** @type {WeakMap} */ const resolverToEphemera = new WeakMap(); + /** @type {WeakMap} */ + const resolverToNonStoredValue = new WeakMap(); + /** * Get the current incarnation's promise kit associated with a vowV0. * @@ -74,6 +77,7 @@ export const prepareVowKit = zone => { stepStatus: /** @type {null | 'pending' | 'fulfilled' | 'rejected'} */ ( null ), + isStoredValue: /** @type {boolean} */ (false), }), { vowV0: { @@ -81,17 +85,28 @@ export const prepareVowKit = zone => { * @returns {Promise} */ async shorten() { - const { stepStatus, value } = this.state; + const { stepStatus, isStoredValue, value } = this.state; const { resolver } = this.facets; - const ephemera = resolverToEphemera.get(resolver); switch (stepStatus) { case 'fulfilled': { - if (ephemera) return ephemera.promise; - return value; + 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 (ephemera) return ephemera.promise; + if (!isStoredValue && resolverToNonStoredValue.has(resolver)) { + // Non-stored reason is available. + throw resolverToNonStoredValue.get(resolver); + } + // Always throw a stored rejection reason. throw value; } case null: @@ -138,18 +153,20 @@ export const prepareVowKit = zone => { }, watchNextStep: { onFulfilled(value) { - const { resolver, watchNextStep } = this.facets; + const { resolver } = this.facets; const { resolve } = getPromiseKitForResolution(resolver); harden(value); if (resolve) { resolve(value); } this.state.stepStatus = 'fulfilled'; - if (zone.isStorable(value)) { + this.state.isStoredValue = zone.isStorable(value); + if (this.state.isStoredValue) { this.state.value = value; } else { - watchNextStep.onRejected( - assert.error(X`Vow fulfillment value is not storable: ${value}`), + resolverToNonStoredValue.set(resolver, value); + this.state.value = assert.error( + X`Vow fulfillment value was not stored: ${value}`, ); } }, @@ -161,11 +178,13 @@ export const prepareVowKit = zone => { reject(reason); } this.state.stepStatus = 'rejected'; - if (zone.isStorable(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 is not storable: ${reason}`, + X`Vow rejection reason was not stored: ${reason}`, ); } }, From ff92211d0431460030fa89eed6f870ae1b5f5f17 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 1 Jul 2024 10:42:34 -0700 Subject: [PATCH 7/7] refactor: 'extra' field for future properties --- packages/vow/src/tools.js | 2 +- packages/vow/src/vow.js | 7 +++++++ packages/vow/src/watch-utils.js | 2 +- packages/vow/test/watch-utils.test.js | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index c93cab4b5a3..26b98e558ee 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -7,7 +7,7 @@ import { makeAsVow } from './vow-utils.js'; /** * @import {Zone} from '@agoric/base-zone'; - * @import {IsRetryableReason, AsPromiseFunction, Vow, EVow} from './types.js'; + * @import {IsRetryableReason, AsPromiseFunction, EVow} from './types.js'; */ /** diff --git a/packages/vow/src/vow.js b/packages/vow/src/vow.js index 4612cc68878..2b4013eb0cb 100644 --- a/packages/vow/src/vow.js +++ b/packages/vow/src/vow.js @@ -9,6 +9,7 @@ const { details: X } = assert; /** * @import {PromiseKit} from '@endo/promise-kit'; * @import {Zone} from '@agoric/base-zone'; + * @import {MapStore} from '@agoric/store'; * @import {VowResolver, VowKit} from './types.js'; */ @@ -78,6 +79,12 @@ export const prepareVowKit = zone => { 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 | undefined} + */ + extra: undefined, }), { vowV0: { diff --git a/packages/vow/src/watch-utils.js b/packages/vow/src/watch-utils.js index 24e70909649..0bb740530bc 100644 --- a/packages/vow/src/watch-utils.js +++ b/packages/vow/src/watch-utils.js @@ -10,7 +10,7 @@ const { Fail, bare, details: X } = assert; * @import {Zone} from '@agoric/base-zone'; * @import {Watch} from './watch.js'; * @import {When} from './when.js'; - * @import {VowKit, AsPromiseFunction, IsRetryableReason, Vow, EVow} from './types.js'; + * @import {VowKit, AsPromiseFunction, IsRetryableReason, EVow} from './types.js'; */ const VowShape = M.tagged( diff --git a/packages/vow/test/watch-utils.test.js b/packages/vow/test/watch-utils.test.js index 701e2d64eae..e92809ac040 100644 --- a/packages/vow/test/watch-utils.test.js +++ b/packages/vow/test/watch-utils.test.js @@ -247,6 +247,7 @@ test('asPromise handles watcher arguments', async t => { }, }; + // XXX fix type: `watcherContext` doesn't need to be an array const result = await asPromise(vow, watcher, ['ctx']); t.is(result, 'watcher test'); t.true(watcherCalled);