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 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.
File without changes
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // todo
3
+ //# sourceMappingURL=index.js.map
@@ -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
+ ![test](https://github.com/ehmpathy/with-bottleneck/workflows/test/badge.svg)
4
+ ![publish](https://github.com/ehmpathy/with-bottleneck/workflows/publish/badge.svg)
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.