Problem
In InMemoryOrchestrationBackend.waitForState() (in-memory-backend.ts), the timeout handler attempts to clean up the stale waiter by comparing w.resolve === resolve, but this comparison always fails because w.resolve is a wrapper function — not the original Promise resolve.
Code (before fix):
const timer = setTimeout(() => {
const waiters = this.stateWaiters.get(instanceId);
if (waiters) {
const index = waiters.findIndex((w) => w.resolve === resolve);
// ^^^^^^^^^^^^^^^^^^^^^^^^
// BUG: w.resolve is a wrapper (result) => { clearTimeout(timer); resolve(result); }
// so it will never === resolve. findIndex always returns -1.
if (index >= 0) {
waiters.splice(index, 1); // Never executed
}
}
reject(new Error(...));
}, timeoutMs);
const waiter: StateWaiter = {
resolve: (result) => {
clearTimeout(timer);
resolve(result); // ← wraps resolve, so waiter.resolve !== resolve
},
...
};
Root Cause
The StateWaiter.resolve property is set to a wrapper function that calls clearTimeout(timer) before calling the original Promise resolve. The timeout handler then tries to find the waiter by comparing w.resolve === resolve — but w.resolve is the wrapper, not the original resolve, so the identity check always fails.
Impact
- Memory leak: Timed-out waiters permanently accumulate in the
stateWaiters map.
- Wasted computation:
notifyWaiters() continues evaluating predicates on stale waiters.
- Timer leak: The
waitForState timeout timer is not tracked in pendingTimers, so it is not cleaned up by reset().
- Affects any test using
waitForState (indirectly via TestOrchestrationClient.waitForOrchestrationCompletion, waitForOrchestrationStart, etc.) where the wait times out.
- No data corruption since calling
resolve() on a settled Promise is a no-op, but the stale waiter and its closure remain in memory.
Proposed Fix
- Move the
waiter declaration before the timer so the timeout callback can use waiters.indexOf(waiter) (object identity) instead of findIndex with a broken reference comparison.
- Track the
waitForState timeout timer in pendingTimers so reset() cleans it up.
- Remove the timer from
pendingTimers when the waiter resolves, rejects, or times out.
- Clean up the
stateWaiters map entry when the last waiter for an instance is removed.
Problem
In
InMemoryOrchestrationBackend.waitForState()(in-memory-backend.ts), the timeout handler attempts to clean up the stale waiter by comparingw.resolve === resolve, but this comparison always fails becausew.resolveis a wrapper function — not the original Promiseresolve.Code (before fix):
Root Cause
The
StateWaiter.resolveproperty is set to a wrapper function that callsclearTimeout(timer)before calling the original Promiseresolve. The timeout handler then tries to find the waiter by comparingw.resolve === resolve— butw.resolveis the wrapper, not the originalresolve, so the identity check always fails.Impact
stateWaitersmap.notifyWaiters()continues evaluating predicates on stale waiters.waitForStatetimeout timer is not tracked inpendingTimers, so it is not cleaned up byreset().waitForState(indirectly viaTestOrchestrationClient.waitForOrchestrationCompletion,waitForOrchestrationStart, etc.) where the wait times out.resolve()on a settled Promise is a no-op, but the stale waiter and its closure remain in memory.Proposed Fix
waiterdeclaration before thetimerso the timeout callback can usewaiters.indexOf(waiter)(object identity) instead offindIndexwith a broken reference comparison.waitForStatetimeout timer inpendingTimerssoreset()cleans it up.pendingTimerswhen the waiter resolves, rejects, or times out.stateWaitersmap entry when the last waiter for an instance is removed.