diff --git a/packages/vow/src/watch.js b/packages/vow/src/watch.js index 9d9651ac8d0a..9391dfeefb73 100644 --- a/packages/vow/src/watch.js +++ b/packages/vow/src/watch.js @@ -65,8 +65,23 @@ const settle = (resolver, watcher, wcb, value, watcherArgs = []) => { * @param {IsRetryableReason} isRetryableReason * @param {ReturnType} watchNextStep */ -const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => - zone.exoClass( +const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => { + // We use an ephemeral WeakSet for the previously seen vows in a watch operation + // While watch is durable, it suffices to detect the cycle in a single incarnation + /** @type {WeakMap>} */ + const watcherSeenPayloads = new WeakMap(); + + /** @param {PromiseWatcher} watcher */ + const getSeenPayloads = watcher => { + let seenPayloads = watcherSeenPayloads.get(watcher); + if (!seenPayloads) { + seenPayloads = new WeakSet(); + watcherSeenPayloads.set(watcher, seenPayloads); + } + return seenPayloads; + }; + + return zone.exoClass( 'PromiseWatcher', PromiseWatcherI, /** @@ -81,7 +96,6 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => const state = { vow: /** @type {unknown} */ (undefined), priorRetryValue: /** @type {any} */ (undefined), - seenPayloads: zone.detached().weakSetStore('seenPayloads'), resolver, watcher, watcherArgs: harden(watcherArgs), @@ -91,19 +105,21 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => { /** @type {Required['onFulfilled']} */ onFulfilled(value) { - const { watcher, watcherArgs, resolver, seenPayloads } = this.state; + const { watcher, watcherArgs, resolver } = this.state; const payload = getVowPayload(value); if (payload) { + const seenPayloads = getSeenPayloads(this.self); // TODO: rely on endowed helper to get storable cap from payload - if (seenPayloads?.has(payload.vowV0)) { + if (seenPayloads.has(payload.vowV0)) { return this.self.onRejected(Error('Vow resolution cycle detected')); } - seenPayloads?.add(payload.vowV0); + seenPayloads.add(payload.vowV0); // We've been shortened, so reflect our state accordingly, and go again. this.state.vow = value; watchNextStep(value, this.self); return; } + watcherSeenPayloads.delete(this.self); this.state.priorRetryValue = undefined; this.state.watcher = undefined; this.state.resolver = undefined; @@ -122,6 +138,7 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => return; } } + watcherSeenPayloads.delete(this.self); this.state.priorRetryValue = undefined; this.state.resolver = undefined; this.state.watcher = undefined; @@ -129,6 +146,7 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => }, }, ); +}; /** * @param {Zone} zone