with-bottleneck 0.0.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/dist/index.d.ts +0 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/license.md +21 -0
- package/package.json +102 -0
- package/readme.md +178 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Uladzimir Kasacheuski
|
|
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/dist/index.d.ts
ADDED
|
File without changes
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,OAAO"}
|
package/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 ehmpathy
|
|
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/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "with-bottleneck",
|
|
3
|
+
"author": "ehmpathy",
|
|
4
|
+
"description": "rate limit and concurrency control utilities",
|
|
5
|
+
"version": "0.0.0",
|
|
6
|
+
"repository": "ehmpathy/with-bottleneck",
|
|
7
|
+
"homepage": "https://github.com/ehmpathy/with-bottleneck",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"bottleneck",
|
|
10
|
+
"rate-limit",
|
|
11
|
+
"throttle",
|
|
12
|
+
"concurrency",
|
|
13
|
+
"limiter"
|
|
14
|
+
],
|
|
15
|
+
"bugs": "https://github.com/ehmpathy/with-bottleneck/issues",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=8.0.0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"/dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "2.3.8",
|
|
27
|
+
"@commitlint/cli": "19.5.0",
|
|
28
|
+
"@commitlint/config-conventional": "19.5.0",
|
|
29
|
+
"@swc/core": "1.15.3",
|
|
30
|
+
"@swc/jest": "0.2.39",
|
|
31
|
+
"@tsconfig/node-lts-strictest": "18.12.1",
|
|
32
|
+
"@tsconfig/node20": "20.1.5",
|
|
33
|
+
"@tsconfig/strictest": "2.0.5",
|
|
34
|
+
"@types/jest": "30.0.0",
|
|
35
|
+
"@types/node": "22.15.21",
|
|
36
|
+
"core-js": "3.26.1",
|
|
37
|
+
"cz-conventional-changelog": "3.3.0",
|
|
38
|
+
"declapract": "0.13.14",
|
|
39
|
+
"declapract-typescript-ehmpathy": "0.47.36",
|
|
40
|
+
"declastruct": "1.7.3",
|
|
41
|
+
"declastruct-github": "1.3.0",
|
|
42
|
+
"depcheck": "1.4.3",
|
|
43
|
+
"domain-objects": "0.31.9",
|
|
44
|
+
"esbuild-register": "3.6.0",
|
|
45
|
+
"helpful-errors": "1.5.3",
|
|
46
|
+
"husky": "8.0.3",
|
|
47
|
+
"jest": "30.2.0",
|
|
48
|
+
"rhachet": "1.41.9",
|
|
49
|
+
"rhachet-brains-anthropic": "0.4.1",
|
|
50
|
+
"rhachet-brains-xai": "0.3.3",
|
|
51
|
+
"rhachet-roles-bhrain": "0.27.6",
|
|
52
|
+
"rhachet-roles-bhuild": "0.21.9",
|
|
53
|
+
"rhachet-roles-ehmpathy": "1.35.5",
|
|
54
|
+
"test-fns": "1.7.2",
|
|
55
|
+
"ts-jest": "29.1.3",
|
|
56
|
+
"ts-node": "10.9.2",
|
|
57
|
+
"tsc-alias": "1.8.10",
|
|
58
|
+
"tsx": "4.20.6",
|
|
59
|
+
"typescript": "5.4.5",
|
|
60
|
+
"yalc": "1.0.0-pre.53"
|
|
61
|
+
},
|
|
62
|
+
"config": {
|
|
63
|
+
"commitizen": {
|
|
64
|
+
"path": "./node_modules/cz-conventional-changelog"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build:ts": "tsc -p ./tsconfig.build.json",
|
|
69
|
+
"commit:with-cli": "npx cz",
|
|
70
|
+
"fix:format:biome": "biome check --write",
|
|
71
|
+
"fix:format": "npm run fix:format:biome",
|
|
72
|
+
"fix:lint": "biome check --write",
|
|
73
|
+
"fix": "npm run fix:format && npm run fix:lint",
|
|
74
|
+
"build:clean:bun": "rm -f ./bin/*.bc",
|
|
75
|
+
"build:clean:tsc": "(chmod -R u+w dist 2>/dev/null || true) && rm -rf dist/",
|
|
76
|
+
"build:clean": "npm run build:clean:tsc && npm run build:clean:bun",
|
|
77
|
+
"build:compile:tsc": "tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
|
|
78
|
+
"build:compile": "npm run build:compile:tsc && npm run build:compile:bun --if-present",
|
|
79
|
+
"build": "npm run build:clean && npm run build:compile && npm run build:complete --if-present",
|
|
80
|
+
"test:auth": "[ \"$ECHO\" = 'true' ] && echo '. .agent/repo=.this/role=any/skills/use.apikeys.sh' || . .agent/repo=.this/role=any/skills/use.apikeys.sh",
|
|
81
|
+
"test:commits": "LAST_TAG=$(git describe --tags --abbrev=0 @^ 2> /dev/null || git rev-list --max-parents=0 HEAD) && npx commitlint --from $LAST_TAG --to HEAD --verbose",
|
|
82
|
+
"test:types": "tsc -p ./tsconfig.json --noEmit",
|
|
83
|
+
"test:format:biome": "biome format",
|
|
84
|
+
"test:format": "npm run test:format:biome",
|
|
85
|
+
"test:lint:deps": "npx depcheck -c ./.depcheckrc.yml",
|
|
86
|
+
"test:lint:biome": "biome check --diagnostic-level=error",
|
|
87
|
+
"test:lint:biome:all": "biome check",
|
|
88
|
+
"test:lint": "npm run test:lint:biome && npm run test:lint:deps",
|
|
89
|
+
"test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
90
|
+
"test:integration": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
91
|
+
"test:acceptance:locally": "npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
92
|
+
"test": "eval $(ECHO=true npm run --silent test:auth) && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
93
|
+
"test:acceptance": "npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
94
|
+
"prepush": "npm run test && npm run build",
|
|
95
|
+
"prepublish": "npm run build",
|
|
96
|
+
"preversion": "npm run prepush",
|
|
97
|
+
"postversion": "git push origin HEAD --tags --no-verify",
|
|
98
|
+
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
99
|
+
"prepare:rhachet": "rhachet init --hooks --roles behaver mechanic reviewer",
|
|
100
|
+
"upgrade:rhachet": "rhachet upgrade"
|
|
101
|
+
}
|
|
102
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# with-bottleneck
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
simple, ergonomic rate limit and concurrency control utilities
|
|
7
|
+
|
|
8
|
+
# install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install with-bottleneck
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
# why
|
|
15
|
+
|
|
16
|
+
don't spill your drink down your shirt — use a bottleneck to control the pour.
|
|
17
|
+
|
|
18
|
+
- limit api calls to avoid rate limit bans
|
|
19
|
+
- limit db connections to avoid saturation
|
|
20
|
+
- limit concurrent requests to respect server capacity
|
|
21
|
+
|
|
22
|
+
**bottleneck = intentional, precise flow control**
|
|
23
|
+
|
|
24
|
+
# use
|
|
25
|
+
|
|
26
|
+
## two usecases
|
|
27
|
+
|
|
28
|
+
`withBottleneck` supports two patterns for where the bottleneck comes from:
|
|
29
|
+
|
|
30
|
+
| usecase | bind time | bottleneck source | when |
|
|
31
|
+
|---------|-----------|-------------------|------|
|
|
32
|
+
| **per declaration** | declaration | static instance | global limits, scripts |
|
|
33
|
+
| **per context** | call time | extracted from context | per-tenant, DI, testability |
|
|
34
|
+
|
|
35
|
+
## usecase 1: per declaration
|
|
36
|
+
|
|
37
|
+
bottleneck created once, bound at declaration. all calls share same instance.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { withBottleneck, genBottleneck } from 'with-bottleneck';
|
|
41
|
+
|
|
42
|
+
// create bottleneck at module level
|
|
43
|
+
const bottleneck = genBottleneck({
|
|
44
|
+
concurrency: 5,
|
|
45
|
+
velocity: { quantity: 10, duration: { seconds: 1 } }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// bind to function at declaration
|
|
49
|
+
const fetchLimited = withBottleneck(fetch, { bottleneck });
|
|
50
|
+
|
|
51
|
+
// all calls share the same bottleneck
|
|
52
|
+
await fetchLimited('https://api.example.com/a');
|
|
53
|
+
await fetchLimited('https://api.example.com/b');
|
|
54
|
+
await fetchLimited('https://api.example.com/c');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## usecase 2: per context
|
|
58
|
+
|
|
59
|
+
bottleneck resolved from context at call time. each call can use different bottleneck.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { withBottleneck, genBottleneck, Bottleneck } from 'with-bottleneck';
|
|
63
|
+
|
|
64
|
+
// declare function — bottleneck comes from context
|
|
65
|
+
const fetchLimited = withBottleneck(fetch, {
|
|
66
|
+
bottleneck: (_input, context) => context.usecase.bottleneck,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// create different bottlenecks for different tenants
|
|
70
|
+
const customerBottleneck = genBottleneck({
|
|
71
|
+
concurrency: 2,
|
|
72
|
+
velocity: { quantity: 5, duration: { seconds: 1 } }
|
|
73
|
+
});
|
|
74
|
+
const adminBottleneck = genBottleneck({
|
|
75
|
+
concurrency: 10,
|
|
76
|
+
velocity: { quantity: 100, duration: { seconds: 1 } }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// each call uses bottleneck from its context
|
|
80
|
+
await fetchLimited('https://api.example.com/data', {
|
|
81
|
+
usecase: { bottleneck: customerBottleneck }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await fetchLimited('https://api.example.com/data', {
|
|
85
|
+
usecase: { bottleneck: adminBottleneck }
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
# exports
|
|
90
|
+
|
|
91
|
+
| export | purpose |
|
|
92
|
+
|--------|---------|
|
|
93
|
+
| `Bottleneck` | type — the bottleneck instance shape |
|
|
94
|
+
| `genBottleneck(config)` | create `Bottleneck` instance |
|
|
95
|
+
| `withBottleneck(fn, { bottleneck })` | wrap fn with bottleneck |
|
|
96
|
+
|
|
97
|
+
# genBottleneck config
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
interface BottleneckConfig {
|
|
101
|
+
/** max concurrent operations (how many at once) */
|
|
102
|
+
concurrency?: number;
|
|
103
|
+
|
|
104
|
+
/** rate limit (how many per time) */
|
|
105
|
+
velocity?: {
|
|
106
|
+
/** how many operations */
|
|
107
|
+
quantity: number;
|
|
108
|
+
/** per duration (IsoDuration from iso-time) */
|
|
109
|
+
duration: IsoDuration;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
examples:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// concurrency only: max 5 at once
|
|
118
|
+
genBottleneck({ concurrency: 5 });
|
|
119
|
+
|
|
120
|
+
// velocity only: max 10 per second
|
|
121
|
+
genBottleneck({ velocity: { quantity: 10, duration: { seconds: 1 } } });
|
|
122
|
+
|
|
123
|
+
// both: max 5 at once, max 100 per minute
|
|
124
|
+
genBottleneck({
|
|
125
|
+
concurrency: 5,
|
|
126
|
+
velocity: { quantity: 100, duration: { minutes: 1 } }
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
# Bottleneck shape
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
interface Bottleneck {
|
|
134
|
+
/** the inner semaphore — for manual acquire/release */
|
|
135
|
+
semaphore: {
|
|
136
|
+
acquire: () => Promise<void>;
|
|
137
|
+
release: () => void;
|
|
138
|
+
queued: number;
|
|
139
|
+
active: number;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** schedule fn for execution — acquires, runs, releases automatically */
|
|
143
|
+
schedule: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
direct semaphore access for complex flows:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// manual control
|
|
151
|
+
await bottleneck.semaphore.acquire();
|
|
152
|
+
try {
|
|
153
|
+
const a = await fetchA();
|
|
154
|
+
const b = await fetchB(a);
|
|
155
|
+
return transform(b);
|
|
156
|
+
} finally {
|
|
157
|
+
bottleneck.semaphore.release();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// check state
|
|
161
|
+
if (bottleneck.semaphore.queued > 100) {
|
|
162
|
+
throw new Error('queue too deep, backpressure');
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
# why "bottleneck"
|
|
167
|
+
|
|
168
|
+
a bottleneck is not a defect — it's precision design.
|
|
169
|
+
|
|
170
|
+
the neck of a beer bottle is *intentional*: controls pour rate, prevents spills, enables clean pour.
|
|
171
|
+
|
|
172
|
+
same here: we *want* a bottleneck to control flow.
|
|
173
|
+
|
|
174
|
+
# background
|
|
175
|
+
|
|
176
|
+
pour one out to the og, [`bottleneck`](https://www.npmjs.com/package/bottleneck), who paved the intuitive frame — abandoned since 2020.
|
|
177
|
+
|
|
178
|
+
we pick up where bottleneck left off; now, with wrappers and dependency injection.
|