Skip to content

Commit

Permalink
fix(vow): handle resolution loops in vows
Browse files Browse the repository at this point in the history
  • Loading branch information
mhofman committed Jun 22, 2024
1 parent 0af876f commit 5727679
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 3 deletions.
10 changes: 8 additions & 2 deletions packages/vow/src/watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -90,8 +91,13 @@ const preparePromiseWatcher = (zone, isRetryableReason, watchNextStep) =>
{
/** @type {Required<PromiseWatcher>['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);
Expand Down
8 changes: 7 additions & 1 deletion packages/vow/src/when.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
53 changes: 53 additions & 0 deletions packages/vow/test/watch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>} 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, {
Expand Down

0 comments on commit 5727679

Please sign in to comment.