@cedvict/http-guardian-plugin-rate-limit 0.0.1-next.10 → 0.0.1-next.11
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/README.md +49 -1
- package/dist/index.cjs +17 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -1,9 +1,57 @@
|
|
|
1
1
|
# @cedvict/http-guardian-plugin-rate-limit
|
|
2
2
|
|
|
3
|
-
Rate
|
|
3
|
+
Rate-limit plugin for http-guardian — combines a per-host **concurrency
|
|
4
|
+
semaphore** and an optional **token bucket** (req/s). Honors upstream
|
|
5
|
+
`Retry-After` on 429.
|
|
4
6
|
|
|
5
7
|
## Usage
|
|
6
8
|
|
|
7
9
|
```ts
|
|
8
10
|
import { rateLimitPlugin } from "@cedvict/http-guardian-plugin-rate-limit";
|
|
11
|
+
import { createHttpClient, createApiParserStructured, bearerAuth } from "@cedvict/http-guardian";
|
|
12
|
+
|
|
13
|
+
const api = createHttpClient({
|
|
14
|
+
baseUrl: "https://api.example.com",
|
|
15
|
+
auth: bearerAuth(() => token),
|
|
16
|
+
parser: createApiParserStructured(),
|
|
17
|
+
plugins: [
|
|
18
|
+
rateLimitPlugin({
|
|
19
|
+
maxConcurrent: 6, // 6 in-flight per host max
|
|
20
|
+
tokensPerInterval: 30, // 30 req per interval
|
|
21
|
+
intervalMs: 1000, // refill window
|
|
22
|
+
honorRetryAfter: true, // pause bucket on 429 Retry-After
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
9
26
|
```
|
|
27
|
+
|
|
28
|
+
## Surfacing 429 to the UI — `onRateLimited`
|
|
29
|
+
|
|
30
|
+
Set `onRateLimited` to be notified every time the upstream returns 429. The
|
|
31
|
+
event carries the parsed `Retry-After` (in ms), the raw headers and the host
|
|
32
|
+
key so you can show a precise toast ("Trop de requêtes, réessayez dans Ns") or
|
|
33
|
+
record a metric without re-parsing at every call-site:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { rateLimitPlugin } from "@cedvict/http-guardian-plugin-rate-limit";
|
|
37
|
+
|
|
38
|
+
rateLimitPlugin({
|
|
39
|
+
maxConcurrent: 6,
|
|
40
|
+
tokensPerInterval: 30,
|
|
41
|
+
intervalMs: 1000,
|
|
42
|
+
onRateLimited: ({ url, status, retryAfterMs, headers, key, method }) => {
|
|
43
|
+
const seconds = retryAfterMs ? Math.ceil(retryAfterMs / 1000) : null;
|
|
44
|
+
toast.warn(
|
|
45
|
+
seconds
|
|
46
|
+
? `Trop de requêtes (${key}). Réessayez dans ${seconds}s.`
|
|
47
|
+
: `Trop de requêtes (${key}). Réessayez plus tard.`,
|
|
48
|
+
);
|
|
49
|
+
metrics.increment("http.rate_limited", { method, host: key });
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The callback fires regardless of `honorRetryAfter` — that flag only controls
|
|
55
|
+
whether the **bucket** pauses; the callback is informational. Errors thrown
|
|
56
|
+
from the callback are caught and ignored so a buggy hook never breaks the
|
|
57
|
+
request pipeline.
|
package/dist/index.cjs
CHANGED
|
@@ -105,9 +105,24 @@ function rateLimitPlugin(opts = {}) {
|
|
|
105
105
|
const release = await sem.acquire();
|
|
106
106
|
try {
|
|
107
107
|
const res = await next(ctx);
|
|
108
|
-
if (
|
|
108
|
+
if (res.status === 429) {
|
|
109
109
|
const ra = parseRetryAfterMs(res.headers, Date.now());
|
|
110
|
-
if (typeof ra === "number")
|
|
110
|
+
if (honorRetryAfter && bucket && typeof ra === "number") {
|
|
111
|
+
bucket.setCooldown(Date.now() + ra);
|
|
112
|
+
}
|
|
113
|
+
if (opts.onRateLimited) {
|
|
114
|
+
try {
|
|
115
|
+
await opts.onRateLimited({
|
|
116
|
+
url: res.url || ctx.url,
|
|
117
|
+
status: res.status,
|
|
118
|
+
headers: res.headers,
|
|
119
|
+
...typeof ra === "number" ? { retryAfterMs: ra } : {},
|
|
120
|
+
key: k,
|
|
121
|
+
method: ctx.method
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
111
126
|
}
|
|
112
127
|
return res;
|
|
113
128
|
} finally {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { HttpPlugin } from \"@cedvict/http-guardian\";\nimport type { Guard } from \"@cedvict/http-guardian\";\nimport type { RequestContext } from \"@cedvict/http-guardian\";\n\ntype KeyFn = (ctx: RequestContext) => string;\n\nexport type RateLimitPluginOptions = {\n maxConcurrent?: number;\n tokensPerInterval?: number;\n intervalMs?: number;\n honorRetryAfter?: boolean;\n key?: KeyFn;\n};\n\nfunction parseRetryAfterMs(h: Headers, nowMs: number): number | undefined {\n const v = h.get(\"retry-after\");\n if (!v) return undefined;\n const asInt = Number(v);\n if (Number.isFinite(asInt)) return Math.max(0, asInt * 1000);\n const asDate = Date.parse(v);\n if (!Number.isNaN(asDate)) return Math.max(0, asDate - nowMs);\n return undefined;\n}\n\nclass Semaphore {\n private max: number;\n private cur = 0;\n private q: Array<() => void> = [];\n constructor(max: number) { this.max = Math.max(1, max); }\n async acquire(): Promise<() => void> {\n if (this.cur < this.max) {\n this.cur++;\n return () => this.release();\n }\n await new Promise<void>((r) => this.q.push(r));\n this.cur++;\n return () => this.release();\n }\n private release() {\n this.cur--;\n const next = this.q.shift();\n if (next) next();\n }\n}\n\nclass TokenBucket {\n private tokens: number;\n private readonly cap: number;\n private readonly refill: number;\n private readonly intervalMs: number;\n private lastRefill: number = Date.now();\n private cooldownUntil = 0;\n\n constructor(tokensPerInterval: number, intervalMs: number) {\n this.cap = Math.max(1, tokensPerInterval);\n this.tokens = this.cap;\n this.refill = this.cap;\n this.intervalMs = Math.max(1, intervalMs);\n }\n\n setCooldown(untilMs: number) { this.cooldownUntil = Math.max(this.cooldownUntil, untilMs); }\n\n private refillIfNeeded(now: number) {\n if (now - this.lastRefill >= this.intervalMs) {\n const steps = Math.floor((now - this.lastRefill) / this.intervalMs);\n this.tokens = Math.min(this.cap, this.tokens + steps * this.refill);\n this.lastRefill += steps * this.intervalMs;\n }\n }\n\n async take(): Promise<void> {\n while (true) {\n const now = Date.now();\n this.refillIfNeeded(now);\n if (now < this.cooldownUntil) {\n await new Promise((r) => setTimeout(r, this.cooldownUntil - now));\n continue;\n }\n if (this.tokens > 0) {\n this.tokens--;\n return;\n }\n const waitMs = Math.max(1, this.intervalMs - (now - this.lastRefill));\n await new Promise((r) => setTimeout(r, waitMs));\n }\n }\n}\n\nexport function rateLimitPlugin(opts: RateLimitPluginOptions = {}): HttpPlugin {\n const maxConcurrent = opts.maxConcurrent ?? 8;\n const tokensPerInterval = opts.tokensPerInterval;\n const intervalMs = opts.intervalMs ?? 1000;\n const honorRetryAfter = opts.honorRetryAfter ?? true;\n const keyFn: KeyFn = opts.key ?? ((ctx) => new URL(ctx.url).host);\n\n const semaphores = new Map<string, Semaphore>();\n const buckets = new Map<string, TokenBucket>();\n\n const getSemaphore = (k: string) => {\n let s = semaphores.get(k);\n if (!s) { s = new Semaphore(maxConcurrent); semaphores.set(k, s); }\n return s;\n };\n const getBucket = (k: string) => {\n if (!tokensPerInterval) return undefined;\n let b = buckets.get(k);\n if (!b) { b = new TokenBucket(tokensPerInterval, intervalMs); buckets.set(k, b); }\n return b;\n };\n\n const guard: Guard = async (ctx, next) => {\n const k = keyFn(ctx);\n const sem = getSemaphore(k);\n const bucket = getBucket(k);\n\n if (bucket) await bucket.take();\n const release = await sem.acquire();\n try {\n const res = await next(ctx);\n if (honorRetryAfter && bucket && res.status === 429) {\n const ra = parseRetryAfterMs(res.headers, Date.now());\n if (typeof ra === \"number\") bucket.setCooldown(Date.now() + ra);\n }\n return res;\n } finally {\n release();\n }\n };\n\n return {\n name: \"rate-limit\",\n apply: (draft) => {\n draft.guards = [...(draft.guards ?? []), guard];\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { HttpPlugin } from \"@cedvict/http-guardian\";\nimport type { Guard } from \"@cedvict/http-guardian\";\nimport type { RequestContext } from \"@cedvict/http-guardian\";\n\ntype KeyFn = (ctx: RequestContext) => string;\n\nexport type RateLimitedEvent = {\n /** Final URL of the rate-limited request. */\n url: string;\n /** Response status (always 429 when this fires). */\n status: number;\n /** Response headers (lets the consumer read Retry-After, X-RateLimit-*, etc.). */\n headers: Headers;\n /**\n * Parsed Retry-After value in milliseconds (from the `Retry-After` header).\n * `undefined` if the header is absent or malformed.\n */\n retryAfterMs?: number;\n /**\n * Rate-limit key the bucket is keyed by (default: request host). Useful when\n * the consumer wants per-key telemetry or selective UI feedback.\n */\n key: string;\n /** HTTP method of the original request. */\n method: string;\n};\n\nexport type RateLimitPluginOptions = {\n maxConcurrent?: number;\n tokensPerInterval?: number;\n intervalMs?: number;\n honorRetryAfter?: boolean;\n key?: KeyFn;\n /**\n * Called every time the upstream returns 429. Fires regardless of\n * `honorRetryAfter` so consumers can always surface toast UX (e.g. \"Trop de\n * requêtes, réessayez dans Ns\") from a single hook.\n *\n * Errors thrown from this callback are caught and ignored to avoid breaking\n * the request pipeline.\n */\n onRateLimited?: (event: RateLimitedEvent) => void | Promise<void>;\n};\n\nfunction parseRetryAfterMs(h: Headers, nowMs: number): number | undefined {\n const v = h.get(\"retry-after\");\n if (!v) return undefined;\n const asInt = Number(v);\n if (Number.isFinite(asInt)) return Math.max(0, asInt * 1000);\n const asDate = Date.parse(v);\n if (!Number.isNaN(asDate)) return Math.max(0, asDate - nowMs);\n return undefined;\n}\n\nclass Semaphore {\n private max: number;\n private cur = 0;\n private q: Array<() => void> = [];\n constructor(max: number) { this.max = Math.max(1, max); }\n async acquire(): Promise<() => void> {\n if (this.cur < this.max) {\n this.cur++;\n return () => this.release();\n }\n await new Promise<void>((r) => this.q.push(r));\n this.cur++;\n return () => this.release();\n }\n private release() {\n this.cur--;\n const next = this.q.shift();\n if (next) next();\n }\n}\n\nclass TokenBucket {\n private tokens: number;\n private readonly cap: number;\n private readonly refill: number;\n private readonly intervalMs: number;\n private lastRefill: number = Date.now();\n private cooldownUntil = 0;\n\n constructor(tokensPerInterval: number, intervalMs: number) {\n this.cap = Math.max(1, tokensPerInterval);\n this.tokens = this.cap;\n this.refill = this.cap;\n this.intervalMs = Math.max(1, intervalMs);\n }\n\n setCooldown(untilMs: number) { this.cooldownUntil = Math.max(this.cooldownUntil, untilMs); }\n\n private refillIfNeeded(now: number) {\n if (now - this.lastRefill >= this.intervalMs) {\n const steps = Math.floor((now - this.lastRefill) / this.intervalMs);\n this.tokens = Math.min(this.cap, this.tokens + steps * this.refill);\n this.lastRefill += steps * this.intervalMs;\n }\n }\n\n async take(): Promise<void> {\n while (true) {\n const now = Date.now();\n this.refillIfNeeded(now);\n if (now < this.cooldownUntil) {\n await new Promise((r) => setTimeout(r, this.cooldownUntil - now));\n continue;\n }\n if (this.tokens > 0) {\n this.tokens--;\n return;\n }\n const waitMs = Math.max(1, this.intervalMs - (now - this.lastRefill));\n await new Promise((r) => setTimeout(r, waitMs));\n }\n }\n}\n\nexport function rateLimitPlugin(opts: RateLimitPluginOptions = {}): HttpPlugin {\n const maxConcurrent = opts.maxConcurrent ?? 8;\n const tokensPerInterval = opts.tokensPerInterval;\n const intervalMs = opts.intervalMs ?? 1000;\n const honorRetryAfter = opts.honorRetryAfter ?? true;\n const keyFn: KeyFn = opts.key ?? ((ctx) => new URL(ctx.url).host);\n\n const semaphores = new Map<string, Semaphore>();\n const buckets = new Map<string, TokenBucket>();\n\n const getSemaphore = (k: string) => {\n let s = semaphores.get(k);\n if (!s) { s = new Semaphore(maxConcurrent); semaphores.set(k, s); }\n return s;\n };\n const getBucket = (k: string) => {\n if (!tokensPerInterval) return undefined;\n let b = buckets.get(k);\n if (!b) { b = new TokenBucket(tokensPerInterval, intervalMs); buckets.set(k, b); }\n return b;\n };\n\n const guard: Guard = async (ctx, next) => {\n const k = keyFn(ctx);\n const sem = getSemaphore(k);\n const bucket = getBucket(k);\n\n if (bucket) await bucket.take();\n const release = await sem.acquire();\n try {\n const res = await next(ctx);\n if (res.status === 429) {\n const ra = parseRetryAfterMs(res.headers, Date.now());\n if (honorRetryAfter && bucket && typeof ra === \"number\") {\n bucket.setCooldown(Date.now() + ra);\n }\n if (opts.onRateLimited) {\n try {\n await opts.onRateLimited({\n url: res.url || ctx.url,\n status: res.status,\n headers: res.headers,\n ...(typeof ra === \"number\" ? { retryAfterMs: ra } : {}),\n key: k,\n method: ctx.method,\n });\n } catch {\n /* never break the pipeline because of a user callback */\n }\n }\n }\n return res;\n } finally {\n release();\n }\n };\n\n return {\n name: \"rate-limit\",\n apply: (draft) => {\n draft.guards = [...(draft.guards ?? []), guard];\n },\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
1
|
import { RequestContext, HttpPlugin } from '@cedvict/http-guardian';
|
|
2
2
|
|
|
3
3
|
type KeyFn = (ctx: RequestContext) => string;
|
|
4
|
+
type RateLimitedEvent = {
|
|
5
|
+
/** Final URL of the rate-limited request. */
|
|
6
|
+
url: string;
|
|
7
|
+
/** Response status (always 429 when this fires). */
|
|
8
|
+
status: number;
|
|
9
|
+
/** Response headers (lets the consumer read Retry-After, X-RateLimit-*, etc.). */
|
|
10
|
+
headers: Headers;
|
|
11
|
+
/**
|
|
12
|
+
* Parsed Retry-After value in milliseconds (from the `Retry-After` header).
|
|
13
|
+
* `undefined` if the header is absent or malformed.
|
|
14
|
+
*/
|
|
15
|
+
retryAfterMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Rate-limit key the bucket is keyed by (default: request host). Useful when
|
|
18
|
+
* the consumer wants per-key telemetry or selective UI feedback.
|
|
19
|
+
*/
|
|
20
|
+
key: string;
|
|
21
|
+
/** HTTP method of the original request. */
|
|
22
|
+
method: string;
|
|
23
|
+
};
|
|
4
24
|
type RateLimitPluginOptions = {
|
|
5
25
|
maxConcurrent?: number;
|
|
6
26
|
tokensPerInterval?: number;
|
|
7
27
|
intervalMs?: number;
|
|
8
28
|
honorRetryAfter?: boolean;
|
|
9
29
|
key?: KeyFn;
|
|
30
|
+
/**
|
|
31
|
+
* Called every time the upstream returns 429. Fires regardless of
|
|
32
|
+
* `honorRetryAfter` so consumers can always surface toast UX (e.g. "Trop de
|
|
33
|
+
* requêtes, réessayez dans Ns") from a single hook.
|
|
34
|
+
*
|
|
35
|
+
* Errors thrown from this callback are caught and ignored to avoid breaking
|
|
36
|
+
* the request pipeline.
|
|
37
|
+
*/
|
|
38
|
+
onRateLimited?: (event: RateLimitedEvent) => void | Promise<void>;
|
|
10
39
|
};
|
|
11
40
|
declare function rateLimitPlugin(opts?: RateLimitPluginOptions): HttpPlugin;
|
|
12
41
|
|
|
13
|
-
export { type RateLimitPluginOptions, rateLimitPlugin };
|
|
42
|
+
export { type RateLimitPluginOptions, type RateLimitedEvent, rateLimitPlugin };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
1
|
import { RequestContext, HttpPlugin } from '@cedvict/http-guardian';
|
|
2
2
|
|
|
3
3
|
type KeyFn = (ctx: RequestContext) => string;
|
|
4
|
+
type RateLimitedEvent = {
|
|
5
|
+
/** Final URL of the rate-limited request. */
|
|
6
|
+
url: string;
|
|
7
|
+
/** Response status (always 429 when this fires). */
|
|
8
|
+
status: number;
|
|
9
|
+
/** Response headers (lets the consumer read Retry-After, X-RateLimit-*, etc.). */
|
|
10
|
+
headers: Headers;
|
|
11
|
+
/**
|
|
12
|
+
* Parsed Retry-After value in milliseconds (from the `Retry-After` header).
|
|
13
|
+
* `undefined` if the header is absent or malformed.
|
|
14
|
+
*/
|
|
15
|
+
retryAfterMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Rate-limit key the bucket is keyed by (default: request host). Useful when
|
|
18
|
+
* the consumer wants per-key telemetry or selective UI feedback.
|
|
19
|
+
*/
|
|
20
|
+
key: string;
|
|
21
|
+
/** HTTP method of the original request. */
|
|
22
|
+
method: string;
|
|
23
|
+
};
|
|
4
24
|
type RateLimitPluginOptions = {
|
|
5
25
|
maxConcurrent?: number;
|
|
6
26
|
tokensPerInterval?: number;
|
|
7
27
|
intervalMs?: number;
|
|
8
28
|
honorRetryAfter?: boolean;
|
|
9
29
|
key?: KeyFn;
|
|
30
|
+
/**
|
|
31
|
+
* Called every time the upstream returns 429. Fires regardless of
|
|
32
|
+
* `honorRetryAfter` so consumers can always surface toast UX (e.g. "Trop de
|
|
33
|
+
* requêtes, réessayez dans Ns") from a single hook.
|
|
34
|
+
*
|
|
35
|
+
* Errors thrown from this callback are caught and ignored to avoid breaking
|
|
36
|
+
* the request pipeline.
|
|
37
|
+
*/
|
|
38
|
+
onRateLimited?: (event: RateLimitedEvent) => void | Promise<void>;
|
|
10
39
|
};
|
|
11
40
|
declare function rateLimitPlugin(opts?: RateLimitPluginOptions): HttpPlugin;
|
|
12
41
|
|
|
13
|
-
export { type RateLimitPluginOptions, rateLimitPlugin };
|
|
42
|
+
export { type RateLimitPluginOptions, type RateLimitedEvent, rateLimitPlugin };
|
package/dist/index.js
CHANGED
|
@@ -103,9 +103,24 @@ function rateLimitPlugin(opts = {}) {
|
|
|
103
103
|
const release = await sem.acquire();
|
|
104
104
|
try {
|
|
105
105
|
const res = await next(ctx);
|
|
106
|
-
if (
|
|
106
|
+
if (res.status === 429) {
|
|
107
107
|
const ra = parseRetryAfterMs(res.headers, Date.now());
|
|
108
|
-
if (typeof ra === "number")
|
|
108
|
+
if (honorRetryAfter && bucket && typeof ra === "number") {
|
|
109
|
+
bucket.setCooldown(Date.now() + ra);
|
|
110
|
+
}
|
|
111
|
+
if (opts.onRateLimited) {
|
|
112
|
+
try {
|
|
113
|
+
await opts.onRateLimited({
|
|
114
|
+
url: res.url || ctx.url,
|
|
115
|
+
status: res.status,
|
|
116
|
+
headers: res.headers,
|
|
117
|
+
...typeof ra === "number" ? { retryAfterMs: ra } : {},
|
|
118
|
+
key: k,
|
|
119
|
+
method: ctx.method
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
}
|
|
110
125
|
return res;
|
|
111
126
|
} finally {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAcA,SAAS,iBAAA,CAAkB,GAAY,KAAA,EAAmC;AACxE,EAAA,MAAM,CAAA,GAAI,CAAA,CAAE,GAAA,CAAI,aAAa,CAAA;AAC7B,EAAA,IAAI,CAAC,GAAG,OAAO,MAAA;AACf,EAAA,MAAM,KAAA,GAAQ,OAAO,CAAC,CAAA;AACtB,EAAA,IAAI,MAAA,CAAO,SAAS,KAAK,CAAA,SAAU,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,GAAQ,GAAI,CAAA;AAC3D,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AAC3B,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA,SAAU,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,MAAA,GAAS,KAAK,CAAA;AAC5D,EAAA,OAAO,MAAA;AACT;AAEA,IAAM,YAAN,MAAgB;AAAA,EACN,GAAA;AAAA,EACA,GAAA,GAAM,CAAA;AAAA,EACN,IAAuB,EAAC;AAAA,EAChC,YAAY,GAAA,EAAa;AAAE,IAAA,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAA;AAAA,EAAG;AAAA,EACxD,MAAM,OAAA,GAA+B;AACnC,IAAA,IAAI,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,GAAA,EAAK;AACvB,MAAA,IAAA,CAAK,GAAA,EAAA;AACL,MAAA,OAAO,MAAM,KAAK,OAAA,EAAQ;AAAA,IAC5B;AACA,IAAA,MAAM,IAAI,QAAc,CAAC,CAAA,KAAM,KAAK,CAAA,CAAE,IAAA,CAAK,CAAC,CAAC,CAAA;AAC7C,IAAA,IAAA,CAAK,GAAA,EAAA;AACL,IAAA,OAAO,MAAM,KAAK,OAAA,EAAQ;AAAA,EAC5B;AAAA,EACQ,OAAA,GAAU;AAChB,IAAA,IAAA,CAAK,GAAA,EAAA;AACL,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,CAAA,CAAE,KAAA,EAAM;AAC1B,IAAA,IAAI,MAAM,IAAA,EAAK;AAAA,EACjB;AACF,CAAA;AAEA,IAAM,cAAN,MAAkB;AAAA,EACR,MAAA;AAAA,EACS,GAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACT,UAAA,GAAqB,KAAK,GAAA,EAAI;AAAA,EAC9B,aAAA,GAAgB,CAAA;AAAA,EAExB,WAAA,CAAY,mBAA2B,UAAA,EAAoB;AACzD,IAAA,IAAA,CAAK,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,iBAAiB,CAAA;AACxC,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,GAAA;AACnB,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,GAAA;AACnB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,UAAU,CAAA;AAAA,EAC1C;AAAA,EAEA,YAAY,OAAA,EAAiB;AAAE,IAAA,IAAA,CAAK,aAAA,GAAgB,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,eAAe,OAAO,CAAA;AAAA,EAAG;AAAA,EAEnF,eAAe,GAAA,EAAa;AAClC,IAAA,IAAI,GAAA,GAAM,IAAA,CAAK,UAAA,IAAc,IAAA,CAAK,UAAA,EAAY;AAC5C,MAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,CAAA,CAAO,MAAM,IAAA,CAAK,UAAA,IAAc,KAAK,UAAU,CAAA;AAClE,MAAA,IAAA,CAAK,MAAA,GAAS,KAAK,GAAA,CAAI,IAAA,CAAK,KAAK,IAAA,CAAK,MAAA,GAAS,KAAA,GAAQ,IAAA,CAAK,MAAM,CAAA;AAClE,MAAA,IAAA,CAAK,UAAA,IAAc,QAAQ,IAAA,CAAK,UAAA;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,OAAO,IAAA,EAAM;AACX,MAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,MAAA,IAAA,CAAK,eAAe,GAAG,CAAA;AACvB,MAAA,IAAI,GAAA,GAAM,KAAK,aAAA,EAAe;AAC5B,QAAA,MAAM,IAAI,QAAQ,CAAC,CAAA,KAAM,WAAW,CAAA,EAAG,IAAA,CAAK,aAAA,GAAgB,GAAG,CAAC,CAAA;AAChE,QAAA;AAAA,MACF;AACA,MAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,QAAA,IAAA,CAAK,MAAA,EAAA;AACL,QAAA;AAAA,MACF;AACA,MAAA,MAAM,MAAA,GAAS,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,UAAA,IAAc,GAAA,GAAM,KAAK,UAAA,CAAW,CAAA;AACpE,MAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,MAAM,CAAC,CAAA;AAAA,IAChD;AAAA,EACF;AACF,CAAA;AAEO,SAAS,eAAA,CAAgB,IAAA,GAA+B,EAAC,EAAe;AAC7E,EAAA,MAAM,aAAA,GAAgB,KAAK,aAAA,IAAiB,CAAA;AAC5C,EAAA,MAAM,oBAAoB,IAAA,CAAK,iBAAA;AAC/B,EAAA,MAAM,UAAA,GAAa,KAAK,UAAA,IAAc,GAAA;AACtC,EAAA,MAAM,eAAA,GAAkB,KAAK,eAAA,IAAmB,IAAA;AAChD,EAAA,MAAM,KAAA,GAAe,KAAK,GAAA,KAAQ,CAAC,QAAQ,IAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,CAAE,IAAA,CAAA;AAE5D,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAuB;AAC9C,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAyB;AAE7C,EAAA,MAAM,YAAA,GAAe,CAAC,CAAA,KAAc;AAClC,IAAA,IAAI,CAAA,GAAI,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA;AACxB,IAAA,IAAI,CAAC,CAAA,EAAG;AAAE,MAAA,CAAA,GAAI,IAAI,UAAU,aAAa,CAAA;AAAG,MAAA,UAAA,CAAW,GAAA,CAAI,GAAG,CAAC,CAAA;AAAA,IAAG;AAClE,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AACA,EAAA,MAAM,SAAA,GAAY,CAAC,CAAA,KAAc;AAC/B,IAAA,IAAI,CAAC,mBAAmB,OAAO,MAAA;AAC/B,IAAA,IAAI,CAAA,GAAI,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA;AACrB,IAAA,IAAI,CAAC,CAAA,EAAG;AAAE,MAAA,CAAA,GAAI,IAAI,WAAA,CAAY,iBAAA,EAAmB,UAAU,CAAA;AAAG,MAAA,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAC,CAAA;AAAA,IAAG;AACjF,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,KAAA,GAAe,OAAO,GAAA,EAAK,IAAA,KAAS;AACxC,IAAA,MAAM,CAAA,GAAI,MAAM,GAAG,CAAA;AACnB,IAAA,MAAM,GAAA,GAAM,aAAa,CAAC,CAAA;AAC1B,IAAA,MAAM,MAAA,GAAS,UAAU,CAAC,CAAA;AAE1B,IAAA,IAAI,MAAA,EAAQ,MAAM,MAAA,CAAO,IAAA,EAAK;AAC9B,IAAA,MAAM,OAAA,GAAU,MAAM,GAAA,CAAI,OAAA,EAAQ;AAClC,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,GAAG,CAAA;AAC1B,MAAA,IAAI,eAAA,IAAmB,MAAA,IAAU,GAAA,CAAI,MAAA,KAAW,GAAA,EAAK;AACnD,QAAA,MAAM,KAAK,iBAAA,CAAkB,GAAA,CAAI,OAAA,EAAS,IAAA,CAAK,KAAK,CAAA;AACpD,QAAA,IAAI,OAAO,OAAO,QAAA,EAAU,MAAA,CAAO,YAAY,IAAA,CAAK,GAAA,KAAQ,EAAE,CAAA;AAAA,MAChE;AACA,MAAA,OAAO,GAAA;AAAA,IACT,CAAA,SAAE;AACA,MAAA,OAAA,EAAQ;AAAA,IACV;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,YAAA;AAAA,IACN,KAAA,EAAO,CAAC,KAAA,KAAU;AAChB,MAAA,KAAA,CAAM,SAAS,CAAC,GAAI,MAAM,MAAA,IAAU,IAAK,KAAK,CAAA;AAAA,IAChD;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import type { HttpPlugin } from \"@cedvict/http-guardian\";\nimport type { Guard } from \"@cedvict/http-guardian\";\nimport type { RequestContext } from \"@cedvict/http-guardian\";\n\ntype KeyFn = (ctx: RequestContext) => string;\n\nexport type RateLimitPluginOptions = {\n maxConcurrent?: number;\n tokensPerInterval?: number;\n intervalMs?: number;\n honorRetryAfter?: boolean;\n key?: KeyFn;\n};\n\nfunction parseRetryAfterMs(h: Headers, nowMs: number): number | undefined {\n const v = h.get(\"retry-after\");\n if (!v) return undefined;\n const asInt = Number(v);\n if (Number.isFinite(asInt)) return Math.max(0, asInt * 1000);\n const asDate = Date.parse(v);\n if (!Number.isNaN(asDate)) return Math.max(0, asDate - nowMs);\n return undefined;\n}\n\nclass Semaphore {\n private max: number;\n private cur = 0;\n private q: Array<() => void> = [];\n constructor(max: number) { this.max = Math.max(1, max); }\n async acquire(): Promise<() => void> {\n if (this.cur < this.max) {\n this.cur++;\n return () => this.release();\n }\n await new Promise<void>((r) => this.q.push(r));\n this.cur++;\n return () => this.release();\n }\n private release() {\n this.cur--;\n const next = this.q.shift();\n if (next) next();\n }\n}\n\nclass TokenBucket {\n private tokens: number;\n private readonly cap: number;\n private readonly refill: number;\n private readonly intervalMs: number;\n private lastRefill: number = Date.now();\n private cooldownUntil = 0;\n\n constructor(tokensPerInterval: number, intervalMs: number) {\n this.cap = Math.max(1, tokensPerInterval);\n this.tokens = this.cap;\n this.refill = this.cap;\n this.intervalMs = Math.max(1, intervalMs);\n }\n\n setCooldown(untilMs: number) { this.cooldownUntil = Math.max(this.cooldownUntil, untilMs); }\n\n private refillIfNeeded(now: number) {\n if (now - this.lastRefill >= this.intervalMs) {\n const steps = Math.floor((now - this.lastRefill) / this.intervalMs);\n this.tokens = Math.min(this.cap, this.tokens + steps * this.refill);\n this.lastRefill += steps * this.intervalMs;\n }\n }\n\n async take(): Promise<void> {\n while (true) {\n const now = Date.now();\n this.refillIfNeeded(now);\n if (now < this.cooldownUntil) {\n await new Promise((r) => setTimeout(r, this.cooldownUntil - now));\n continue;\n }\n if (this.tokens > 0) {\n this.tokens--;\n return;\n }\n const waitMs = Math.max(1, this.intervalMs - (now - this.lastRefill));\n await new Promise((r) => setTimeout(r, waitMs));\n }\n }\n}\n\nexport function rateLimitPlugin(opts: RateLimitPluginOptions = {}): HttpPlugin {\n const maxConcurrent = opts.maxConcurrent ?? 8;\n const tokensPerInterval = opts.tokensPerInterval;\n const intervalMs = opts.intervalMs ?? 1000;\n const honorRetryAfter = opts.honorRetryAfter ?? true;\n const keyFn: KeyFn = opts.key ?? ((ctx) => new URL(ctx.url).host);\n\n const semaphores = new Map<string, Semaphore>();\n const buckets = new Map<string, TokenBucket>();\n\n const getSemaphore = (k: string) => {\n let s = semaphores.get(k);\n if (!s) { s = new Semaphore(maxConcurrent); semaphores.set(k, s); }\n return s;\n };\n const getBucket = (k: string) => {\n if (!tokensPerInterval) return undefined;\n let b = buckets.get(k);\n if (!b) { b = new TokenBucket(tokensPerInterval, intervalMs); buckets.set(k, b); }\n return b;\n };\n\n const guard: Guard = async (ctx, next) => {\n const k = keyFn(ctx);\n const sem = getSemaphore(k);\n const bucket = getBucket(k);\n\n if (bucket) await bucket.take();\n const release = await sem.acquire();\n try {\n const res = await next(ctx);\n if (honorRetryAfter && bucket && res.status === 429) {\n const ra = parseRetryAfterMs(res.headers, Date.now());\n if (typeof ra === \"number\") bucket.setCooldown(Date.now() + ra);\n }\n return res;\n } finally {\n release();\n }\n };\n\n return {\n name: \"rate-limit\",\n apply: (draft) => {\n draft.guards = [...(draft.guards ?? []), guard];\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { HttpPlugin } from \"@cedvict/http-guardian\";\nimport type { Guard } from \"@cedvict/http-guardian\";\nimport type { RequestContext } from \"@cedvict/http-guardian\";\n\ntype KeyFn = (ctx: RequestContext) => string;\n\nexport type RateLimitedEvent = {\n /** Final URL of the rate-limited request. */\n url: string;\n /** Response status (always 429 when this fires). */\n status: number;\n /** Response headers (lets the consumer read Retry-After, X-RateLimit-*, etc.). */\n headers: Headers;\n /**\n * Parsed Retry-After value in milliseconds (from the `Retry-After` header).\n * `undefined` if the header is absent or malformed.\n */\n retryAfterMs?: number;\n /**\n * Rate-limit key the bucket is keyed by (default: request host). Useful when\n * the consumer wants per-key telemetry or selective UI feedback.\n */\n key: string;\n /** HTTP method of the original request. */\n method: string;\n};\n\nexport type RateLimitPluginOptions = {\n maxConcurrent?: number;\n tokensPerInterval?: number;\n intervalMs?: number;\n honorRetryAfter?: boolean;\n key?: KeyFn;\n /**\n * Called every time the upstream returns 429. Fires regardless of\n * `honorRetryAfter` so consumers can always surface toast UX (e.g. \"Trop de\n * requêtes, réessayez dans Ns\") from a single hook.\n *\n * Errors thrown from this callback are caught and ignored to avoid breaking\n * the request pipeline.\n */\n onRateLimited?: (event: RateLimitedEvent) => void | Promise<void>;\n};\n\nfunction parseRetryAfterMs(h: Headers, nowMs: number): number | undefined {\n const v = h.get(\"retry-after\");\n if (!v) return undefined;\n const asInt = Number(v);\n if (Number.isFinite(asInt)) return Math.max(0, asInt * 1000);\n const asDate = Date.parse(v);\n if (!Number.isNaN(asDate)) return Math.max(0, asDate - nowMs);\n return undefined;\n}\n\nclass Semaphore {\n private max: number;\n private cur = 0;\n private q: Array<() => void> = [];\n constructor(max: number) { this.max = Math.max(1, max); }\n async acquire(): Promise<() => void> {\n if (this.cur < this.max) {\n this.cur++;\n return () => this.release();\n }\n await new Promise<void>((r) => this.q.push(r));\n this.cur++;\n return () => this.release();\n }\n private release() {\n this.cur--;\n const next = this.q.shift();\n if (next) next();\n }\n}\n\nclass TokenBucket {\n private tokens: number;\n private readonly cap: number;\n private readonly refill: number;\n private readonly intervalMs: number;\n private lastRefill: number = Date.now();\n private cooldownUntil = 0;\n\n constructor(tokensPerInterval: number, intervalMs: number) {\n this.cap = Math.max(1, tokensPerInterval);\n this.tokens = this.cap;\n this.refill = this.cap;\n this.intervalMs = Math.max(1, intervalMs);\n }\n\n setCooldown(untilMs: number) { this.cooldownUntil = Math.max(this.cooldownUntil, untilMs); }\n\n private refillIfNeeded(now: number) {\n if (now - this.lastRefill >= this.intervalMs) {\n const steps = Math.floor((now - this.lastRefill) / this.intervalMs);\n this.tokens = Math.min(this.cap, this.tokens + steps * this.refill);\n this.lastRefill += steps * this.intervalMs;\n }\n }\n\n async take(): Promise<void> {\n while (true) {\n const now = Date.now();\n this.refillIfNeeded(now);\n if (now < this.cooldownUntil) {\n await new Promise((r) => setTimeout(r, this.cooldownUntil - now));\n continue;\n }\n if (this.tokens > 0) {\n this.tokens--;\n return;\n }\n const waitMs = Math.max(1, this.intervalMs - (now - this.lastRefill));\n await new Promise((r) => setTimeout(r, waitMs));\n }\n }\n}\n\nexport function rateLimitPlugin(opts: RateLimitPluginOptions = {}): HttpPlugin {\n const maxConcurrent = opts.maxConcurrent ?? 8;\n const tokensPerInterval = opts.tokensPerInterval;\n const intervalMs = opts.intervalMs ?? 1000;\n const honorRetryAfter = opts.honorRetryAfter ?? true;\n const keyFn: KeyFn = opts.key ?? ((ctx) => new URL(ctx.url).host);\n\n const semaphores = new Map<string, Semaphore>();\n const buckets = new Map<string, TokenBucket>();\n\n const getSemaphore = (k: string) => {\n let s = semaphores.get(k);\n if (!s) { s = new Semaphore(maxConcurrent); semaphores.set(k, s); }\n return s;\n };\n const getBucket = (k: string) => {\n if (!tokensPerInterval) return undefined;\n let b = buckets.get(k);\n if (!b) { b = new TokenBucket(tokensPerInterval, intervalMs); buckets.set(k, b); }\n return b;\n };\n\n const guard: Guard = async (ctx, next) => {\n const k = keyFn(ctx);\n const sem = getSemaphore(k);\n const bucket = getBucket(k);\n\n if (bucket) await bucket.take();\n const release = await sem.acquire();\n try {\n const res = await next(ctx);\n if (res.status === 429) {\n const ra = parseRetryAfterMs(res.headers, Date.now());\n if (honorRetryAfter && bucket && typeof ra === \"number\") {\n bucket.setCooldown(Date.now() + ra);\n }\n if (opts.onRateLimited) {\n try {\n await opts.onRateLimited({\n url: res.url || ctx.url,\n status: res.status,\n headers: res.headers,\n ...(typeof ra === \"number\" ? { retryAfterMs: ra } : {}),\n key: k,\n method: ctx.method,\n });\n } catch {\n /* never break the pipeline because of a user callback */\n }\n }\n }\n return res;\n } finally {\n release();\n }\n };\n\n return {\n name: \"rate-limit\",\n apply: (draft) => {\n draft.guards = [...(draft.guards ?? []), guard];\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedvict/http-guardian-plugin-rate-limit",
|
|
3
|
-
"version": "0.0.1-next.
|
|
3
|
+
"version": "0.0.1-next.11",
|
|
4
4
|
"description": "Rate limit plugin (concurrency + token bucket) for http-guardian.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "git+https://
|
|
31
|
+
"url": "git+https://gitlab.com/code-libs/npm/http-guardian.git"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@cedvict/http-guardian": "^0.0.1-next.
|
|
34
|
+
"@cedvict/http-guardian": "^0.0.1-next.11"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsup",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"@typescript-eslint/parser": "^8.0.0",
|
|
53
53
|
"@typescript-eslint/eslint-plugin": "^8.0.0"
|
|
54
54
|
},
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
}
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://gitlab.com/code-libs/npm/http-guardian/-/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://gitlab.com/code-libs/npm/http-guardian"
|
|
58
59
|
}
|