Skip to content

Commit 24ecf1d

Browse files
feat: Add support for a cancel-able timeout. (#476)
The current timedPromise doesn't provide access to the handle, so it cannot cancel the timer. The timer itself will not have a harmful result, but it is an open handle. Open handles often prevent runtimes from exiting. For instance initializing something like the node server with a really long timeout may keep the process open for that entire timeout even if the SDK initialized. I am adding this as a new function, which node server will then migrate to. RN can of course migrate as well, but this change will be independent. I noticed this unit testing the openfeature-node-provider with a timeout and each test had a remaining open handle. --------- Co-authored-by: Matthew M. Keeler <[email protected]>
1 parent ed34f61 commit 24ecf1d

File tree

3 files changed

+95
-0
lines changed

3 files changed

+95
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { LDTimeoutError } from '../src/errors';
2+
import { cancelableTimedPromise } from '../src/utils/cancelableTimedPromise';
3+
4+
it('throws when it times out', async () => {
5+
try {
6+
await cancelableTimedPromise(0.1, 'test-task').promise;
7+
fail('timeout did not fire');
8+
} catch (err) {
9+
expect(err).toBeInstanceOf(LDTimeoutError);
10+
}
11+
});
12+
13+
it('promise resolves when cancelled', async () => {
14+
// This test would take many minutes if the cancellation didn't work.
15+
const cancelableTimeout = cancelableTimedPromise(300, 'test-task');
16+
cancelableTimeout.cancel();
17+
await cancelableTimeout.promise;
18+
});
19+
20+
it('can be used for timing out a task', async () => {
21+
const cancelableTimeout = cancelableTimedPromise(0.1, 'exampleTimeout');
22+
let timeout: ReturnType<typeof setTimeout>;
23+
const sampleTask = new Promise<void>((resolve, _reject) => {
24+
timeout = setTimeout(() => resolve, 1000);
25+
});
26+
27+
try {
28+
await Promise.race([sampleTask, cancelableTimeout.promise]);
29+
fail('timeout did not fire');
30+
} catch (err) {
31+
expect(err).toBeInstanceOf(LDTimeoutError);
32+
}
33+
// @ts-ignore
34+
clearTimeout(timeout);
35+
});
36+
37+
it('a raced promise can cancel the task', async () => {
38+
const cancelableTimeout = cancelableTimedPromise(1000, 'exampleTimeout');
39+
const sampleTask = new Promise<void>((resolve, _reject) => {
40+
setTimeout(resolve, 100);
41+
});
42+
43+
try {
44+
await Promise.race([
45+
sampleTask.then(() => {
46+
cancelableTimeout.cancel();
47+
}),
48+
cancelableTimeout.promise,
49+
]);
50+
} catch (err) {
51+
fail('should not have timed out');
52+
}
53+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { LDTimeoutError } from '../errors';
2+
import { VoidFunction } from './VoidFunction';
3+
4+
/**
5+
* Represents a timeout that can be cancelled.
6+
*
7+
* When racing a timeout, and another task completes before the timeout,
8+
* then the timeout should be cancelled. This prevents leaving open handles
9+
* which can stop the runtime from exiting.
10+
*/
11+
export interface CancelableTimeout {
12+
promise: Promise<void>;
13+
cancel: VoidFunction;
14+
}
15+
16+
/**
17+
* Returns a promise which errors after t seconds.
18+
*
19+
* @param t Timeout in seconds.
20+
* @param taskName Name of task being timed for logging and error reporting.
21+
*/
22+
export function cancelableTimedPromise(t: number, taskName: string): CancelableTimeout {
23+
let timeout: ReturnType<typeof setTimeout>;
24+
let resolve: VoidFunction;
25+
const promise = new Promise<void>((_res, reject) => {
26+
resolve = _res;
27+
timeout = setTimeout(() => {
28+
const e = `${taskName} timed out after ${t} seconds.`;
29+
reject(new LDTimeoutError(e));
30+
}, t * 1000);
31+
});
32+
return {
33+
promise,
34+
cancel: () => {
35+
resolve();
36+
clearTimeout(timeout);
37+
},
38+
};
39+
}

packages/shared/common/src/utils/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cancelableTimedPromise, type CancelableTimeout } from './cancelableTimedPromise';
12
import clone from './clone';
23
import { secondsToMillis } from './date';
34
import debounce from './debounce';
@@ -24,4 +25,6 @@ export {
2425
sleep,
2526
timedPromise,
2627
VoidFunction,
28+
type CancelableTimeout,
29+
cancelableTimedPromise,
2730
};

0 commit comments

Comments
 (0)