From 57276799015bf478e1b6d2b0199c8dd0934c758a Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Fri, 21 Jun 2024 19:54:27 +0000 Subject: [PATCH] fix(vow): handle resolution loops in vows --- packages/vow/src/watch.js | 10 +++++-- packages/vow/src/when.js | 8 ++++- packages/vow/test/watch.test.js | 53 +++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/vow/src/watch.js b/packages/vow/src/watch.js index c1f87f82095..c8cca5d1ec5 100644 --- a/packages/vow/src/watch.js +++ b/packages/vow/src/watch.js @@ -81,6 +81,7 @@ 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), @@ -90,8 +91,13 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) => { /** @type {Required['onFulfilled']} */ onFulfilled(value) { - const { watcher, watcherArgs, resolver } = this.state; - if (getVowPayload(value)) { + const { watcher, watcherArgs, resolver, seenPayloads } = this.state; + const payload = getVowPayload(value); + if (payload) { + if (seenPayloads?.has(payload.vowV0)) { + return this.self.onRejected(Error('Vow resolution cycle detected')); + } + seenPayloads?.add(payload.vowV0); // We've been shortened, so reflect our state accordingly, and go again. this.state.vow = value; watchNextStep(value, this.self); diff --git a/packages/vow/src/when.js b/packages/vow/src/when.js index ffc47165a5c..7fd1a7ba5f8 100644 --- a/packages/vow/src/when.js +++ b/packages/vow/src/when.js @@ -28,8 +28,14 @@ export const makeWhen = ( let result = await specimenP; let payload = getVowPayload(result); let priorRetryValue; + const seenPayloads = new WeakSet(); while (payload) { - result = await basicE(payload.vowV0) + const { vowV0 } = payload; + if (seenPayloads.has(vowV0)) { + throw Error('Vow resolution cycle detected'); + } + seenPayloads.add(vowV0); + result = await basicE(vowV0) .shorten() .then( res => { diff --git a/packages/vow/test/watch.test.js b/packages/vow/test/watch.test.js index 9d9239733ba..851b43ae9f4 100644 --- a/packages/vow/test/watch.test.js +++ b/packages/vow/test/watch.test.js @@ -171,6 +171,59 @@ test('watcher args arity - shim', async t => { } }); +test('vow self resolution', async t => { + const zone = makeHeapZone(); + const { watch, when, makeVowKit } = prepareVowTools(zone); + + // A direct self vow resolution + const { vow: vow1, resolver: resolver1 } = makeVowKit(); + resolver1.resolve(vow1); + + // A self vow resolution through promise + const { vow: vow2, resolver: resolver2 } = makeVowKit(); + const vow2P = Promise.resolve(vow2); + resolver2.resolve(vow2P); + + // A 2 vow loop + const { vow: vow3, resolver: resolver3 } = makeVowKit(); + const { vow: vow4, resolver: resolver4 } = makeVowKit(); + resolver3.resolve(vow4); + resolver4.resolve(vow3); + + // A head vow pointing to a 2 vow loop (a lasso?) + const { vow: vow5, resolver: resolver5 } = makeVowKit(); + resolver5.resolve(vow4); + + const turnTimeout = async n => { + if (n > 0) { + return Promise.resolve(n - 1).then(turnTimeout); + } + + return 'timeout'; + }; + + /** + * @param {number} n + * @param {Promise} promise + */ + const raceTurnTimeout = async (n, promise) => + Promise.race([promise, turnTimeout(n)]); + + const expectedError = { + message: 'Vow resolution cycle detected', + }; + + await t.throwsAsync(raceTurnTimeout(20, when(vow1)), expectedError); + await t.throwsAsync(raceTurnTimeout(20, when(vow2)), expectedError); + await t.throwsAsync(raceTurnTimeout(20, when(vow3)), expectedError); + await t.throwsAsync(raceTurnTimeout(20, when(vow5)), expectedError); + + await t.throwsAsync(raceTurnTimeout(20, when(watch(vow1))), expectedError); + await t.throwsAsync(raceTurnTimeout(20, when(watch(vow2))), expectedError); + await t.throwsAsync(raceTurnTimeout(20, when(watch(vow3))), expectedError); + await t.throwsAsync(raceTurnTimeout(20, when(watch(vow5))), expectedError); +}); + test('disconnection of non-vow informs watcher', async t => { const zone = makeHeapZone(); const { watch, when } = prepareVowTools(zone, {