throttleai 0.1.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/README.md +308 -0
- package/dist/adapters/express.cjs +75 -0
- package/dist/adapters/express.cjs.map +1 -0
- package/dist/adapters/express.d.cts +79 -0
- package/dist/adapters/express.d.ts +79 -0
- package/dist/adapters/express.js +50 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fetch.cjs +99 -0
- package/dist/adapters/fetch.cjs.map +1 -0
- package/dist/adapters/fetch.d.cts +69 -0
- package/dist/adapters/fetch.d.ts +69 -0
- package/dist/adapters/fetch.js +68 -0
- package/dist/adapters/fetch.js.map +1 -0
- package/dist/adapters/hono.cjs +74 -0
- package/dist/adapters/hono.cjs.map +1 -0
- package/dist/adapters/hono.d.cts +73 -0
- package/dist/adapters/hono.d.ts +73 -0
- package/dist/adapters/hono.js +49 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/openai.cjs +103 -0
- package/dist/adapters/openai.cjs.map +1 -0
- package/dist/adapters/openai.d.cts +102 -0
- package/dist/adapters/openai.d.ts +102 -0
- package/dist/adapters/openai.js +70 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/tools.cjs +80 -0
- package/dist/adapters/tools.cjs.map +1 -0
- package/dist/adapters/tools.d.cts +56 -0
- package/dist/adapters/tools.d.ts +56 -0
- package/dist/adapters/tools.js +49 -0
- package/dist/adapters/tools.js.map +1 -0
- package/dist/chunk-YHOXYRXL.js +11 -0
- package/dist/chunk-YHOXYRXL.js.map +1 -0
- package/dist/governor-MVaCesqM.d.cts +206 -0
- package/dist/governor-MVaCesqM.d.ts +206 -0
- package/dist/index.cjs +1163 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +213 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.js +1128 -0
- package/dist/index.js.map +1 -0
- package/dist/types-BkfBESR2.d.ts +47 -0
- package/dist/types-DOUI5hr7.d.cts +47 -0
- package/package.json +114 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mcp-tool-shop
|
|
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/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.png" alt="ThrottleAI" width="400">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/throttleai"><img src="https://img.shields.io/npm/v/throttleai" alt="npm"></a>
|
|
7
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<em>A token-based lease governor for AI calls — small enough to embed anywhere, strict enough to prevent stampedes.</em>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 60-second quickstart
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add throttleai
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createGovernor, withLease, presets } from "throttleai";
|
|
24
|
+
|
|
25
|
+
const gov = createGovernor(presets.balanced());
|
|
26
|
+
|
|
27
|
+
const result = await withLease(
|
|
28
|
+
gov,
|
|
29
|
+
{ actorId: "user-1", action: "chat" },
|
|
30
|
+
async () => await callMyModel(),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (result.granted) {
|
|
34
|
+
console.log(result.result);
|
|
35
|
+
} else {
|
|
36
|
+
console.log("Throttled:", result.decision.recommendation);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That's it. The governor enforces concurrency, rate limits, and fairness. Leases auto-expire if you forget to release.
|
|
41
|
+
|
|
42
|
+
## Why
|
|
43
|
+
|
|
44
|
+
AI applications hit rate limits, blow budgets, and create stampedes. ThrottleAI sits between your code and the model call, enforcing:
|
|
45
|
+
|
|
46
|
+
- **Concurrency** — cap in-flight calls with weighted slots and interactive reserve
|
|
47
|
+
- **Rate** — requests/min and tokens/min with rolling windows
|
|
48
|
+
- **Fairness** — no single actor monopolizes capacity
|
|
49
|
+
- **Leases** — acquire before, release after, auto-expire on timeout
|
|
50
|
+
- **Observability** — `snapshot()`, `onEvent`, and `formatEvent()` for debugging
|
|
51
|
+
|
|
52
|
+
Zero dependencies. Node.js 18+. Tree-shakeable.
|
|
53
|
+
|
|
54
|
+
## Presets
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { presets } from "throttleai";
|
|
58
|
+
|
|
59
|
+
// Single user, CLI tools — 1 call at a time, 10 req/min
|
|
60
|
+
createGovernor(presets.quiet());
|
|
61
|
+
|
|
62
|
+
// SaaS backend — 5 concurrent (2 interactive reserve), 60 req/min, fairness
|
|
63
|
+
createGovernor(presets.balanced());
|
|
64
|
+
|
|
65
|
+
// Batch processing — 20 concurrent, 300 req/min, fairness + adaptive tuning
|
|
66
|
+
createGovernor(presets.aggressive());
|
|
67
|
+
|
|
68
|
+
// Override any field
|
|
69
|
+
createGovernor({ ...presets.balanced(), leaseTtlMs: 30_000 });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Common patterns
|
|
73
|
+
|
|
74
|
+
### Server endpoint: 429 vs queue
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// Option A: immediate deny with 429
|
|
78
|
+
const result = await withLease(gov, request, fn);
|
|
79
|
+
// result.granted === false → respond with 429
|
|
80
|
+
|
|
81
|
+
// Option B: wait with bounded retries
|
|
82
|
+
const result = await withLease(gov, request, fn, {
|
|
83
|
+
strategy: "wait-then-deny",
|
|
84
|
+
maxAttempts: 3,
|
|
85
|
+
maxWaitMs: 5_000,
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### UI interactive vs background
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// User-facing chat gets priority
|
|
93
|
+
gov.acquire({ actorId: "user", action: "chat", priority: "interactive" });
|
|
94
|
+
|
|
95
|
+
// Background embedding can wait
|
|
96
|
+
gov.acquire({ actorId: "pipeline", action: "embed", priority: "background" });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With `interactiveReserve: 2`, background tasks are blocked when only 2 slots remain, keeping those for interactive requests.
|
|
100
|
+
|
|
101
|
+
### Streaming calls
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const decision = gov.acquire({ actorId: "user", action: "stream" });
|
|
105
|
+
if (!decision.granted) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const stream = await openai.chat.completions.create({ stream: true, ... });
|
|
109
|
+
for await (const chunk of stream) {
|
|
110
|
+
// process chunk
|
|
111
|
+
}
|
|
112
|
+
gov.release(decision.leaseId, { outcome: "success" });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
gov.release(decision.leaseId, { outcome: "error" });
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Acquire once, release once — the lease holds for the entire stream duration.
|
|
120
|
+
|
|
121
|
+
### Observability: see why it throttles
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { createGovernor, formatEvent, formatSnapshot } from "throttleai";
|
|
125
|
+
|
|
126
|
+
const gov = createGovernor({
|
|
127
|
+
...presets.balanced(),
|
|
128
|
+
onEvent: (e) => console.log(formatEvent(e)),
|
|
129
|
+
// [deny] actor=user-1 action=chat reason=concurrency retryAfterMs=500 — All 5 slots in use...
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Point-in-time view
|
|
133
|
+
console.log(formatSnapshot(gov.snapshot()));
|
|
134
|
+
// concurrency=3/5 rate=12/60 leases=3
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
createGovernor({
|
|
141
|
+
// Concurrency (optional)
|
|
142
|
+
concurrency: {
|
|
143
|
+
maxInFlight: 5, // max simultaneous weight
|
|
144
|
+
interactiveReserve: 1, // slots reserved for interactive priority
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// Rate limiting (optional)
|
|
148
|
+
rate: {
|
|
149
|
+
requestsPerMinute: 60, // request-rate cap
|
|
150
|
+
tokensPerMinute: 100_000, // token-rate cap
|
|
151
|
+
windowMs: 60_000, // rolling window (default 60s)
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Advanced (optional)
|
|
155
|
+
fairness: true, // prevent actor monopolization
|
|
156
|
+
adaptive: true, // auto-tune concurrency from deny rate + latency
|
|
157
|
+
strict: true, // throw on double release / unknown ID (dev mode)
|
|
158
|
+
|
|
159
|
+
// Lease settings
|
|
160
|
+
leaseTtlMs: 60_000, // auto-expire (default 60s)
|
|
161
|
+
reaperIntervalMs: 5_000, // sweep interval (default 5s)
|
|
162
|
+
|
|
163
|
+
// Observability
|
|
164
|
+
onEvent: (e) => { /* acquire, deny, release, expire, warn */ },
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## API
|
|
169
|
+
|
|
170
|
+
### `createGovernor(config): Governor`
|
|
171
|
+
|
|
172
|
+
Factory function. Returns a `Governor` instance.
|
|
173
|
+
|
|
174
|
+
### `governor.acquire(request): AcquireDecision`
|
|
175
|
+
|
|
176
|
+
Request a lease. Returns:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// Granted
|
|
180
|
+
{ granted: true, leaseId: string, expiresAt: number }
|
|
181
|
+
|
|
182
|
+
// Denied
|
|
183
|
+
{ granted: false, reason, retryAfterMs, recommendation, limitsHint? }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Deny reasons: `"concurrency"` | `"rate"` | `"budget"` | `"policy"`
|
|
187
|
+
|
|
188
|
+
### `governor.release(leaseId, report?): void`
|
|
189
|
+
|
|
190
|
+
Release a lease. Always call this — even on errors.
|
|
191
|
+
|
|
192
|
+
### `withLease(governor, request, fn, options?)`
|
|
193
|
+
|
|
194
|
+
Execute `fn` under a lease with automatic release.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
withLease(gov, request, fn, {
|
|
198
|
+
strategy: "deny", // default — fail immediately
|
|
199
|
+
strategy: "wait", // retry with backoff until maxWaitMs
|
|
200
|
+
strategy: "wait-then-deny", // retry up to maxAttempts
|
|
201
|
+
maxWaitMs: 10_000, // max total wait (default 10s)
|
|
202
|
+
maxAttempts: 3, // for "wait-then-deny" (default 3)
|
|
203
|
+
initialBackoffMs: 250, // starting backoff (default 250ms)
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `governor.snapshot(): GovernorSnapshot`
|
|
208
|
+
|
|
209
|
+
Point-in-time state: concurrency, rate, tokens, last deny.
|
|
210
|
+
|
|
211
|
+
### `formatEvent(event): string` / `formatSnapshot(snap): string`
|
|
212
|
+
|
|
213
|
+
One-line human-readable formatters.
|
|
214
|
+
|
|
215
|
+
### Status getters
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
gov.activeLeases // active lease count
|
|
219
|
+
gov.concurrencyActive // in-flight weight
|
|
220
|
+
gov.concurrencyAvailable // remaining capacity
|
|
221
|
+
gov.rateCount // requests in current window
|
|
222
|
+
gov.tokenRateCount // tokens in current window
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `governor.dispose(): void`
|
|
226
|
+
|
|
227
|
+
Stop the TTL reaper. Call on shutdown.
|
|
228
|
+
|
|
229
|
+
## Adapters
|
|
230
|
+
|
|
231
|
+
Tree-shakeable wrappers — import only what you use. No runtime deps.
|
|
232
|
+
|
|
233
|
+
### fetch
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { wrapFetch } from "throttleai/adapters/fetch";
|
|
237
|
+
const throttledFetch = wrapFetch(fetch, { governor: gov });
|
|
238
|
+
const r = await throttledFetch("https://api.example.com/v1/chat");
|
|
239
|
+
if (r.ok) console.log(r.response.status);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### OpenAI-compatible
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { wrapChatCompletions } from "throttleai/adapters/openai";
|
|
246
|
+
const chat = wrapChatCompletions(openai.chat.completions.create, { governor: gov });
|
|
247
|
+
const r = await chat({ model: "gpt-4", messages });
|
|
248
|
+
if (r.ok) console.log(r.result.choices[0].message.content);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Tool call
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { wrapTool } from "throttleai/adapters/tools";
|
|
255
|
+
const embed = wrapTool(myEmbedFn, { governor: gov, toolId: "embed", costWeight: 2 });
|
|
256
|
+
const r = await embed("hello");
|
|
257
|
+
if (r.ok) console.log(r.result);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Express
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { throttleMiddleware } from "throttleai/adapters/express";
|
|
264
|
+
app.use("/ai", throttleMiddleware({ governor: gov }));
|
|
265
|
+
// 429 + Retry-After header + JSON body on deny
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Hono
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { throttle } from "throttleai/adapters/hono";
|
|
272
|
+
app.use("/ai/*", throttle({ governor: gov }));
|
|
273
|
+
// 429 JSON on deny, leaseId stored on context
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
All adapters return `{ ok: true, result, latencyMs }` on grant, `{ ok: false, decision }` on deny.
|
|
277
|
+
|
|
278
|
+
## Tuning guide
|
|
279
|
+
|
|
280
|
+
| You see this | Adjust this |
|
|
281
|
+
|---|---|
|
|
282
|
+
| `reason: "concurrency"` | Increase `maxInFlight` or decrease call duration |
|
|
283
|
+
| `reason: "rate"` | Increase `requestsPerMinute` / `tokensPerMinute` |
|
|
284
|
+
| `reason: "policy"` (fairness) | Lower `softCapRatio` or increase `maxInFlight` |
|
|
285
|
+
| High `retryAfterMs` | Reduce `leaseTtlMs` so expired leases free faster |
|
|
286
|
+
| Background tasks starved | Increase `maxInFlight` or reduce `interactiveReserve` |
|
|
287
|
+
| Interactive latency high | Increase `interactiveReserve` |
|
|
288
|
+
| Adaptive shrinks too fast | Lower `alpha` or raise `targetDenyRate` |
|
|
289
|
+
|
|
290
|
+
Use `snapshot()` and `formatSnapshot()` to observe state in production.
|
|
291
|
+
|
|
292
|
+
## Examples
|
|
293
|
+
|
|
294
|
+
See [`examples/`](examples/) for runnable demos:
|
|
295
|
+
|
|
296
|
+
- **[node-basic.ts](examples/node-basic.ts)** — burst simulation with snapshot printing
|
|
297
|
+
- **[express-middleware.ts](examples/express-middleware.ts)** — 429 + retry-after endpoint
|
|
298
|
+
- **[cookbook-adapters.ts](examples/cookbook-adapters.ts)** — all five adapters in action
|
|
299
|
+
- **[cookbook-burst-snapshot.ts](examples/cookbook-burst-snapshot.ts)** — burst load with governor snapshots
|
|
300
|
+
- **[cookbook-interactive-reserve.ts](examples/cookbook-interactive-reserve.ts)** — interactive vs background priority
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npx tsx examples/node-basic.ts
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/express.ts
|
|
21
|
+
var express_exports = {};
|
|
22
|
+
__export(express_exports, {
|
|
23
|
+
throttleMiddleware: () => throttleMiddleware
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(express_exports);
|
|
26
|
+
function throttleMiddleware(options) {
|
|
27
|
+
const {
|
|
28
|
+
governor,
|
|
29
|
+
getActorId,
|
|
30
|
+
getAction,
|
|
31
|
+
getPriority,
|
|
32
|
+
getEstimate,
|
|
33
|
+
onDeny
|
|
34
|
+
} = options;
|
|
35
|
+
return (req, res, next) => {
|
|
36
|
+
const actorId = getActorId ? getActorId(req) : asString(req.headers["x-actor-id"]) ?? req.ip ?? "anonymous";
|
|
37
|
+
const request = {
|
|
38
|
+
actorId,
|
|
39
|
+
action: getAction ? getAction(req) : req.path,
|
|
40
|
+
priority: getPriority ? getPriority(req) : "interactive",
|
|
41
|
+
estimate: getEstimate ? getEstimate(req) : void 0
|
|
42
|
+
};
|
|
43
|
+
const decision = governor.acquire(request);
|
|
44
|
+
if (!decision.granted) {
|
|
45
|
+
if (onDeny) {
|
|
46
|
+
onDeny(req, res, decision);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
res.setHeader("Retry-After", String(Math.ceil(decision.retryAfterMs / 1e3)));
|
|
50
|
+
res.status(429).json({
|
|
51
|
+
error: "Too many requests",
|
|
52
|
+
reason: decision.reason,
|
|
53
|
+
retryAfterMs: decision.retryAfterMs,
|
|
54
|
+
recommendation: decision.recommendation
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const leaseId = decision.leaseId;
|
|
59
|
+
res.on("finish", () => {
|
|
60
|
+
governor.release(leaseId, {
|
|
61
|
+
outcome: (res.statusCode ?? 200) < 400 ? "success" : "error"
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
next();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function asString(value) {
|
|
68
|
+
if (Array.isArray(value)) return value[0];
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
72
|
+
0 && (module.exports = {
|
|
73
|
+
throttleMiddleware
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=express.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/express.ts"],"sourcesContent":["/**\n * ThrottleAI Express adapter — drop-in middleware.\n *\n * No dependency on Express — this exports a plain function that\n * returns `(req, res, next)`. You already have Express installed.\n *\n * @module throttleai/adapters/express\n */\n\nexport type {\n AdapterGovernor,\n AdapterOptions,\n} from \"./types.js\";\n\nimport type { AcquireRequest, AcquireDecision, Priority, TokenEstimate } from \"../types.js\";\nimport type { AdapterGovernor } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Types — use minimal shapes so we don't need @types/express\n// ---------------------------------------------------------------------------\n\n/** Minimal Express-compatible request shape. */\nexport interface ExpressLikeRequest {\n path: string;\n method: string;\n ip?: string;\n headers: Record<string, string | string[] | undefined>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Minimal Express-compatible response shape. */\nexport interface ExpressLikeResponse {\n status(code: number): this;\n json(body: unknown): void;\n setHeader(name: string, value: string | number): void;\n on(event: string, listener: () => void): void;\n statusCode?: number;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Options for the Express throttle middleware. */\nexport interface ThrottleMiddlewareOptions {\n /** Governor instance. */\n governor: AdapterGovernor;\n /**\n * Derive the actor ID from the request (default: x-actor-id header or req.ip).\n */\n getActorId?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the action from the request (default: req.path).\n */\n getAction?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the priority from the request (default: interactive).\n */\n getPriority?: (req: ExpressLikeRequest) => Priority;\n /**\n * Derive a token estimate from the request (optional).\n */\n getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;\n /**\n * Custom handler for denied requests (default: 429 JSON response).\n */\n onDeny?: (\n req: ExpressLikeRequest,\n res: ExpressLikeResponse,\n decision: AcquireDecision & { granted: false },\n ) => void;\n}\n\n// ---------------------------------------------------------------------------\n// throttleMiddleware\n// ---------------------------------------------------------------------------\n\n/**\n * Create an Express middleware that throttles requests via the governor.\n *\n * ```ts\n * import express from \"express\";\n * import { createGovernor, presets } from \"throttleai\";\n * import { throttleMiddleware } from \"throttleai/adapters/express\";\n *\n * const gov = createGovernor(presets.balanced());\n * const app = express();\n *\n * app.use(\"/ai\", throttleMiddleware({ governor: gov }));\n *\n * app.post(\"/ai/chat\", (req, res) => {\n * // This only runs if the governor granted a lease\n * res.json({ message: \"ok\" });\n * });\n * ```\n */\nexport function throttleMiddleware(\n options: ThrottleMiddlewareOptions,\n): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void {\n const {\n governor,\n getActorId,\n getAction,\n getPriority,\n getEstimate,\n onDeny,\n } = options;\n\n return (req, res, next) => {\n const actorId = getActorId\n ? getActorId(req)\n : (asString(req.headers[\"x-actor-id\"]) ?? req.ip ?? \"anonymous\");\n\n const request: AcquireRequest = {\n actorId,\n action: getAction ? getAction(req) : req.path,\n priority: getPriority ? getPriority(req) : \"interactive\",\n estimate: getEstimate ? getEstimate(req) : undefined,\n };\n\n const decision = governor.acquire(request);\n\n if (!decision.granted) {\n if (onDeny) {\n onDeny(req, res, decision as AcquireDecision & { granted: false });\n return;\n }\n\n // Default: 429 JSON response\n res.setHeader(\"Retry-After\", String(Math.ceil(decision.retryAfterMs / 1000)));\n res.status(429).json({\n error: \"Too many requests\",\n reason: decision.reason,\n retryAfterMs: decision.retryAfterMs,\n recommendation: decision.recommendation,\n });\n return;\n }\n\n // Release lease when response finishes\n const leaseId = decision.leaseId;\n res.on(\"finish\", () => {\n governor.release(leaseId, {\n outcome: (res.statusCode ?? 200) < 400 ? \"success\" : \"error\",\n });\n });\n\n next();\n };\n}\n\n/** Safely convert a header value to string. */\nfunction asString(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0];\n return value;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA+FO,SAAS,mBACd,SAC+E;AAC/E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,SAAO,CAAC,KAAK,KAAK,SAAS;AACzB,UAAM,UAAU,aACZ,WAAW,GAAG,IACb,SAAS,IAAI,QAAQ,YAAY,CAAC,KAAK,IAAI,MAAM;AAEtD,UAAM,UAA0B;AAAA,MAC9B;AAAA,MACA,QAAQ,YAAY,UAAU,GAAG,IAAI,IAAI;AAAA,MACzC,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,MAC3C,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,IAC7C;AAEA,UAAM,WAAW,SAAS,QAAQ,OAAO;AAEzC,QAAI,CAAC,SAAS,SAAS;AACrB,UAAI,QAAQ;AACV,eAAO,KAAK,KAAK,QAAgD;AACjE;AAAA,MACF;AAGA,UAAI,UAAU,eAAe,OAAO,KAAK,KAAK,SAAS,eAAe,GAAI,CAAC,CAAC;AAC5E,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,QAAQ,SAAS;AAAA,QACjB,cAAc,SAAS;AAAA,QACvB,gBAAgB,SAAS;AAAA,MAC3B,CAAC;AACD;AAAA,IACF;AAGA,UAAM,UAAU,SAAS;AACzB,QAAI,GAAG,UAAU,MAAM;AACrB,eAAS,QAAQ,SAAS;AAAA,QACxB,UAAU,IAAI,cAAc,OAAO,MAAM,YAAY;AAAA,MACvD,CAAC;AAAA,IACH,CAAC;AAED,SAAK;AAAA,EACP;AACF;AAGA,SAAS,SAAS,OAA0D;AAC1E,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,CAAC;AACxC,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { A as AdapterGovernor } from '../types-DOUI5hr7.cjs';
|
|
2
|
+
export { a as AdapterOptions } from '../types-DOUI5hr7.cjs';
|
|
3
|
+
import { P as Priority, T as TokenEstimate, A as AcquireDecision } from '../governor-MVaCesqM.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ThrottleAI Express adapter — drop-in middleware.
|
|
7
|
+
*
|
|
8
|
+
* No dependency on Express — this exports a plain function that
|
|
9
|
+
* returns `(req, res, next)`. You already have Express installed.
|
|
10
|
+
*
|
|
11
|
+
* @module throttleai/adapters/express
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Minimal Express-compatible request shape. */
|
|
15
|
+
interface ExpressLikeRequest {
|
|
16
|
+
path: string;
|
|
17
|
+
method: string;
|
|
18
|
+
ip?: string;
|
|
19
|
+
headers: Record<string, string | string[] | undefined>;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
/** Minimal Express-compatible response shape. */
|
|
23
|
+
interface ExpressLikeResponse {
|
|
24
|
+
status(code: number): this;
|
|
25
|
+
json(body: unknown): void;
|
|
26
|
+
setHeader(name: string, value: string | number): void;
|
|
27
|
+
on(event: string, listener: () => void): void;
|
|
28
|
+
statusCode?: number;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
/** Options for the Express throttle middleware. */
|
|
32
|
+
interface ThrottleMiddlewareOptions {
|
|
33
|
+
/** Governor instance. */
|
|
34
|
+
governor: AdapterGovernor;
|
|
35
|
+
/**
|
|
36
|
+
* Derive the actor ID from the request (default: x-actor-id header or req.ip).
|
|
37
|
+
*/
|
|
38
|
+
getActorId?: (req: ExpressLikeRequest) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Derive the action from the request (default: req.path).
|
|
41
|
+
*/
|
|
42
|
+
getAction?: (req: ExpressLikeRequest) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Derive the priority from the request (default: interactive).
|
|
45
|
+
*/
|
|
46
|
+
getPriority?: (req: ExpressLikeRequest) => Priority;
|
|
47
|
+
/**
|
|
48
|
+
* Derive a token estimate from the request (optional).
|
|
49
|
+
*/
|
|
50
|
+
getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Custom handler for denied requests (default: 429 JSON response).
|
|
53
|
+
*/
|
|
54
|
+
onDeny?: (req: ExpressLikeRequest, res: ExpressLikeResponse, decision: AcquireDecision & {
|
|
55
|
+
granted: false;
|
|
56
|
+
}) => void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an Express middleware that throttles requests via the governor.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* import express from "express";
|
|
63
|
+
* import { createGovernor, presets } from "throttleai";
|
|
64
|
+
* import { throttleMiddleware } from "throttleai/adapters/express";
|
|
65
|
+
*
|
|
66
|
+
* const gov = createGovernor(presets.balanced());
|
|
67
|
+
* const app = express();
|
|
68
|
+
*
|
|
69
|
+
* app.use("/ai", throttleMiddleware({ governor: gov }));
|
|
70
|
+
*
|
|
71
|
+
* app.post("/ai/chat", (req, res) => {
|
|
72
|
+
* // This only runs if the governor granted a lease
|
|
73
|
+
* res.json({ message: "ok" });
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
declare function throttleMiddleware(options: ThrottleMiddlewareOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
|
|
78
|
+
|
|
79
|
+
export { AdapterGovernor, type ExpressLikeRequest, type ExpressLikeResponse, type ThrottleMiddlewareOptions, throttleMiddleware };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { A as AdapterGovernor } from '../types-BkfBESR2.js';
|
|
2
|
+
export { a as AdapterOptions } from '../types-BkfBESR2.js';
|
|
3
|
+
import { P as Priority, T as TokenEstimate, A as AcquireDecision } from '../governor-MVaCesqM.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ThrottleAI Express adapter — drop-in middleware.
|
|
7
|
+
*
|
|
8
|
+
* No dependency on Express — this exports a plain function that
|
|
9
|
+
* returns `(req, res, next)`. You already have Express installed.
|
|
10
|
+
*
|
|
11
|
+
* @module throttleai/adapters/express
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Minimal Express-compatible request shape. */
|
|
15
|
+
interface ExpressLikeRequest {
|
|
16
|
+
path: string;
|
|
17
|
+
method: string;
|
|
18
|
+
ip?: string;
|
|
19
|
+
headers: Record<string, string | string[] | undefined>;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
/** Minimal Express-compatible response shape. */
|
|
23
|
+
interface ExpressLikeResponse {
|
|
24
|
+
status(code: number): this;
|
|
25
|
+
json(body: unknown): void;
|
|
26
|
+
setHeader(name: string, value: string | number): void;
|
|
27
|
+
on(event: string, listener: () => void): void;
|
|
28
|
+
statusCode?: number;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
/** Options for the Express throttle middleware. */
|
|
32
|
+
interface ThrottleMiddlewareOptions {
|
|
33
|
+
/** Governor instance. */
|
|
34
|
+
governor: AdapterGovernor;
|
|
35
|
+
/**
|
|
36
|
+
* Derive the actor ID from the request (default: x-actor-id header or req.ip).
|
|
37
|
+
*/
|
|
38
|
+
getActorId?: (req: ExpressLikeRequest) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Derive the action from the request (default: req.path).
|
|
41
|
+
*/
|
|
42
|
+
getAction?: (req: ExpressLikeRequest) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Derive the priority from the request (default: interactive).
|
|
45
|
+
*/
|
|
46
|
+
getPriority?: (req: ExpressLikeRequest) => Priority;
|
|
47
|
+
/**
|
|
48
|
+
* Derive a token estimate from the request (optional).
|
|
49
|
+
*/
|
|
50
|
+
getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Custom handler for denied requests (default: 429 JSON response).
|
|
53
|
+
*/
|
|
54
|
+
onDeny?: (req: ExpressLikeRequest, res: ExpressLikeResponse, decision: AcquireDecision & {
|
|
55
|
+
granted: false;
|
|
56
|
+
}) => void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an Express middleware that throttles requests via the governor.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* import express from "express";
|
|
63
|
+
* import { createGovernor, presets } from "throttleai";
|
|
64
|
+
* import { throttleMiddleware } from "throttleai/adapters/express";
|
|
65
|
+
*
|
|
66
|
+
* const gov = createGovernor(presets.balanced());
|
|
67
|
+
* const app = express();
|
|
68
|
+
*
|
|
69
|
+
* app.use("/ai", throttleMiddleware({ governor: gov }));
|
|
70
|
+
*
|
|
71
|
+
* app.post("/ai/chat", (req, res) => {
|
|
72
|
+
* // This only runs if the governor granted a lease
|
|
73
|
+
* res.json({ message: "ok" });
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
declare function throttleMiddleware(options: ThrottleMiddlewareOptions): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void;
|
|
78
|
+
|
|
79
|
+
export { AdapterGovernor, type ExpressLikeRequest, type ExpressLikeResponse, type ThrottleMiddlewareOptions, throttleMiddleware };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/adapters/express.ts
|
|
2
|
+
function throttleMiddleware(options) {
|
|
3
|
+
const {
|
|
4
|
+
governor,
|
|
5
|
+
getActorId,
|
|
6
|
+
getAction,
|
|
7
|
+
getPriority,
|
|
8
|
+
getEstimate,
|
|
9
|
+
onDeny
|
|
10
|
+
} = options;
|
|
11
|
+
return (req, res, next) => {
|
|
12
|
+
const actorId = getActorId ? getActorId(req) : asString(req.headers["x-actor-id"]) ?? req.ip ?? "anonymous";
|
|
13
|
+
const request = {
|
|
14
|
+
actorId,
|
|
15
|
+
action: getAction ? getAction(req) : req.path,
|
|
16
|
+
priority: getPriority ? getPriority(req) : "interactive",
|
|
17
|
+
estimate: getEstimate ? getEstimate(req) : void 0
|
|
18
|
+
};
|
|
19
|
+
const decision = governor.acquire(request);
|
|
20
|
+
if (!decision.granted) {
|
|
21
|
+
if (onDeny) {
|
|
22
|
+
onDeny(req, res, decision);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
res.setHeader("Retry-After", String(Math.ceil(decision.retryAfterMs / 1e3)));
|
|
26
|
+
res.status(429).json({
|
|
27
|
+
error: "Too many requests",
|
|
28
|
+
reason: decision.reason,
|
|
29
|
+
retryAfterMs: decision.retryAfterMs,
|
|
30
|
+
recommendation: decision.recommendation
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const leaseId = decision.leaseId;
|
|
35
|
+
res.on("finish", () => {
|
|
36
|
+
governor.release(leaseId, {
|
|
37
|
+
outcome: (res.statusCode ?? 200) < 400 ? "success" : "error"
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
next();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function asString(value) {
|
|
44
|
+
if (Array.isArray(value)) return value[0];
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
throttleMiddleware
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/express.ts"],"sourcesContent":["/**\n * ThrottleAI Express adapter — drop-in middleware.\n *\n * No dependency on Express — this exports a plain function that\n * returns `(req, res, next)`. You already have Express installed.\n *\n * @module throttleai/adapters/express\n */\n\nexport type {\n AdapterGovernor,\n AdapterOptions,\n} from \"./types.js\";\n\nimport type { AcquireRequest, AcquireDecision, Priority, TokenEstimate } from \"../types.js\";\nimport type { AdapterGovernor } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Types — use minimal shapes so we don't need @types/express\n// ---------------------------------------------------------------------------\n\n/** Minimal Express-compatible request shape. */\nexport interface ExpressLikeRequest {\n path: string;\n method: string;\n ip?: string;\n headers: Record<string, string | string[] | undefined>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Minimal Express-compatible response shape. */\nexport interface ExpressLikeResponse {\n status(code: number): this;\n json(body: unknown): void;\n setHeader(name: string, value: string | number): void;\n on(event: string, listener: () => void): void;\n statusCode?: number;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: string]: any;\n}\n\n/** Options for the Express throttle middleware. */\nexport interface ThrottleMiddlewareOptions {\n /** Governor instance. */\n governor: AdapterGovernor;\n /**\n * Derive the actor ID from the request (default: x-actor-id header or req.ip).\n */\n getActorId?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the action from the request (default: req.path).\n */\n getAction?: (req: ExpressLikeRequest) => string;\n /**\n * Derive the priority from the request (default: interactive).\n */\n getPriority?: (req: ExpressLikeRequest) => Priority;\n /**\n * Derive a token estimate from the request (optional).\n */\n getEstimate?: (req: ExpressLikeRequest) => TokenEstimate | undefined;\n /**\n * Custom handler for denied requests (default: 429 JSON response).\n */\n onDeny?: (\n req: ExpressLikeRequest,\n res: ExpressLikeResponse,\n decision: AcquireDecision & { granted: false },\n ) => void;\n}\n\n// ---------------------------------------------------------------------------\n// throttleMiddleware\n// ---------------------------------------------------------------------------\n\n/**\n * Create an Express middleware that throttles requests via the governor.\n *\n * ```ts\n * import express from \"express\";\n * import { createGovernor, presets } from \"throttleai\";\n * import { throttleMiddleware } from \"throttleai/adapters/express\";\n *\n * const gov = createGovernor(presets.balanced());\n * const app = express();\n *\n * app.use(\"/ai\", throttleMiddleware({ governor: gov }));\n *\n * app.post(\"/ai/chat\", (req, res) => {\n * // This only runs if the governor granted a lease\n * res.json({ message: \"ok\" });\n * });\n * ```\n */\nexport function throttleMiddleware(\n options: ThrottleMiddlewareOptions,\n): (req: ExpressLikeRequest, res: ExpressLikeResponse, next: () => void) => void {\n const {\n governor,\n getActorId,\n getAction,\n getPriority,\n getEstimate,\n onDeny,\n } = options;\n\n return (req, res, next) => {\n const actorId = getActorId\n ? getActorId(req)\n : (asString(req.headers[\"x-actor-id\"]) ?? req.ip ?? \"anonymous\");\n\n const request: AcquireRequest = {\n actorId,\n action: getAction ? getAction(req) : req.path,\n priority: getPriority ? getPriority(req) : \"interactive\",\n estimate: getEstimate ? getEstimate(req) : undefined,\n };\n\n const decision = governor.acquire(request);\n\n if (!decision.granted) {\n if (onDeny) {\n onDeny(req, res, decision as AcquireDecision & { granted: false });\n return;\n }\n\n // Default: 429 JSON response\n res.setHeader(\"Retry-After\", String(Math.ceil(decision.retryAfterMs / 1000)));\n res.status(429).json({\n error: \"Too many requests\",\n reason: decision.reason,\n retryAfterMs: decision.retryAfterMs,\n recommendation: decision.recommendation,\n });\n return;\n }\n\n // Release lease when response finishes\n const leaseId = decision.leaseId;\n res.on(\"finish\", () => {\n governor.release(leaseId, {\n outcome: (res.statusCode ?? 200) < 400 ? \"success\" : \"error\",\n });\n });\n\n next();\n };\n}\n\n/** Safely convert a header value to string. */\nfunction asString(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0];\n return value;\n}\n"],"mappings":";AA+FO,SAAS,mBACd,SAC+E;AAC/E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,SAAO,CAAC,KAAK,KAAK,SAAS;AACzB,UAAM,UAAU,aACZ,WAAW,GAAG,IACb,SAAS,IAAI,QAAQ,YAAY,CAAC,KAAK,IAAI,MAAM;AAEtD,UAAM,UAA0B;AAAA,MAC9B;AAAA,MACA,QAAQ,YAAY,UAAU,GAAG,IAAI,IAAI;AAAA,MACzC,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,MAC3C,UAAU,cAAc,YAAY,GAAG,IAAI;AAAA,IAC7C;AAEA,UAAM,WAAW,SAAS,QAAQ,OAAO;AAEzC,QAAI,CAAC,SAAS,SAAS;AACrB,UAAI,QAAQ;AACV,eAAO,KAAK,KAAK,QAAgD;AACjE;AAAA,MACF;AAGA,UAAI,UAAU,eAAe,OAAO,KAAK,KAAK,SAAS,eAAe,GAAI,CAAC,CAAC;AAC5E,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,QAAQ,SAAS;AAAA,QACjB,cAAc,SAAS;AAAA,QACvB,gBAAgB,SAAS;AAAA,MAC3B,CAAC;AACD;AAAA,IACF;AAGA,UAAM,UAAU,SAAS;AACzB,QAAI,GAAG,UAAU,MAAM;AACrB,eAAS,QAAQ,SAAS;AAAA,QACxB,UAAU,IAAI,cAAc,OAAO,MAAM,YAAY;AAAA,MACvD,CAAC;AAAA,IACH,CAAC;AAED,SAAK;AAAA,EACP;AACF;AAGA,SAAS,SAAS,OAA0D;AAC1E,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,CAAC;AACxC,SAAO;AACT;","names":[]}
|