@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 CHANGED
@@ -1,9 +1,57 @@
1
1
  # @cedvict/http-guardian-plugin-rate-limit
2
2
 
3
- Rate limit plugin (concurrency + token bucket) for http-guardian.
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 (honorRetryAfter && bucket && res.status === 429) {
108
+ if (res.status === 429) {
109
109
  const ra = parseRetryAfterMs(res.headers, Date.now());
110
- if (typeof ra === "number") bucket.setCooldown(Date.now() + ra);
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 {
@@ -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 (honorRetryAfter && bucket && res.status === 429) {
106
+ if (res.status === 429) {
107
107
  const ra = parseRetryAfterMs(res.headers, Date.now());
108
- if (typeof ra === "number") bucket.setCooldown(Date.now() + ra);
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":"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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 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":";AA4CA,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,GAAA,CAAI,WAAW,GAAA,EAAK;AACtB,QAAA,MAAM,KAAK,iBAAA,CAAkB,GAAA,CAAI,OAAA,EAAS,IAAA,CAAK,KAAK,CAAA;AACpD,QAAA,IAAI,eAAA,IAAmB,MAAA,IAAU,OAAO,EAAA,KAAO,QAAA,EAAU;AACvD,UAAA,MAAA,CAAO,WAAA,CAAY,IAAA,CAAK,GAAA,EAAI,GAAI,EAAE,CAAA;AAAA,QACpC;AACA,QAAA,IAAI,KAAK,aAAA,EAAe;AACtB,UAAA,IAAI;AACF,YAAA,MAAM,KAAK,aAAA,CAAc;AAAA,cACvB,GAAA,EAAK,GAAA,CAAI,GAAA,IAAO,GAAA,CAAI,GAAA;AAAA,cACpB,QAAQ,GAAA,CAAI,MAAA;AAAA,cACZ,SAAS,GAAA,CAAI,OAAA;AAAA,cACb,GAAI,OAAO,EAAA,KAAO,QAAA,GAAW,EAAE,YAAA,EAAc,EAAA,KAAO,EAAC;AAAA,cACrD,GAAA,EAAK,CAAA;AAAA,cACL,QAAQ,GAAA,CAAI;AAAA,aACb,CAAA;AAAA,UACH,CAAA,CAAA,MAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;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 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.10",
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://github.com/cedvict/http-guardian.git"
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.6"
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
- "dependencies": {
56
- "@cedvict/http-guardian": "^0.0.1-next.10"
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
  }