Skip to content

Commit

Permalink
feat: support promise with timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 committed Dec 18, 2024
1 parent 08195c0 commit 4a82db6
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 36 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ jobs:
uses: node-modules/github-actions/.github/workflows/node-test.yml@master
with:
os: 'ubuntu-latest'
version: '16, 18, 20, 22'
version: '16, 18, 20, 22, 23'
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
23 changes: 23 additions & 0 deletions .github/workflows/pkg.pr.new.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Publish Any Commit
on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm install

- name: Build
run: npm run prepublishOnly --if-present

- run: npx pkg-pr-new publish
104 changes: 70 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![CI](https://github.com/node-modules/utility/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/utility/actions/workflows/nodejs.yml)
[![Test coverage][codecov-image]][codecov-url]
[![npm download][download-image]][download-url]
[![Node.js Version](https://img.shields.io/node/v/utility.svg?style=flat)](https://nodejs.org/en/download/)

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

```ts
import * as utility from 'utility';
import * as utils from 'utility';
```

### md5

```js
utils.md5('苏千').should.equal('5f733c47c58a077d61257102b2d44481');
utils.md5(Buffer.from('苏千')).should.equal('5f733c47c58a077d61257102b2d44481');
```ts
import { md5 } from 'utility';

md5('苏千');
// '5f733c47c58a077d61257102b2d44481'

md5(Buffer.from('苏千'));
// '5f733c47c58a077d61257102b2d44481'

// md5 base64 format
utils.md5('苏千', 'base64'); // 'X3M8R8WKB31hJXECstREgQ=='
md5('苏千', 'base64');
// 'X3M8R8WKB31hJXECstREgQ=='

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

### sha1

```js
utils.sha1('苏千').should.equal('0a4aff6bab634b9c2f99b71f25e976921fcde5a5');
utils.sha1(Buffer.from('苏千')).should.equal('0a4aff6bab634b9c2f99b71f25e976921fcde5a5');
```ts
import { sha1 } from 'utility';

sha1('苏千');
// '0a4aff6bab634b9c2f99b71f25e976921fcde5a5'

sha1(Buffer.from('苏千'));
// '0a4aff6bab634b9c2f99b71f25e976921fcde5a5'

// sha1 base64 format
utils.sha1('苏千', 'base64'); // 'Ckr/a6tjS5wvmbcfJel2kh/N5aU='
sha1('苏千', 'base64');
// 'Ckr/a6tjS5wvmbcfJel2kh/N5aU='

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

### sha256

```js
utils.sha256(Buffer.from('苏千')).should.equal('75dd03e3fcdbba7d5bec07900bae740cc8e361d77e7df8949de421d3df5d3635');
```ts
import { sha256 } from 'utility';

sha256(Buffer.from('苏千'));
// '75dd03e3fcdbba7d5bec07900bae740cc8e361d77e7df8949de421d3df5d3635'
```

### hmac

```js
```ts
import { hmac } from 'utility';

// hmac-sha1 with base64 output encoding
utils.hmac('sha1', 'I am a key', 'hello world'); // 'pO6J0LKDxRRkvSECSEdxwKx84L0='
hmac('sha1', 'I am a key', 'hello world');
// 'pO6J0LKDxRRkvSECSEdxwKx84L0='
```

### decode and encode

```js
```ts
import { base64encode, base64decode, escape, unescape, encodeURIComponent, decodeURIComponent } from 'utility';

// base64 encode
utils.base64encode('你好¥'); // '5L2g5aW977+l'
utils.base64decode('5L2g5aW977+l') // '你好¥'
base64encode('你好¥');
// '5L2g5aW977+l'
base64decode('5L2g5aW977+l');
// '你好¥'

// urlsafe base64 encode
utils.base64encode('你好¥', true); // '5L2g5aW977-l'
utils.base64decode('5L2g5aW977-l', true); // '你好¥'
base64encode('你好¥', true);
// '5L2g5aW977-l'
base64decode('5L2g5aW977-l', true);
// '你好¥'

// html escape and unescape
utils.escape('<script/>"& &amp;'); // '&lt;script/&gt;&quot;&amp; &amp;amp;'
utils.unescape('&lt;script/&gt;&quot;&amp; &amp;amp;'); // '<script/>"& &amp;'
escape('<script/>"& &amp;');
// '&lt;script/&gt;&quot;&amp; &amp;amp;'
unescape('&lt;script/&gt;&quot;&amp; &amp;amp;');
// '<script/>"& &amp;'

// Safe encodeURIComponent and decodeURIComponent
utils.decodeURIComponent(utils.encodeURIComponent('你好, nodejs')).should.equal('你好, nodejs');
decodeURIComponent(encodeURIComponent('你好, Node.js'));
// '你好, Node.js'
```

### others

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

```js
// get a function parameter's names
Expand Down Expand Up @@ -164,12 +194,18 @@ utils.random(2, 1000); // [2, 1000)
utils.random(); // 0
```

### Timers
### Timeout

```js
utils.setImmediate(function () {
console.log('hi');
});
#### `runWithTimeout(promise, timeout)`

Run promise with timeout

```ts
import { sha256 } from 'utility';

await runWithTimeout(async () => {
// long run operation here
}, 1000);
```

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

```js
import * as utility from 'utility';
import { UNSTABLE_METHOD } from 'utility';

utility.UNSTABLE_METHOD.try(...);
UNSTABLE_METHOD.try(...);
...
```

### argumentsToArray

```js
function foo() {
const arr = utility.argumentsToArray(arguments);
const arr = utils.argumentsToArray(arguments);
console.log(arr.join(', '));
}
```
Expand Down Expand Up @@ -268,10 +304,10 @@ async () => {

```js
// assign object
utility.assign({}, { a: 1 });
utils.assign({}, { a: 1 });

// assign multiple object
utility.assign({}, [ { a: 1 }, { b: 1 } ]);
utils.assign({}, [ { a: 1 }, { b: 1 } ]);
```

## benchmark
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"pretest": "npm run lint -- --fix && npm run prepublishOnly",
"test": "egg-bin test",
"test-local": "egg-bin test",
"preci": "npm run prepublishOnly",
"preci": "npm run lint && npm run prepublishOnly && attw --pack",
"ci": "egg-bin cov",
"prepublishOnly": "tshy && tshy-after"
},
Expand All @@ -16,6 +16,7 @@
"unescape": "^1.0.1"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.1",
"@eggjs/tsconfig": "^1.3.3",
"@types/escape-html": "^1.0.4",
"@types/mocha": "^10.0.6",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './number.js';
export * from './string.js';
export * from './optimize.js';
export * from './object.js';
export * from './timeout.js';
37 changes: 37 additions & 0 deletions src/timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export class TimeoutError extends Error {
timeout: number;

constructor(timeout: number) {
super(`Timed out after ${timeout}ms`);
this.name = this.constructor.name;
this.timeout = timeout;
Error.captureStackTrace(this, this.constructor);
}
}

// https://betterstack.com/community/guides/scaling-nodejs/nodejs-timeouts/
export async function promiseTimeout<T>(
promiseArg: Promise<T>,
timeout: number,
): Promise<T> {
let timer: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timer = setTimeout(() => {
reject(new TimeoutError(timeout));
}, timeout);
});

try {
return await Promise.race([ promiseArg, timeoutPromise ]);
} finally {
clearTimeout(timer!);
}
}

export async function runWithTimeout<T>(
scope: () => Promise<T>,
timeout: number,
): Promise<T> {
return await promiseTimeout(scope(), timeout);
}

61 changes: 61 additions & 0 deletions test/timeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { strict as assert } from 'node:assert';
import * as utility from '../src/index.js';
import { runWithTimeout, TimeoutError, promiseTimeout } from '../src/index.js';

function sleep(ms: number) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}

describe('test/timeout.test.ts', () => {
describe('runWithTimeout()', () => {
it('should timeout', async () => {
await assert.rejects(async () => {
await runWithTimeout(async () => {
await sleep(20);
}, 10);
}, (err: unknown) => {
assert(err instanceof TimeoutError);
assert.equal(err.timeout, 10);
assert.equal(err.message, 'Timed out after 10ms');
// console.error(err);
return true;
});

await assert.rejects(async () => {
await utility.runWithTimeout(async () => {
await sleep(1000);
}, 15);
}, (err: unknown) => {
assert(err instanceof TimeoutError);
assert.equal(err.timeout, 15);
assert.equal(err.message, 'Timed out after 15ms');
// console.error(err);
return true;
});
});

it('should timeout', async () => {
const result = await runWithTimeout(async () => {
await sleep(20);
return 100000;
}, 100);
assert.equal(result, 100000);
});
});

describe('promiseTimeout()', () => {
it('should timeout', async () => {
await assert.rejects(async () => {
await promiseTimeout(sleep(20), 10);
}, (err: unknown) => {
assert(err instanceof TimeoutError);
assert.equal(err.timeout, 10);
assert.equal(err.message, 'Timed out after 10ms');
// console.error(err);
return true;
});
});
});
});

0 comments on commit 4a82db6

Please sign in to comment.