├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchmarks └── limit.mjs ├── examples └── limit.mjs ├── index.js ├── lib └── limit.js ├── package.json └── test └── limit.test.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: standard 10 | versions: 11 | - 16.0.3 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | pull_request: 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | 13 | jobs: 14 | test: 15 | runs-on: ${{matrix.os}} 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 21.x] 20 | os: [ubuntu-latest] 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Use Node.js 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install 30 | run: | 31 | npm install 32 | 33 | - name: Run tests 34 | run: | 35 | npm run test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matteo Collina 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastp 2 | 3 | Fast promise utilities 4 | 5 | ## Install 6 | 7 | ```js 8 | npm i fastp 9 | ``` 10 | 11 | ## API 12 | 13 | ### `p.limit(concurrency=1)` 14 | 15 | Limit the number of promises resolved concurrently. 16 | Offers the same API of [p-limit](http://npm.im/p-limit) 17 | 18 | ```js 19 | import p from 'fastp' 20 | import { settimeout as sleep } from 'node:timers/promises' 21 | const limit = p.limit(2); 22 | 23 | const result1 = await Promise.all([ 24 | limit(() => sleep(300, 'foo')), 25 | limit(() => sleep(200, 'bar')), 26 | limit(() => sleep(100, 'baz')) 27 | ]); 28 | console.log(result1) 29 | // [ 'foo', 'bar', 'baz' ] 30 | 31 | const result2 = await Promise.race([ 32 | limit(() => sleep(300, 'foo')), 33 | limit(() => sleep(200, 'bar')), 34 | limit(() => sleep(100, 'baz')) 35 | ]); 36 | console.log(result2) 37 | // 'bar' 38 | ``` 39 | 40 | It's over 3x faster than `p-limit` 41 | 42 | ``` 43 | ╔══════════════╤═════════╤═════════════════╤═══════════╗ 44 | ║ Slower tests │ Samples │ Result │ Tolerance ║ 45 | ╟──────────────┼─────────┼─────────────────┼───────────╢ 46 | ║ pLimit │ 10000 │ 24994.77 op/sec │ ± 3.08 % ║ 47 | ╟──────────────┼─────────┼─────────────────┼───────────╢ 48 | ║ Fastest test │ Samples │ Result │ Tolerance ║ 49 | ╟──────────────┼─────────┼─────────────────┼───────────╢ 50 | ║ fastp.limit │ 10000 │ 91958.28 op/sec │ ± 5.76 % ║ 51 | ╚══════════════╧═════════╧═════════════════╧═══════════╝ 52 | ``` 53 | 54 | ## License 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /benchmarks/limit.mjs: -------------------------------------------------------------------------------- 1 | import cronometro from 'cronometro' 2 | import pLimit from 'p-limit' 3 | import p from '../index.js' 4 | import { setImmediate as immediate } from 'timers/promises' 5 | 6 | cronometro({ 7 | pLimit: async function () { 8 | const limit = pLimit(1) 9 | 10 | const input = [ 11 | limit(immediate), 12 | limit(immediate), 13 | limit(immediate) 14 | ] 15 | 16 | await Promise.all(input) 17 | }, 18 | 'fastp.limit': async function () { 19 | const limit = p.limit(1) 20 | 21 | const input = [ 22 | limit(immediate), 23 | limit(immediate), 24 | limit(immediate) 25 | ] 26 | 27 | await Promise.all(input) 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /examples/limit.mjs: -------------------------------------------------------------------------------- 1 | import p from '../index.js' 2 | import { setTimeout as sleep } from 'node:timers/promises' 3 | const limit = p.limit(2) 4 | 5 | const result1 = await Promise.all([ 6 | limit(() => sleep(300, 'foo')), 7 | limit(() => sleep(200, 'bar')), 8 | limit(() => sleep(100, 'baz')) 9 | ]) 10 | console.log(result1) 11 | // [ 'foo', 'bar', 'baz' ] 12 | 13 | const result2 = await Promise.race([ 14 | limit(() => sleep(300, 'foo')), 15 | limit(() => sleep(200, 'bar')), 16 | limit(() => sleep(100, 'baz')) 17 | ]) 18 | console.log(result2) 19 | // 'bar' 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports.limit = require('./lib/limit') 4 | -------------------------------------------------------------------------------- /lib/limit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { AsyncResource } = require('node:async_hooks') 4 | const fastq = require('fastq') 5 | 6 | function limit (concurrency = 1) { 7 | const q = fastq.promise(({ task, args, resource }) => { 8 | return resource.runInAsyncScope(task, null, ...args) 9 | }, concurrency) 10 | 11 | return async (task, ...args) => { 12 | const resource = new AsyncResource('fastp.limit') 13 | try { 14 | return await q.push({ task, args, resource }) 15 | } finally { 16 | resource.emitDestroy() 17 | } 18 | } 19 | } 20 | 21 | module.exports = limit 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastp", 3 | "version": "0.1.0", 4 | "description": "Fast promise utilities", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard | snazzy && borp --coverage" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mcollina/fastp.git" 12 | }, 13 | "keywords": [ 14 | "promise", 15 | "promises", 16 | "fast", 17 | "map", 18 | "limit", 19 | "stream", 20 | "transform" 21 | ], 22 | "author": "Matteo Collina ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/mcollina/fastp/issues" 26 | }, 27 | "homepage": "https://github.com/mcollina/fastp#readme", 28 | "dependencies": { 29 | "fastq": "^1.17.1" 30 | }, 31 | "devDependencies": { 32 | "@matteo.collina/tspl": "^0.1.1", 33 | "borp": "^0.20.0", 34 | "cronometro": "^4.0.0", 35 | "p-limit": "^6.1.0", 36 | "p-map": "^7.0.1", 37 | "snazzy": "^9.0.0", 38 | "standard": "^17.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/limit.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, describe } = require('node:test') 4 | const { AsyncLocalStorage } = require('node:async_hooks') 5 | const assert = require('assert') 6 | const { setTimeout: sleep } = require('timers/promises') 7 | const fastp = require('../') 8 | 9 | describe('limit', () => { 10 | test('should limit the number of parallel promises', async () => { 11 | const input = [1, 2, 3, 4, 5] 12 | const expected = [1, 2, 3, 4, 5] 13 | const inflight = new Set() 14 | const actual = [] 15 | const limit = fastp.limit(2) 16 | await Promise.all(input.map(i => { 17 | return limit(async () => { 18 | assert.strictEqual(inflight.size <= 2, true) 19 | inflight.add(i) 20 | await sleep(1) 21 | inflight.delete(i) 22 | actual.push(i) 23 | }) 24 | })) 25 | assert.deepStrictEqual(actual, expected) 26 | }) 27 | 28 | test('should pass arguments to the task', async () => { 29 | const input = [1, 2, 3, 4, 5] 30 | const expected = [1, 2, 3, 4, 5] 31 | const actual = [] 32 | const limit = fastp.limit(2) 33 | await Promise.all(input.map(i => { 34 | return limit(async (j) => { 35 | actual.push(j) 36 | }, i) 37 | })) 38 | assert.deepStrictEqual(actual, expected) 39 | }) 40 | 41 | test('should return the value', async () => { 42 | const input = [1, 2, 3, 4, 5] 43 | const expected = [1, 2, 3, 4, 5] 44 | const limit = fastp.limit(2) 45 | const res = await Promise.all(input.map(i => limit(fn, i))) 46 | 47 | async function fn (j) { 48 | return j 49 | } 50 | assert.deepStrictEqual(res, expected) 51 | }) 52 | 53 | test('should propagate async context', async () => { 54 | const input = [1, 2, 3, 4, 5] 55 | const limit = fastp.limit(2) 56 | const storage = new AsyncLocalStorage() 57 | await Promise.all(input.map(i => { 58 | return storage.run({ i }, async () => { 59 | return limit(async (j) => { 60 | assert.deepStrictEqual(storage.getStore(), { i }) 61 | }, i) 62 | }) 63 | })) 64 | }) 65 | 66 | test('should reject with errors', async () => { 67 | const input = [1, 2, 3, 4, 5] 68 | const limit = fastp.limit(2) 69 | const expected = new Error('boom') 70 | const actual = [] 71 | try { 72 | await Promise.all(input.map(i => { 73 | return limit(async () => { 74 | if (i === 3) { 75 | throw expected 76 | } 77 | actual.push(i) 78 | }) 79 | })) 80 | } catch (err) { 81 | assert.strictEqual(err, expected) 82 | } 83 | assert.deepStrictEqual(actual, [1, 2, 4, 5]) 84 | }) 85 | }) 86 | --------------------------------------------------------------------------------