diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 075facf..8e62f3d 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -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 }}
diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml
new file mode 100644
index 0000000..bac3fac
--- /dev/null
+++ b/.github/workflows/pkg.pr.new.yml
@@ -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
diff --git a/README.md b/README.md
index ef6d8cc..7869776 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
[](https://github.com/node-modules/utility/actions/workflows/nodejs.yml)
[![Test coverage][codecov-image]][codecov-url]
[![npm download][download-image]][download-url]
+[](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
@@ -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/>"& &'
-utils.unescape('<script/>"& &'); // '"& &'
+escape('"& &');
+// '<script/>"& &'
+unescape('<script/>"& &');
+// '"& &'
// 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
@@ -164,12 +194,18 @@ utils.random(2, 1000); // [2, 1000)
utils.random(); // 0
```
-### Timers
+### Timeout
-```js
-utils.setImmediate(function () {
- console.log('hi');
-});
+#### `runWithTimeout(scope, timeout)`
+
+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`.
+
+```ts
+import { runWithTimeout } from 'utility';
+
+await runWithTimeout(async () => {
+ // long run operation here
+}, 1000);
```
### map
@@ -216,9 +252,9 @@ 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(...);
...
```
@@ -226,7 +262,7 @@ utility.UNSTABLE_METHOD.try(...);
```js
function foo() {
- const arr = utility.argumentsToArray(arguments);
+ const arr = utils.argumentsToArray(arguments);
console.log(arr.join(', '));
}
```
@@ -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
diff --git a/package.json b/package.json
index a100618..01f9f0c 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
@@ -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",
diff --git a/src/index.ts b/src/index.ts
index b8a38a9..aecde29 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,3 +8,4 @@ export * from './number.js';
export * from './string.js';
export * from './optimize.js';
export * from './object.js';
+export * from './timeout.js';
diff --git a/src/timeout.ts b/src/timeout.ts
new file mode 100644
index 0000000..0335604
--- /dev/null
+++ b/src/timeout.ts
@@ -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(
+ promiseArg: Promise,
+ timeout: number,
+): Promise {
+ let timer: NodeJS.Timeout;
+ const timeoutPromise = new Promise((_, reject) => {
+ timer = setTimeout(() => {
+ reject(new TimeoutError(timeout));
+ }, timeout);
+ });
+
+ try {
+ return await Promise.race([ promiseArg, timeoutPromise ]);
+ } finally {
+ clearTimeout(timer!);
+ }
+}
+
+export async function runWithTimeout(
+ scope: () => Promise,
+ timeout: number,
+): Promise {
+ return await promiseTimeout(scope(), timeout);
+}
+
diff --git a/test/timeout.test.ts b/test/timeout.test.ts
new file mode 100644
index 0000000..355428c
--- /dev/null
+++ b/test/timeout.test.ts
@@ -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;
+ });
+ });
+ });
+});