timefence 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.cjs +127 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tung Tran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# timefence
|
|
2
|
+
|
|
3
|
+
> Put a **time limit** on any promise — with real **`AbortSignal` cancellation** and a composable **deadline** signal. **Zero dependencies**.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/trananhtung/timefence/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/timefence)
|
|
7
|
+
[](https://bundlephobia.com/package/timefence)
|
|
8
|
+
[](https://www.npmjs.com/package/timefence)
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
|
|
11
|
+
`Promise.race([op, timer])` "times out" — but the slow operation keeps running in
|
|
12
|
+
the background, holding a socket open and burning quota. `timefence` gives the
|
|
13
|
+
operation an `AbortSignal` that fires on timeout, so the underlying `fetch` (or any
|
|
14
|
+
cooperative work) is actually **cancelled**, not just ignored.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { withTimeout } from "timefence";
|
|
18
|
+
|
|
19
|
+
// On timeout, the fetch is aborted — not left dangling.
|
|
20
|
+
const res = await withTimeout((signal) => fetch(url, { signal }), 5000);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Why timefence?
|
|
24
|
+
|
|
25
|
+
- **Real cancellation.** The function form receives an `AbortSignal` that fires on
|
|
26
|
+
timeout *or* when your own `signal` aborts — wire it to `fetch`/streams.
|
|
27
|
+
- **Fallback or throw.** Reject with a typed `TimeoutError`, or resolve with a
|
|
28
|
+
`fallback` value when you'd rather degrade than fail.
|
|
29
|
+
- **Composable deadlines.** `deadline(ms, signal?)` returns an `AbortSignal` that
|
|
30
|
+
trips after `ms` (or earlier if your signal aborts) — like `AbortSignal.timeout`
|
|
31
|
+
but composable, with an `unref`'d timer so it won't keep Node alive.
|
|
32
|
+
- **Zero dependencies**, ESM + CJS + types.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install timefence
|
|
38
|
+
# or: pnpm add timefence / yarn add timefence / bun add timefence
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### `withTimeout(input, ms, options?) → Promise<T>`
|
|
44
|
+
|
|
45
|
+
`input` is a promise, or `(signal) => Promise` (preferred — enables cancellation).
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
await withTimeout(slowPromise, 1000); // throws TimeoutError
|
|
49
|
+
await withTimeout((s) => fetch(url, { signal: s }), 1000); // aborts the fetch
|
|
50
|
+
await withTimeout(loadFresh(), 800, { fallback: () => cached });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Option | Type | Default | Description |
|
|
54
|
+
| ---------- | ----------------------- | ------- | ---------------------------------------------------- |
|
|
55
|
+
| `message` | `string` | — | Message for the thrown `TimeoutError`. |
|
|
56
|
+
| `signal` | `AbortSignal` | — | Cancel early from outside (rejects with its reason). |
|
|
57
|
+
| `fallback` | `() => T \| Promise<T>` | — | Resolve with this on timeout instead of throwing. |
|
|
58
|
+
|
|
59
|
+
`ms = Infinity` disables the timeout while still honoring `signal`.
|
|
60
|
+
|
|
61
|
+
### `deadline(ms, signal?) → AbortSignal`
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const signal = deadline(10_000, userSignal);
|
|
65
|
+
await fetch(url, { signal }); // aborts after 10s, or when userSignal does
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `TimeoutError` / `isTimeoutError(err)`
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
try {
|
|
72
|
+
await withTimeout(op, 1000);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (isTimeoutError(err)) retryOrDegrade();
|
|
75
|
+
else throw err;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Pairs well with
|
|
80
|
+
|
|
81
|
+
- [`retryfn`](https://www.npmjs.com/package/retryfn) — retry an operation that timed out.
|
|
82
|
+
- [`runpool`](https://www.npmjs.com/package/runpool) / [`ratebucket`](https://www.npmjs.com/package/ratebucket) — bound concurrency and rate; `timefence` bounds *time*.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
[MIT](./LICENSE) © Tung Tran
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
TimeoutError: () => TimeoutError,
|
|
24
|
+
deadline: () => deadline,
|
|
25
|
+
isTimeoutError: () => isTimeoutError,
|
|
26
|
+
withTimeout: () => withTimeout
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(src_exports);
|
|
29
|
+
|
|
30
|
+
// src/timeout.ts
|
|
31
|
+
var TimeoutError = class extends Error {
|
|
32
|
+
constructor(message = "Operation timed out") {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "TimeoutError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function isTimeoutError(err) {
|
|
38
|
+
return err instanceof Error && err.name === "TimeoutError";
|
|
39
|
+
}
|
|
40
|
+
function abortError(signal) {
|
|
41
|
+
return signal?.reason ?? new DOMException("This operation was aborted", "AbortError");
|
|
42
|
+
}
|
|
43
|
+
function withTimeout(input, ms, options = {}) {
|
|
44
|
+
const { signal: external, message, fallback } = options;
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
let timer;
|
|
48
|
+
let settled = false;
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
51
|
+
external?.removeEventListener("abort", onExternalAbort);
|
|
52
|
+
};
|
|
53
|
+
const settle = (action) => {
|
|
54
|
+
if (settled) return;
|
|
55
|
+
settled = true;
|
|
56
|
+
cleanup();
|
|
57
|
+
action();
|
|
58
|
+
};
|
|
59
|
+
function onExternalAbort() {
|
|
60
|
+
controller.abort(external?.reason);
|
|
61
|
+
settle(() => reject(abortError(external)));
|
|
62
|
+
}
|
|
63
|
+
if (external) {
|
|
64
|
+
if (external.aborted) {
|
|
65
|
+
controller.abort(external.reason);
|
|
66
|
+
return settle(() => reject(abortError(external)));
|
|
67
|
+
}
|
|
68
|
+
external.addEventListener("abort", onExternalAbort, { once: true });
|
|
69
|
+
}
|
|
70
|
+
const onTimeout = () => {
|
|
71
|
+
const err = new TimeoutError(message ?? `Operation timed out after ${ms} ms`);
|
|
72
|
+
controller.abort(err);
|
|
73
|
+
if (fallback) {
|
|
74
|
+
Promise.resolve().then(fallback).then(
|
|
75
|
+
(value) => settle(() => resolve(value)),
|
|
76
|
+
(e) => settle(() => reject(e))
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
settle(() => reject(err));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (Number.isFinite(ms) && ms >= 0) {
|
|
83
|
+
timer = setTimeout(onTimeout, ms);
|
|
84
|
+
}
|
|
85
|
+
let source;
|
|
86
|
+
if (typeof input === "function") {
|
|
87
|
+
try {
|
|
88
|
+
source = Promise.resolve(input(controller.signal));
|
|
89
|
+
} catch (err) {
|
|
90
|
+
source = Promise.reject(err);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
source = Promise.resolve(input);
|
|
94
|
+
}
|
|
95
|
+
source.then(
|
|
96
|
+
(value) => settle(() => resolve(value)),
|
|
97
|
+
(err) => settle(() => reject(err))
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function deadline(ms, signal) {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
if (signal) {
|
|
104
|
+
if (signal.aborted) {
|
|
105
|
+
controller.abort(signal.reason);
|
|
106
|
+
return controller.signal;
|
|
107
|
+
}
|
|
108
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
109
|
+
}
|
|
110
|
+
if (Number.isFinite(ms) && ms >= 0) {
|
|
111
|
+
const timer = setTimeout(
|
|
112
|
+
() => controller.abort(new TimeoutError(`Deadline of ${ms} ms exceeded`)),
|
|
113
|
+
ms
|
|
114
|
+
);
|
|
115
|
+
timer.unref?.();
|
|
116
|
+
controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
|
|
117
|
+
}
|
|
118
|
+
return controller.signal;
|
|
119
|
+
}
|
|
120
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
121
|
+
0 && (module.exports = {
|
|
122
|
+
TimeoutError,
|
|
123
|
+
deadline,
|
|
124
|
+
isTimeoutError,
|
|
125
|
+
withTimeout
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/timeout.ts"],"sourcesContent":["/**\n * timefence — put a time limit on any promise, with real AbortSignal cancellation\n * and a composable deadline signal. Zero dependencies.\n *\n * @packageDocumentation\n */\n\nexport {\n withTimeout,\n deadline,\n TimeoutError,\n isTimeoutError,\n type WithTimeoutOptions,\n} from \"./timeout.js\";\n","/** Error thrown when an operation exceeds its time budget. */\nexport class TimeoutError extends Error {\n constructor(message = \"Operation timed out\") {\n super(message);\n this.name = \"TimeoutError\";\n }\n}\n\n/** `true` if `err` is a {@link TimeoutError}. */\nexport function isTimeoutError(err: unknown): err is TimeoutError {\n return err instanceof Error && err.name === \"TimeoutError\";\n}\n\nfunction abortError(signal?: AbortSignal): unknown {\n return signal?.reason ?? new DOMException(\"This operation was aborted\", \"AbortError\");\n}\n\n/** Options for {@link withTimeout}. */\nexport interface WithTimeoutOptions<T> {\n /** Message for the thrown {@link TimeoutError}. */\n message?: string;\n /** External signal that cancels the operation early. */\n signal?: AbortSignal;\n /** If provided, resolve with this instead of rejecting when the time runs out. */\n fallback?: () => T | Promise<T>;\n}\n\n/**\n * Run an operation with a time limit.\n *\n * `input` may be a promise, or a function receiving an `AbortSignal` that fires\n * when the deadline (or an external `signal`) is reached — so cooperative work\n * (`fetch`, etc.) is actually cancelled, not just abandoned.\n *\n * @example\n * ```ts\n * // Function form: the fetch is aborted on timeout.\n * const res = await withTimeout((signal) => fetch(url, { signal }), 5000);\n *\n * // Fallback instead of throwing:\n * const data = await withTimeout(loadFresh(), 800, { fallback: () => cached });\n * ```\n *\n * @param input - A promise, or `(signal) => Promise`.\n * @param ms - Timeout in ms. `Infinity` disables the timeout (abort still works).\n * @throws {TimeoutError} on timeout (unless `fallback` is given); rejects if the\n * external `signal` aborts.\n */\nexport function withTimeout<T>(\n input: Promise<T> | ((signal: AbortSignal) => Promise<T> | T),\n ms: number,\n options: WithTimeoutOptions<T> = {},\n): Promise<T> {\n const { signal: external, message, fallback } = options;\n\n return new Promise<T>((resolve, reject) => {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let settled = false;\n\n const cleanup = () => {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", onExternalAbort);\n };\n const settle = (action: () => void): void => {\n if (settled) return;\n settled = true;\n cleanup();\n action();\n };\n\n function onExternalAbort(): void {\n controller.abort(external?.reason);\n settle(() => reject(abortError(external)));\n }\n\n if (external) {\n if (external.aborted) {\n controller.abort(external.reason);\n return settle(() => reject(abortError(external)));\n }\n external.addEventListener(\"abort\", onExternalAbort, { once: true });\n }\n\n const onTimeout = (): void => {\n const err = new TimeoutError(message ?? `Operation timed out after ${ms} ms`);\n controller.abort(err);\n if (fallback) {\n Promise.resolve()\n .then(fallback)\n .then(\n (value) => settle(() => resolve(value)),\n (e) => settle(() => reject(e)),\n );\n } else {\n settle(() => reject(err));\n }\n };\n\n if (Number.isFinite(ms) && ms >= 0) {\n timer = setTimeout(onTimeout, ms);\n }\n\n let source: Promise<T>;\n if (typeof input === \"function\") {\n try {\n source = Promise.resolve((input as (s: AbortSignal) => Promise<T> | T)(controller.signal));\n } catch (err) {\n source = Promise.reject(err);\n }\n } else {\n source = Promise.resolve(input);\n }\n\n source.then(\n (value) => settle(() => resolve(value)),\n (err) => settle(() => reject(err)),\n );\n });\n}\n\n/**\n * Create an `AbortSignal` that aborts after `ms` (with a {@link TimeoutError}\n * reason), optionally composed with an external `signal` that aborts it earlier.\n *\n * Like `AbortSignal.timeout`, but composable and with a typed reason. The\n * internal timer is `unref`'d so it never keeps a Node process alive.\n *\n * @example\n * ```ts\n * const res = await fetch(url, { signal: deadline(10_000, userSignal) });\n * ```\n */\nexport function deadline(ms: number, signal?: AbortSignal): AbortSignal {\n const controller = new AbortController();\n\n if (signal) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n return controller.signal;\n }\n signal.addEventListener(\"abort\", () => controller.abort(signal.reason), { once: true });\n }\n\n if (Number.isFinite(ms) && ms >= 0) {\n const timer = setTimeout(\n () => controller.abort(new TimeoutError(`Deadline of ${ms} ms exceeded`)),\n ms,\n );\n (timer as { unref?: () => void }).unref?.();\n controller.signal.addEventListener(\"abort\", () => clearTimeout(timer), { once: true });\n }\n\n return controller.signal;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Error thrown when an operation exceeds its time budget. */
|
|
2
|
+
declare class TimeoutError extends Error {
|
|
3
|
+
constructor(message?: string);
|
|
4
|
+
}
|
|
5
|
+
/** `true` if `err` is a {@link TimeoutError}. */
|
|
6
|
+
declare function isTimeoutError(err: unknown): err is TimeoutError;
|
|
7
|
+
/** Options for {@link withTimeout}. */
|
|
8
|
+
interface WithTimeoutOptions<T> {
|
|
9
|
+
/** Message for the thrown {@link TimeoutError}. */
|
|
10
|
+
message?: string;
|
|
11
|
+
/** External signal that cancels the operation early. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/** If provided, resolve with this instead of rejecting when the time runs out. */
|
|
14
|
+
fallback?: () => T | Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Run an operation with a time limit.
|
|
18
|
+
*
|
|
19
|
+
* `input` may be a promise, or a function receiving an `AbortSignal` that fires
|
|
20
|
+
* when the deadline (or an external `signal`) is reached — so cooperative work
|
|
21
|
+
* (`fetch`, etc.) is actually cancelled, not just abandoned.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Function form: the fetch is aborted on timeout.
|
|
26
|
+
* const res = await withTimeout((signal) => fetch(url, { signal }), 5000);
|
|
27
|
+
*
|
|
28
|
+
* // Fallback instead of throwing:
|
|
29
|
+
* const data = await withTimeout(loadFresh(), 800, { fallback: () => cached });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @param input - A promise, or `(signal) => Promise`.
|
|
33
|
+
* @param ms - Timeout in ms. `Infinity` disables the timeout (abort still works).
|
|
34
|
+
* @throws {TimeoutError} on timeout (unless `fallback` is given); rejects if the
|
|
35
|
+
* external `signal` aborts.
|
|
36
|
+
*/
|
|
37
|
+
declare function withTimeout<T>(input: Promise<T> | ((signal: AbortSignal) => Promise<T> | T), ms: number, options?: WithTimeoutOptions<T>): Promise<T>;
|
|
38
|
+
/**
|
|
39
|
+
* Create an `AbortSignal` that aborts after `ms` (with a {@link TimeoutError}
|
|
40
|
+
* reason), optionally composed with an external `signal` that aborts it earlier.
|
|
41
|
+
*
|
|
42
|
+
* Like `AbortSignal.timeout`, but composable and with a typed reason. The
|
|
43
|
+
* internal timer is `unref`'d so it never keeps a Node process alive.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const res = await fetch(url, { signal: deadline(10_000, userSignal) });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare function deadline(ms: number, signal?: AbortSignal): AbortSignal;
|
|
51
|
+
|
|
52
|
+
export { TimeoutError, type WithTimeoutOptions, deadline, isTimeoutError, withTimeout };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Error thrown when an operation exceeds its time budget. */
|
|
2
|
+
declare class TimeoutError extends Error {
|
|
3
|
+
constructor(message?: string);
|
|
4
|
+
}
|
|
5
|
+
/** `true` if `err` is a {@link TimeoutError}. */
|
|
6
|
+
declare function isTimeoutError(err: unknown): err is TimeoutError;
|
|
7
|
+
/** Options for {@link withTimeout}. */
|
|
8
|
+
interface WithTimeoutOptions<T> {
|
|
9
|
+
/** Message for the thrown {@link TimeoutError}. */
|
|
10
|
+
message?: string;
|
|
11
|
+
/** External signal that cancels the operation early. */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/** If provided, resolve with this instead of rejecting when the time runs out. */
|
|
14
|
+
fallback?: () => T | Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Run an operation with a time limit.
|
|
18
|
+
*
|
|
19
|
+
* `input` may be a promise, or a function receiving an `AbortSignal` that fires
|
|
20
|
+
* when the deadline (or an external `signal`) is reached — so cooperative work
|
|
21
|
+
* (`fetch`, etc.) is actually cancelled, not just abandoned.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Function form: the fetch is aborted on timeout.
|
|
26
|
+
* const res = await withTimeout((signal) => fetch(url, { signal }), 5000);
|
|
27
|
+
*
|
|
28
|
+
* // Fallback instead of throwing:
|
|
29
|
+
* const data = await withTimeout(loadFresh(), 800, { fallback: () => cached });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @param input - A promise, or `(signal) => Promise`.
|
|
33
|
+
* @param ms - Timeout in ms. `Infinity` disables the timeout (abort still works).
|
|
34
|
+
* @throws {TimeoutError} on timeout (unless `fallback` is given); rejects if the
|
|
35
|
+
* external `signal` aborts.
|
|
36
|
+
*/
|
|
37
|
+
declare function withTimeout<T>(input: Promise<T> | ((signal: AbortSignal) => Promise<T> | T), ms: number, options?: WithTimeoutOptions<T>): Promise<T>;
|
|
38
|
+
/**
|
|
39
|
+
* Create an `AbortSignal` that aborts after `ms` (with a {@link TimeoutError}
|
|
40
|
+
* reason), optionally composed with an external `signal` that aborts it earlier.
|
|
41
|
+
*
|
|
42
|
+
* Like `AbortSignal.timeout`, but composable and with a typed reason. The
|
|
43
|
+
* internal timer is `unref`'d so it never keeps a Node process alive.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const res = await fetch(url, { signal: deadline(10_000, userSignal) });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare function deadline(ms: number, signal?: AbortSignal): AbortSignal;
|
|
51
|
+
|
|
52
|
+
export { TimeoutError, type WithTimeoutOptions, deadline, isTimeoutError, withTimeout };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/timeout.ts
|
|
2
|
+
var TimeoutError = class extends Error {
|
|
3
|
+
constructor(message = "Operation timed out") {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "TimeoutError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
function isTimeoutError(err) {
|
|
9
|
+
return err instanceof Error && err.name === "TimeoutError";
|
|
10
|
+
}
|
|
11
|
+
function abortError(signal) {
|
|
12
|
+
return signal?.reason ?? new DOMException("This operation was aborted", "AbortError");
|
|
13
|
+
}
|
|
14
|
+
function withTimeout(input, ms, options = {}) {
|
|
15
|
+
const { signal: external, message, fallback } = options;
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
let timer;
|
|
19
|
+
let settled = false;
|
|
20
|
+
const cleanup = () => {
|
|
21
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
22
|
+
external?.removeEventListener("abort", onExternalAbort);
|
|
23
|
+
};
|
|
24
|
+
const settle = (action) => {
|
|
25
|
+
if (settled) return;
|
|
26
|
+
settled = true;
|
|
27
|
+
cleanup();
|
|
28
|
+
action();
|
|
29
|
+
};
|
|
30
|
+
function onExternalAbort() {
|
|
31
|
+
controller.abort(external?.reason);
|
|
32
|
+
settle(() => reject(abortError(external)));
|
|
33
|
+
}
|
|
34
|
+
if (external) {
|
|
35
|
+
if (external.aborted) {
|
|
36
|
+
controller.abort(external.reason);
|
|
37
|
+
return settle(() => reject(abortError(external)));
|
|
38
|
+
}
|
|
39
|
+
external.addEventListener("abort", onExternalAbort, { once: true });
|
|
40
|
+
}
|
|
41
|
+
const onTimeout = () => {
|
|
42
|
+
const err = new TimeoutError(message ?? `Operation timed out after ${ms} ms`);
|
|
43
|
+
controller.abort(err);
|
|
44
|
+
if (fallback) {
|
|
45
|
+
Promise.resolve().then(fallback).then(
|
|
46
|
+
(value) => settle(() => resolve(value)),
|
|
47
|
+
(e) => settle(() => reject(e))
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
settle(() => reject(err));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
if (Number.isFinite(ms) && ms >= 0) {
|
|
54
|
+
timer = setTimeout(onTimeout, ms);
|
|
55
|
+
}
|
|
56
|
+
let source;
|
|
57
|
+
if (typeof input === "function") {
|
|
58
|
+
try {
|
|
59
|
+
source = Promise.resolve(input(controller.signal));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
source = Promise.reject(err);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
source = Promise.resolve(input);
|
|
65
|
+
}
|
|
66
|
+
source.then(
|
|
67
|
+
(value) => settle(() => resolve(value)),
|
|
68
|
+
(err) => settle(() => reject(err))
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function deadline(ms, signal) {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
if (signal) {
|
|
75
|
+
if (signal.aborted) {
|
|
76
|
+
controller.abort(signal.reason);
|
|
77
|
+
return controller.signal;
|
|
78
|
+
}
|
|
79
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
80
|
+
}
|
|
81
|
+
if (Number.isFinite(ms) && ms >= 0) {
|
|
82
|
+
const timer = setTimeout(
|
|
83
|
+
() => controller.abort(new TimeoutError(`Deadline of ${ms} ms exceeded`)),
|
|
84
|
+
ms
|
|
85
|
+
);
|
|
86
|
+
timer.unref?.();
|
|
87
|
+
controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
|
|
88
|
+
}
|
|
89
|
+
return controller.signal;
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
TimeoutError,
|
|
93
|
+
deadline,
|
|
94
|
+
isTimeoutError,
|
|
95
|
+
withTimeout
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/timeout.ts"],"sourcesContent":["/** Error thrown when an operation exceeds its time budget. */\nexport class TimeoutError extends Error {\n constructor(message = \"Operation timed out\") {\n super(message);\n this.name = \"TimeoutError\";\n }\n}\n\n/** `true` if `err` is a {@link TimeoutError}. */\nexport function isTimeoutError(err: unknown): err is TimeoutError {\n return err instanceof Error && err.name === \"TimeoutError\";\n}\n\nfunction abortError(signal?: AbortSignal): unknown {\n return signal?.reason ?? new DOMException(\"This operation was aborted\", \"AbortError\");\n}\n\n/** Options for {@link withTimeout}. */\nexport interface WithTimeoutOptions<T> {\n /** Message for the thrown {@link TimeoutError}. */\n message?: string;\n /** External signal that cancels the operation early. */\n signal?: AbortSignal;\n /** If provided, resolve with this instead of rejecting when the time runs out. */\n fallback?: () => T | Promise<T>;\n}\n\n/**\n * Run an operation with a time limit.\n *\n * `input` may be a promise, or a function receiving an `AbortSignal` that fires\n * when the deadline (or an external `signal`) is reached — so cooperative work\n * (`fetch`, etc.) is actually cancelled, not just abandoned.\n *\n * @example\n * ```ts\n * // Function form: the fetch is aborted on timeout.\n * const res = await withTimeout((signal) => fetch(url, { signal }), 5000);\n *\n * // Fallback instead of throwing:\n * const data = await withTimeout(loadFresh(), 800, { fallback: () => cached });\n * ```\n *\n * @param input - A promise, or `(signal) => Promise`.\n * @param ms - Timeout in ms. `Infinity` disables the timeout (abort still works).\n * @throws {TimeoutError} on timeout (unless `fallback` is given); rejects if the\n * external `signal` aborts.\n */\nexport function withTimeout<T>(\n input: Promise<T> | ((signal: AbortSignal) => Promise<T> | T),\n ms: number,\n options: WithTimeoutOptions<T> = {},\n): Promise<T> {\n const { signal: external, message, fallback } = options;\n\n return new Promise<T>((resolve, reject) => {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let settled = false;\n\n const cleanup = () => {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", onExternalAbort);\n };\n const settle = (action: () => void): void => {\n if (settled) return;\n settled = true;\n cleanup();\n action();\n };\n\n function onExternalAbort(): void {\n controller.abort(external?.reason);\n settle(() => reject(abortError(external)));\n }\n\n if (external) {\n if (external.aborted) {\n controller.abort(external.reason);\n return settle(() => reject(abortError(external)));\n }\n external.addEventListener(\"abort\", onExternalAbort, { once: true });\n }\n\n const onTimeout = (): void => {\n const err = new TimeoutError(message ?? `Operation timed out after ${ms} ms`);\n controller.abort(err);\n if (fallback) {\n Promise.resolve()\n .then(fallback)\n .then(\n (value) => settle(() => resolve(value)),\n (e) => settle(() => reject(e)),\n );\n } else {\n settle(() => reject(err));\n }\n };\n\n if (Number.isFinite(ms) && ms >= 0) {\n timer = setTimeout(onTimeout, ms);\n }\n\n let source: Promise<T>;\n if (typeof input === \"function\") {\n try {\n source = Promise.resolve((input as (s: AbortSignal) => Promise<T> | T)(controller.signal));\n } catch (err) {\n source = Promise.reject(err);\n }\n } else {\n source = Promise.resolve(input);\n }\n\n source.then(\n (value) => settle(() => resolve(value)),\n (err) => settle(() => reject(err)),\n );\n });\n}\n\n/**\n * Create an `AbortSignal` that aborts after `ms` (with a {@link TimeoutError}\n * reason), optionally composed with an external `signal` that aborts it earlier.\n *\n * Like `AbortSignal.timeout`, but composable and with a typed reason. The\n * internal timer is `unref`'d so it never keeps a Node process alive.\n *\n * @example\n * ```ts\n * const res = await fetch(url, { signal: deadline(10_000, userSignal) });\n * ```\n */\nexport function deadline(ms: number, signal?: AbortSignal): AbortSignal {\n const controller = new AbortController();\n\n if (signal) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n return controller.signal;\n }\n signal.addEventListener(\"abort\", () => controller.abort(signal.reason), { once: true });\n }\n\n if (Number.isFinite(ms) && ms >= 0) {\n const timer = setTimeout(\n () => controller.abort(new TimeoutError(`Deadline of ${ms} ms exceeded`)),\n ms,\n );\n (timer as { unref?: () => void }).unref?.();\n controller.signal.addEventListener(\"abort\", () => clearTimeout(timer), { once: true });\n }\n\n return controller.signal;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "timefence",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Put a time limit on any promise, with real AbortSignal cancellation and a composable deadline signal. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"timeout",
|
|
7
|
+
"deadline",
|
|
8
|
+
"p-timeout",
|
|
9
|
+
"abortsignal",
|
|
10
|
+
"abort",
|
|
11
|
+
"promise",
|
|
12
|
+
"cancel",
|
|
13
|
+
"async",
|
|
14
|
+
"race",
|
|
15
|
+
"resilience",
|
|
16
|
+
"zero-dependency"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Tung Tran (https://github.com/trananhtung)",
|
|
20
|
+
"homepage": "https://github.com/trananhtung/timefence#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/trananhtung/timefence.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/trananhtung/timefence/issues"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"dev": "tsup --watch",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"prepublishOnly": "npm run build"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20.17.10",
|
|
57
|
+
"tsup": "^8.3.5",
|
|
58
|
+
"typescript": "^5.7.2",
|
|
59
|
+
"vitest": "^2.1.8"
|
|
60
|
+
},
|
|
61
|
+
"sideEffects": false,
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
}
|
|
65
|
+
}
|