Skip to content

Commit bc3e168

Browse files
authored
feat: support promise with timeout (#64)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for Node.js version 23. - Introduced a new workflow for automating project publishing. - New `TimeoutError` class and timeout handling functions (`promiseTimeout`, `runWithTimeout`) for better asynchronous control. - Updated the README with new examples and a section on `runWithTimeout`. - **Bug Fixes** - Improved clarity and consistency in usage examples in the README. - **Documentation** - Added a badge for Node.js version in the README. - Updated import statements in usage examples for consistency. - **Chores** - Updated package version and added a new development dependency. - Modified script to include linting before publishing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 08195c0 commit bc3e168

File tree

7 files changed

+197
-36
lines changed

7 files changed

+197
-36
lines changed

.github/workflows/nodejs.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ jobs:
1212
uses: node-modules/github-actions/.github/workflows/node-test.yml@master
1313
with:
1414
os: 'ubuntu-latest'
15-
version: '16, 18, 20, 22'
15+
version: '16, 18, 20, 22, 23'
16+
secrets:
17+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/pkg.pr.new.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Publish Any Commit
2+
on: [push, pull_request]
3+
4+
jobs:
5+
build:
6+
runs-on: ubuntu-latest
7+
8+
steps:
9+
- name: Checkout code
10+
uses: actions/checkout@v4
11+
12+
- run: corepack enable
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 20
16+
17+
- name: Install dependencies
18+
run: npm install
19+
20+
- name: Build
21+
run: npm run prepublishOnly --if-present
22+
23+
- run: npx pkg-pr-new publish

README.md

+70-34
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[![CI](https://github.com/node-modules/utility/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/utility/actions/workflows/nodejs.yml)
55
[![Test coverage][codecov-image]][codecov-url]
66
[![npm download][download-image]][download-url]
7+
[![Node.js Version](https://img.shields.io/node/v/utility.svg?style=flat)](https://nodejs.org/en/download/)
78

89
[npm-image]: https://img.shields.io/npm/v/utility.svg?style=flat-square
910
[npm-url]: https://npmjs.org/package/utility
@@ -29,68 +30,97 @@ const utils = require('utility');
2930
Also you can use it within typescript, like this ↓
3031

3132
```ts
32-
import * as utility from 'utility';
33+
import * as utils from 'utility';
3334
```
3435

3536
### md5
3637

37-
```js
38-
utils.md5('苏千').should.equal('5f733c47c58a077d61257102b2d44481');
39-
utils.md5(Buffer.from('苏千')).should.equal('5f733c47c58a077d61257102b2d44481');
38+
```ts
39+
import { md5 } from 'utility';
40+
41+
md5('苏千');
42+
// '5f733c47c58a077d61257102b2d44481'
43+
44+
md5(Buffer.from('苏千'));
45+
// '5f733c47c58a077d61257102b2d44481'
46+
4047
// md5 base64 format
41-
utils.md5('苏千', 'base64'); // 'X3M8R8WKB31hJXECstREgQ=='
48+
md5('苏千', 'base64');
49+
// 'X3M8R8WKB31hJXECstREgQ=='
4250

4351
// Object md5 hash. Sorted by key, and JSON.stringify. See source code for detail
44-
utils.md5({foo: 'bar', bar: 'foo'}).should.equal(utils.md5({bar: 'foo', foo: 'bar'}));
52+
md5({foo: 'bar', bar: 'foo'}).should.equal(md5({bar: 'foo', foo: 'bar'}));
4553
```
4654

4755
### sha1
4856

49-
```js
50-
utils.sha1('苏千').should.equal('0a4aff6bab634b9c2f99b71f25e976921fcde5a5');
51-
utils.sha1(Buffer.from('苏千')).should.equal('0a4aff6bab634b9c2f99b71f25e976921fcde5a5');
57+
```ts
58+
import { sha1 } from 'utility';
59+
60+
sha1('苏千');
61+
// '0a4aff6bab634b9c2f99b71f25e976921fcde5a5'
62+
63+
sha1(Buffer.from('苏千'));
64+
// '0a4aff6bab634b9c2f99b71f25e976921fcde5a5'
65+
5266
// sha1 base64 format
53-
utils.sha1('苏千', 'base64'); // 'Ckr/a6tjS5wvmbcfJel2kh/N5aU='
67+
sha1('苏千', 'base64');
68+
// 'Ckr/a6tjS5wvmbcfJel2kh/N5aU='
5469

5570
// Object sha1 hash. Sorted by key, and JSON.stringify. See source code for detail
56-
utils.sha1({foo: 'bar', bar: 'foo'}).should.equal(utils.sha1({bar: 'foo', foo: 'bar'}));
71+
sha1({foo: 'bar', bar: 'foo'}).should.equal(sha1({bar: 'foo', foo: 'bar'}));
5772
```
5873

5974
### sha256
6075

61-
```js
62-
utils.sha256(Buffer.from('苏千')).should.equal('75dd03e3fcdbba7d5bec07900bae740cc8e361d77e7df8949de421d3df5d3635');
76+
```ts
77+
import { sha256 } from 'utility';
78+
79+
sha256(Buffer.from('苏千'));
80+
// '75dd03e3fcdbba7d5bec07900bae740cc8e361d77e7df8949de421d3df5d3635'
6381
```
6482

6583
### hmac
6684

67-
```js
85+
```ts
86+
import { hmac } from 'utility';
87+
6888
// hmac-sha1 with base64 output encoding
69-
utils.hmac('sha1', 'I am a key', 'hello world'); // 'pO6J0LKDxRRkvSECSEdxwKx84L0='
89+
hmac('sha1', 'I am a key', 'hello world');
90+
// 'pO6J0LKDxRRkvSECSEdxwKx84L0='
7091
```
7192

7293
### decode and encode
7394

74-
```js
95+
```ts
96+
import { base64encode, base64decode, escape, unescape, encodeURIComponent, decodeURIComponent } from 'utility';
97+
7598
// base64 encode
76-
utils.base64encode('你好¥'); // '5L2g5aW977+l'
77-
utils.base64decode('5L2g5aW977+l') // '你好¥'
99+
base64encode('你好¥');
100+
// '5L2g5aW977+l'
101+
base64decode('5L2g5aW977+l');
102+
// '你好¥'
78103

79104
// urlsafe base64 encode
80-
utils.base64encode('你好¥', true); // '5L2g5aW977-l'
81-
utils.base64decode('5L2g5aW977-l', true); // '你好¥'
105+
base64encode('你好¥', true);
106+
// '5L2g5aW977-l'
107+
base64decode('5L2g5aW977-l', true);
108+
// '你好¥'
82109

83110
// html escape and unescape
84-
utils.escape('<script/>"& &amp;'); // '&lt;script/&gt;&quot;&amp; &amp;amp;'
85-
utils.unescape('&lt;script/&gt;&quot;&amp; &amp;amp;'); // '<script/>"& &amp;'
111+
escape('<script/>"& &amp;');
112+
// '&lt;script/&gt;&quot;&amp; &amp;amp;'
113+
unescape('&lt;script/&gt;&quot;&amp; &amp;amp;');
114+
// '<script/>"& &amp;'
86115

87116
// Safe encodeURIComponent and decodeURIComponent
88-
utils.decodeURIComponent(utils.encodeURIComponent('你好, nodejs')).should.equal('你好, nodejs');
117+
decodeURIComponent(encodeURIComponent('你好, Node.js'));
118+
// '你好, Node.js'
89119
```
90120

91121
### others
92122

93-
___[WARNNING] getIP() remove, PLEASE use `https://github.com/node-modules/address` module instead.___
123+
___[WARNNING] `getIP()` remove, PLEASE use `https://github.com/node-modules/address` module instead.___
94124

95125
```js
96126
// get a function parameter's names
@@ -164,12 +194,18 @@ utils.random(2, 1000); // [2, 1000)
164194
utils.random(); // 0
165195
```
166196

167-
### Timers
197+
### Timeout
168198

169-
```js
170-
utils.setImmediate(function () {
171-
console.log('hi');
172-
});
199+
#### `runWithTimeout(scope, timeout)`
200+
201+
Executes a scope promise with a specified timeout duration. If the promise doesn't resolve within the timeout period, it will reject with a `TimeoutError`.
202+
203+
```ts
204+
import { runWithTimeout } from 'utility';
205+
206+
await runWithTimeout(async () => {
207+
// long run operation here
208+
}, 1000);
173209
```
174210

175211
### map
@@ -216,17 +252,17 @@ const res = utils.try(function () {
216252
```Note``` that when you use ```typescript```, you must use the following methods to call ' Try '
217253

218254
```js
219-
import * as utility from 'utility';
255+
import { UNSTABLE_METHOD } from 'utility';
220256

221-
utility.UNSTABLE_METHOD.try(...);
257+
UNSTABLE_METHOD.try(...);
222258
...
223259
```
224260

225261
### argumentsToArray
226262

227263
```js
228264
function foo() {
229-
const arr = utility.argumentsToArray(arguments);
265+
const arr = utils.argumentsToArray(arguments);
230266
console.log(arr.join(', '));
231267
}
232268
```
@@ -268,10 +304,10 @@ async () => {
268304

269305
```js
270306
// assign object
271-
utility.assign({}, { a: 1 });
307+
utils.assign({}, { a: 1 });
272308

273309
// assign multiple object
274-
utility.assign({}, [ { a: 1 }, { b: 1 } ]);
310+
utils.assign({}, [ { a: 1 }, { b: 1 } ]);
275311
```
276312

277313
## benchmark

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"pretest": "npm run lint -- --fix && npm run prepublishOnly",
88
"test": "egg-bin test",
99
"test-local": "egg-bin test",
10-
"preci": "npm run prepublishOnly",
10+
"preci": "npm run lint && npm run prepublishOnly && attw --pack",
1111
"ci": "egg-bin cov",
1212
"prepublishOnly": "tshy && tshy-after"
1313
},
@@ -16,6 +16,7 @@
1616
"unescape": "^1.0.1"
1717
},
1818
"devDependencies": {
19+
"@arethetypeswrong/cli": "^0.17.1",
1920
"@eggjs/tsconfig": "^1.3.3",
2021
"@types/escape-html": "^1.0.4",
2122
"@types/mocha": "^10.0.6",

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './number.js';
88
export * from './string.js';
99
export * from './optimize.js';
1010
export * from './object.js';
11+
export * from './timeout.js';

src/timeout.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export class TimeoutError extends Error {
2+
timeout: number;
3+
4+
constructor(timeout: number) {
5+
super(`Timed out after ${timeout}ms`);
6+
this.name = this.constructor.name;
7+
this.timeout = timeout;
8+
Error.captureStackTrace(this, this.constructor);
9+
}
10+
}
11+
12+
// https://betterstack.com/community/guides/scaling-nodejs/nodejs-timeouts/
13+
export async function promiseTimeout<T>(
14+
promiseArg: Promise<T>,
15+
timeout: number,
16+
): Promise<T> {
17+
let timer: NodeJS.Timeout;
18+
const timeoutPromise = new Promise<never>((_, reject) => {
19+
timer = setTimeout(() => {
20+
reject(new TimeoutError(timeout));
21+
}, timeout);
22+
});
23+
24+
try {
25+
return await Promise.race([ promiseArg, timeoutPromise ]);
26+
} finally {
27+
clearTimeout(timer!);
28+
}
29+
}
30+
31+
export async function runWithTimeout<T>(
32+
scope: () => Promise<T>,
33+
timeout: number,
34+
): Promise<T> {
35+
return await promiseTimeout(scope(), timeout);
36+
}
37+

test/timeout.test.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { strict as assert } from 'node:assert';
2+
import * as utility from '../src/index.js';
3+
import { runWithTimeout, TimeoutError, promiseTimeout } from '../src/index.js';
4+
5+
function sleep(ms: number) {
6+
return new Promise(resolve => {
7+
setTimeout(resolve, ms);
8+
});
9+
}
10+
11+
describe('test/timeout.test.ts', () => {
12+
describe('runWithTimeout()', () => {
13+
it('should timeout', async () => {
14+
await assert.rejects(async () => {
15+
await runWithTimeout(async () => {
16+
await sleep(20);
17+
}, 10);
18+
}, (err: unknown) => {
19+
assert(err instanceof TimeoutError);
20+
assert.equal(err.timeout, 10);
21+
assert.equal(err.message, 'Timed out after 10ms');
22+
// console.error(err);
23+
return true;
24+
});
25+
26+
await assert.rejects(async () => {
27+
await utility.runWithTimeout(async () => {
28+
await sleep(1000);
29+
}, 15);
30+
}, (err: unknown) => {
31+
assert(err instanceof TimeoutError);
32+
assert.equal(err.timeout, 15);
33+
assert.equal(err.message, 'Timed out after 15ms');
34+
// console.error(err);
35+
return true;
36+
});
37+
});
38+
39+
it('should timeout', async () => {
40+
const result = await runWithTimeout(async () => {
41+
await sleep(20);
42+
return 100000;
43+
}, 100);
44+
assert.equal(result, 100000);
45+
});
46+
});
47+
48+
describe('promiseTimeout()', () => {
49+
it('should timeout', async () => {
50+
await assert.rejects(async () => {
51+
await promiseTimeout(sleep(20), 10);
52+
}, (err: unknown) => {
53+
assert(err instanceof TimeoutError);
54+
assert.equal(err.timeout, 10);
55+
assert.equal(err.message, 'Timed out after 10ms');
56+
// console.error(err);
57+
return true;
58+
});
59+
});
60+
});
61+
});

0 commit comments

Comments
 (0)