lunibee 0.3.1 → 0.3.3
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/dist/builders/attachment.d.ts +1 -0
- package/dist/builders/clone.d.ts +10 -0
- package/dist/builders/commands.d.ts +1 -1
- package/dist/builders/components.d.ts +24 -5
- package/dist/builders/index.js +64 -17
- package/dist/builders/index.js.map +10 -9
- package/dist/collection/index.d.ts +1 -0
- package/dist/collection/index.js +297 -19
- package/dist/collection/index.js.map +5 -4
- package/dist/collection/redis.d.ts +98 -0
- package/dist/core/collector.d.ts +8 -0
- package/dist/core/events.d.ts +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +6242 -4733
- package/dist/core/index.js.map +47 -40
- package/dist/core/permissions.d.ts +3 -402
- package/dist/core/redis-cache.d.ts +85 -0
- package/dist/formatters/index.d.ts +5 -1
- package/dist/formatters/index.js +7 -3
- package/dist/formatters/index.js.map +3 -3
- package/dist/handlers/index.js +11 -2
- package/dist/handlers/index.js.map +3 -3
- package/dist/index.js +8688 -6136
- package/dist/index.js.map +67 -59
- package/dist/managers/application.d.ts +10 -6
- package/dist/managers/base.d.ts +11 -1
- package/dist/managers/guild.d.ts +3 -1
- package/dist/managers/index.d.ts +6 -1
- package/dist/managers/index.js +383 -109
- package/dist/managers/index.js.map +20 -23
- package/dist/managers/member.d.ts +3 -1
- package/dist/managers/message.d.ts +13 -4
- package/dist/managers/thread.d.ts +3 -3
- package/dist/rest/decoder.d.ts +2 -0
- package/dist/rest/errors.d.ts +5 -2
- package/dist/rest/headers.d.ts +31 -0
- package/dist/rest/identity.d.ts +31 -0
- package/dist/rest/index.d.ts +97 -6
- package/dist/rest/index.js +1355 -220
- package/dist/rest/index.js.map +16 -13
- package/dist/rest/limiter.d.ts +75 -10
- package/dist/rest/redis.d.ts +103 -22
- package/dist/rest/scheduler.d.ts +13 -0
- package/dist/rest/store.d.ts +135 -12
- package/dist/rest/telemetry.d.ts +151 -0
- package/dist/rest/webhook.d.ts +18 -6
- package/dist/sharding/bus.d.ts +4 -2
- package/dist/sharding/cluster.d.ts +29 -1
- package/dist/sharding/health.d.ts +1 -1
- package/dist/sharding/host.d.ts +13 -2
- package/dist/sharding/identify.d.ts +2 -0
- package/dist/sharding/index.d.ts +8 -0
- package/dist/sharding/index.js +579 -65
- package/dist/sharding/index.js.map +17 -15
- package/dist/sharding/leader.d.ts +34 -0
- package/dist/sharding/remote.d.ts +36 -2
- package/dist/sharding/supervisor.d.ts +1 -1
- package/dist/sharding/thread.d.ts +30 -0
- package/dist/sharding/transport.d.ts +6 -5
- package/dist/sharding/worker.d.ts +11 -1
- package/dist/structures/base.d.ts +16 -5
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +57 -91
- package/dist/structures/index.js.map +10 -18
- package/dist/structures/options.d.ts +3 -2
- package/dist/structures/permissions.d.ts +409 -0
- package/dist/structures/resources.d.ts +4 -1
- package/dist/types/gateway.d.ts +4 -0
- package/dist/types/index.d.ts +14 -4
- package/dist/types/index.js.map +3 -3
- package/dist/utils/args.d.ts +3 -1
- package/dist/utils/index.d.ts +6 -1
- package/dist/utils/index.js +17 -10
- package/dist/utils/index.js.map +4 -4
- package/dist/voice/index.d.ts +7 -1
- package/dist/voice/index.js +33 -10
- package/dist/voice/index.js.map +3 -3
- package/dist/ws/index.d.ts +2 -0
- package/dist/ws/index.js +32 -4
- package/dist/ws/index.js.map +5 -5
- package/package.json +21 -17
package/dist/rest/limiter.d.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RateLimitStore } from "./store.js";
|
|
2
|
+
import { type RESTTelemetry } from "./telemetry.js";
|
|
3
|
+
/** Default for `maxBackoff`: ten minutes. */
|
|
4
|
+
export declare const DEFAULT_MAX_BACKOFF_MS = 600000;
|
|
5
|
+
/** What {@link RateLimiter.acquire} obtained. */
|
|
6
|
+
export interface Grant {
|
|
7
|
+
/** A unit was reserved atomically (false for a store without `reserve`). */
|
|
8
|
+
reserved: boolean;
|
|
9
|
+
/** The bucket's limit is known, so further requests need not wait for this one's response. */
|
|
10
|
+
known: boolean;
|
|
11
|
+
/** The window the unit came from (store clock), when the store reports it. */
|
|
12
|
+
resetAt?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Context for {@link RateLimiter.applyResponse} beyond the response itself. */
|
|
15
|
+
export interface ApplyResponseOptions {
|
|
16
|
+
/** Bucket-key prefix of the request's identity (see `requestIdentity`). */
|
|
17
|
+
identity?: string;
|
|
18
|
+
/** Global-limit scope the request counts against, if any. */
|
|
19
|
+
global?: string;
|
|
20
|
+
/** The decoded error body, for `retry_after` and `global` on a 429. */
|
|
21
|
+
body?: {
|
|
22
|
+
retry_after?: number;
|
|
23
|
+
global?: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** The grant the request was sent under. */
|
|
26
|
+
grant?: Grant;
|
|
27
|
+
}
|
|
2
28
|
/**
|
|
3
29
|
* Owns Discord's rate-limit state: when a request may go out, and what a
|
|
4
30
|
* response says about the bucket it landed in.
|
|
@@ -10,33 +36,72 @@ export declare class RateLimiter {
|
|
|
10
36
|
#private;
|
|
11
37
|
/**
|
|
12
38
|
* @param store Rate-limit state store.
|
|
13
|
-
* @param options `
|
|
14
|
-
*
|
|
39
|
+
* @param options `maxBackoff`: the most, in ms, that a rate-limit header can
|
|
40
|
+
* make later requests wait (default 10 minutes). `probeMs`: how long a
|
|
41
|
+
* request discovering an unknown bucket holds it (default 16 s; keep it
|
|
42
|
+
* above the transport timeout). `globalLimit`: most requests per second
|
|
43
|
+
* per global scope, enforced before Discord answers 429 (default none;
|
|
44
|
+
* `REST` passes 50). `telemetry`: event sink. `concurrent` is
|
|
45
|
+
* accepted for compatibility: responses are always merged so a late one
|
|
46
|
+
* cannot raise `remaining`.
|
|
15
47
|
*/
|
|
16
48
|
constructor(store: RateLimitStore, options?: {
|
|
17
49
|
concurrent?: boolean;
|
|
50
|
+
maxBackoff?: number;
|
|
51
|
+
probeMs?: number;
|
|
52
|
+
globalLimit?: number;
|
|
53
|
+
telemetry?: RESTTelemetry;
|
|
18
54
|
});
|
|
55
|
+
/** The ceiling on a wait a server header can cause, in ms. */
|
|
56
|
+
get maxBackoff(): number;
|
|
19
57
|
/** The backing store, shared across processes when a distributed store is used. */
|
|
20
58
|
get store(): RateLimitStore;
|
|
21
|
-
/** Resolves the bucket key a request should be gated on. */
|
|
22
|
-
resolveBucketKey(route: string, major: string): Promise<string>;
|
|
23
59
|
/** Whether the backing store can reserve allowance atomically. */
|
|
24
60
|
get reserves(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Resolves the bucket key a request should be gated on. With an
|
|
63
|
+
* `identity`, the key is prefixed by it, so two bots never share a bucket.
|
|
64
|
+
*/
|
|
65
|
+
resolveBucketKey(route: string, major: string, identity?: string): Promise<string>;
|
|
25
66
|
/**
|
|
26
67
|
* Waits until both the route bucket and the global limit permit sending.
|
|
27
68
|
*
|
|
28
69
|
* When the store supports it, allowance is *reserved* rather than merely
|
|
29
70
|
* observed. Reading `remaining` and deciding to send is a race across
|
|
30
71
|
* workers: three processes all read `remaining: 1` and all send. A
|
|
31
|
-
* reservation hands the unit to exactly one of them.
|
|
72
|
+
* reservation hands the unit to exactly one of them. A request is never
|
|
73
|
+
* sent without its reservation: a store that keeps refusing without a
|
|
74
|
+
* wait, or that fails, ends the request with a `"coordination"` error.
|
|
75
|
+
* @throws {RESTError} `"aborted"` on cancellation, `"coordination"` on a
|
|
76
|
+
* broken store, `"rateLimited"` when waiting would exceed `maxBackoff`.
|
|
77
|
+
*/
|
|
78
|
+
acquire(bucketKey: string, signal: AbortSignal | undefined, path: string, options?: {
|
|
79
|
+
global?: string;
|
|
80
|
+
}): Promise<Grant>;
|
|
81
|
+
/**
|
|
82
|
+
* Extends the global limit (of `scope`), never shortening a longer one
|
|
83
|
+
* already recorded. Atomic when the store has `extendGlobalReset`.
|
|
84
|
+
*/
|
|
85
|
+
noteGlobalReset(resetAt: number, scope?: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* How long a 429 asks to wait, in ms: the body's `retry_after`, else the
|
|
88
|
+
* `Retry-After` header, else `X-RateLimit-Reset-After`, else one second.
|
|
89
|
+
* Negative or non-numeric values are ignored, not read as zero.
|
|
32
90
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
91
|
+
retryAfterMs(response: Response, body?: {
|
|
92
|
+
retry_after?: number;
|
|
93
|
+
}): number;
|
|
36
94
|
/**
|
|
37
95
|
* Records a response's rate-limit headers and returns the bucket key the
|
|
38
96
|
* request actually belongs to: Discord may reveal a server bucket hash that
|
|
39
97
|
* differs from the route-derived key used before the first response.
|
|
40
98
|
*/
|
|
41
|
-
applyResponse(response: Response, bucketKey: string, route: string, major: string): Promise<string>;
|
|
99
|
+
applyResponse(response: Response, bucketKey: string, route: string, major: string, options?: ApplyResponseOptions): Promise<string>;
|
|
100
|
+
/**
|
|
101
|
+
* Gives back what a grant held when the request did not get a response:
|
|
102
|
+
* a probe placeholder is always cleared; the unit itself is returned only
|
|
103
|
+
* with `notSent` (the request certainly never left), because a request
|
|
104
|
+
* that may have reached Discord may also have spent it.
|
|
105
|
+
*/
|
|
106
|
+
releaseGrant(bucketKey: string, grant: Grant, notSent: boolean): Promise<void>;
|
|
42
107
|
}
|
package/dist/rest/redis.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type BucketState, type BucketUpdate, type RateLimitStore, type Reservation, type ReserveOptions } from "./store.js";
|
|
2
|
+
import { type CoordinationMode, type RESTTelemetry } from "./telemetry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Atomically checks the global limit and consumes one unit of a bucket.
|
|
5
|
+
* A port of `reserveState` in store.ts.
|
|
6
|
+
*
|
|
7
|
+
* `KEYS[1]` bucket · `KEYS[2]` global reset · `KEYS[3]` global request count
|
|
8
|
+
* (both optional) · `ARGV` probe ms, poll ms, linger ms, global limit, window ms.
|
|
9
|
+
* Returns `{1, resetAt, known}` or `{0, waitMs, reason}`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RESERVE_SCRIPT = "\nlocal function now_ms()\n local t = redis.call('TIME')\n return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal function load(key)\n local raw = redis.call('GET', key)\n if not raw then return nil end\n local ok, s = pcall(cjson.decode, raw)\n if ok and type(s) == 'table' and type(s.resetAt) == 'number'\n and type(s.remaining) == 'number' then return s end\n return nil\nend\nlocal function save(key, s, now, linger)\n local ttl = math.max(1, math.ceil(s.resetAt - now)) + linger\n redis.call('SET', key, cjson.encode(s), 'PX', ttl)\nend\n\nlocal now = now_ms()\nlocal glimit, gwindow = tonumber(ARGV[4]) or 0, tonumber(ARGV[5]) or 1000\nif #KEYS > 1 then\n local g = tonumber(redis.call('GET', KEYS[2]) or '0') or 0\n if g > now then return {0, math.ceil(g - now), 'global'} end\nend\nif #KEYS < 3 then glimit = 0 end\n-- A sliding log of send times: at most glimit in any gwindow span.\nif glimit > 0 then\n redis.call('ZREMRANGEBYSCORE', KEYS[3], '-inf', now - gwindow)\n if redis.call('ZCARD', KEYS[3]) >= glimit then\n local oldest = redis.call('ZRANGE', KEYS[3], 0, 0, 'WITHSCORES')\n return {0, math.max(1, tonumber(oldest[2]) + gwindow - now + 1), 'global'}\n end\nend\nlocal function count()\n if glimit > 0 then\n local seq = redis.call('ZCARD', KEYS[3])\n redis.call('ZADD', KEYS[3], now, now .. ':' .. seq)\n redis.call('PEXPIRE', KEYS[3], gwindow + 1000)\n end\nend\nlocal probe, poll, linger = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])\nlocal s = load(KEYS[1])\nif s and s.resetAt > now then\n if s.remaining > 0 then\n s.remaining = s.remaining - 1\n save(KEYS[1], s, now, linger)\n count()\n return {1, s.resetAt, 1}\n end\n local wait = s.resetAt - now\n if s.provisional then wait = math.min(wait, poll) end\n local reason = 'bucket'\n if s.probe then reason = 'probe' end\n return {0, math.ceil(wait), reason}\nend\nlocal resetAt = now + probe\nif s and type(s.limit) == 'number' and s.limit > 0 then\n save(KEYS[1], {remaining = s.limit - 1, resetAt = resetAt, limit = s.limit, provisional = true}, now, linger)\n count()\n return {1, resetAt, 1}\nend\nsave(KEYS[1], {remaining = 0, resetAt = resetAt, provisional = true, probe = true}, now, 0)\ncount()\nreturn {1, resetAt, 0}\n";
|
|
12
|
+
/**
|
|
13
|
+
* Atomically merges a response into a bucket. A port of `reconcileState`.
|
|
14
|
+
*
|
|
15
|
+
* `KEYS[1]` bucket · `ARGV` remaining, reset-after ms, limit, window, retry ms
|
|
16
|
+
* (each '' when absent), linger ms.
|
|
17
|
+
*/
|
|
18
|
+
export declare const RECONCILE_SCRIPT = "\nlocal function now_ms()\n local t = redis.call('TIME')\n return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal function load(key)\n local raw = redis.call('GET', key)\n if not raw then return nil end\n local ok, s = pcall(cjson.decode, raw)\n if ok and type(s) == 'table' and type(s.resetAt) == 'number'\n and type(s.remaining) == 'number' then return s end\n return nil\nend\nlocal function save(key, s, now, linger)\n local ttl = math.max(1, math.ceil(s.resetAt - now)) + linger\n redis.call('SET', key, cjson.encode(s), 'PX', ttl)\nend\n\nlocal now = now_ms()\nlocal rem, after, limit = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])\nlocal win, retry, linger = tonumber(ARGV[4]), tonumber(ARGV[5]), tonumber(ARGV[6])\nlocal tol = 50\nlocal incoming = nil\nif after then incoming = math.ceil(now + after) end\nlocal s = load(KEYS[1])\nlocal n\nif s and s.resetAt > now and not s.probe then\n n = s\n local stale = win and n.window and win < n.window - tol\n if not stale then\n if rem then n.remaining = math.min(n.remaining, rem) end\n if incoming then\n local newer = win and n.window and win > n.window + tol\n if n.provisional or newer then n.resetAt = incoming\n else n.resetAt = math.max(n.resetAt, incoming) end\n n.provisional = nil\n end\n if win then n.window = math.max(n.window or 0, win) end\n end\nelse\n if not rem and not retry then\n if s and s.probe then redis.call('DEL', KEYS[1]) end\n return 0\n end\n n = {remaining = rem or 0, resetAt = incoming or now}\n if s and type(s.limit) == 'number' then n.limit = s.limit end\n if win then n.window = win end\nend\nif limit and limit > 0 then n.limit = limit end\nif retry then\n n.remaining = 0\n n.resetAt = math.max(n.resetAt, math.ceil(now + retry))\n n.provisional = nil\nend\nsave(KEYS[1], n, now, linger)\nreturn 1\n";
|
|
19
|
+
/** Extends a global limit, never shortening it. `KEYS[1]` global · `ARGV[1]` wait ms. */
|
|
20
|
+
export declare const EXTEND_GLOBAL_SCRIPT = "\nlocal function now_ms()\n local t = redis.call('TIME')\n return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal function load(key)\n local raw = redis.call('GET', key)\n if not raw then return nil end\n local ok, s = pcall(cjson.decode, raw)\n if ok and type(s) == 'table' and type(s.resetAt) == 'number'\n and type(s.remaining) == 'number' then return s end\n return nil\nend\nlocal function save(key, s, now, linger)\n local ttl = math.max(1, math.ceil(s.resetAt - now)) + linger\n redis.call('SET', key, cjson.encode(s), 'PX', ttl)\nend\n\nlocal now = now_ms()\nlocal target = math.ceil(now + tonumber(ARGV[1]))\nlocal cur = tonumber(redis.call('GET', KEYS[1]) or '0') or 0\nif target > cur then\n redis.call('SET', KEYS[1], tostring(target), 'PX', math.max(1, target - now))\n return target - now\nend\nreturn math.max(0, cur - now)\n";
|
|
21
|
+
/** Clears a probe or refunds a unit. `KEYS[1]` bucket · `ARGV` resetAt, refund (1/0). */
|
|
22
|
+
export declare const RELEASE_SCRIPT = "\nlocal function now_ms()\n local t = redis.call('TIME')\n return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nend\nlocal function load(key)\n local raw = redis.call('GET', key)\n if not raw then return nil end\n local ok, s = pcall(cjson.decode, raw)\n if ok and type(s) == 'table' and type(s.resetAt) == 'number'\n and type(s.remaining) == 'number' then return s end\n return nil\nend\nlocal function save(key, s, now, linger)\n local ttl = math.max(1, math.ceil(s.resetAt - now)) + linger\n redis.call('SET', key, cjson.encode(s), 'PX', ttl)\nend\n\nlocal s = load(KEYS[1])\nif not s or s.resetAt ~= tonumber(ARGV[1]) then return 0 end\nif s.probe then\n redis.call('DEL', KEYS[1])\n return 1\nend\nif ARGV[2] == '1' then\n local cap = s.remaining + 1\n if type(s.limit) == 'number' then cap = math.min(cap, s.limit) end\n s.remaining = cap\n redis.call('SET', KEYS[1], cjson.encode(s), 'KEEPTTL')\n return 1\nend\nreturn 0\n";
|
|
2
23
|
/**
|
|
3
24
|
* A Redis client interface that matches the subset of ioredis
|
|
4
25
|
* methods needed for the RedisRateLimitStore.
|
|
@@ -10,45 +31,105 @@ export interface MinimalRedisClient {
|
|
|
10
31
|
del(...keys: string[]): Promise<number>;
|
|
11
32
|
/**
|
|
12
33
|
* Optional Lua evaluation (ioredis/node-redis compatible). When present,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
34
|
+
* the store reserves, reconciles and extends limits atomically; without
|
|
35
|
+
* it the store is observational and workers can race on the last unit.
|
|
15
36
|
*/
|
|
16
37
|
eval?(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
|
|
17
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Adapts Bun's built-in `Bun.RedisClient` (or anything with
|
|
41
|
+
* `send(command, args)`) to {@link MinimalRedisClient}, with `eval`.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* const store = new RedisRateLimitStore({
|
|
45
|
+
* client: fromBunRedis(new Bun.RedisClient(process.env.REDIS_URL)),
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function fromBunRedis(client: {
|
|
50
|
+
send(command: string, args: string[]): Promise<unknown>;
|
|
51
|
+
}): MinimalRedisClient;
|
|
52
|
+
/**
|
|
53
|
+
* What the store does when Redis cannot be reached.
|
|
54
|
+
*
|
|
55
|
+
* - `degraded-local` (default): limit from this process's last known state,
|
|
56
|
+
* report the mode change, and return to Redis when it answers again. Several
|
|
57
|
+
* processes limiting alone can together exceed a limit.
|
|
58
|
+
* - `fail-closed`: refuse to send. Requests fail with a `RESTError` of kind
|
|
59
|
+
* `"coordination"` until Redis answers.
|
|
60
|
+
*/
|
|
61
|
+
export type RedisFailurePolicy = "degraded-local" | "fail-closed";
|
|
62
|
+
/** Options for `RedisRateLimitStore`. */
|
|
18
63
|
export interface RedisRateLimitStoreOptions {
|
|
19
|
-
/** The Redis client instance (e.g. ioredis). */
|
|
64
|
+
/** The Redis client instance (e.g. ioredis, or `fromBunRedis(...)`). */
|
|
20
65
|
client: MinimalRedisClient;
|
|
21
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Prefix of every key. Defaults to `lunibee:rest:`; keys are then
|
|
68
|
+
* versioned (`lunibee:rest:v1:…`). On Redis Cluster, put a hash tag in it
|
|
69
|
+
* (`{lunibee}:rest:`) so one script's keys share a slot.
|
|
70
|
+
*/
|
|
22
71
|
prefix?: string;
|
|
23
|
-
/** Called whenever a Redis operation fails
|
|
72
|
+
/** Called whenever a Redis operation fails. */
|
|
24
73
|
onError?: (operation: string, error: unknown) => void;
|
|
74
|
+
/** What to do while Redis is unreachable. Default `"degraded-local"`. */
|
|
75
|
+
failurePolicy?: RedisFailurePolicy;
|
|
76
|
+
/** Longest one Redis command may take before it counts as failed, in ms. Default 2000. */
|
|
77
|
+
commandTimeout?: number;
|
|
78
|
+
/** Throw at construction when the client has no `eval` (no atomic reservations). Default false. */
|
|
79
|
+
requireAtomic?: boolean;
|
|
80
|
+
/** Receives `store` latency and `mode` events. */
|
|
81
|
+
telemetry?: RESTTelemetry;
|
|
82
|
+
}
|
|
83
|
+
/** The store's health, for diagnostics endpoints. */
|
|
84
|
+
export interface RedisStoreDiagnostics {
|
|
85
|
+
mode: CoordinationMode;
|
|
86
|
+
failurePolicy: RedisFailurePolicy;
|
|
87
|
+
atomic: boolean;
|
|
88
|
+
failures: number;
|
|
89
|
+
lastFailureAt?: number;
|
|
90
|
+
lastError?: string;
|
|
25
91
|
}
|
|
26
92
|
/**
|
|
27
|
-
* Redis-backed implementation of RateLimitStore for distributed rate limit
|
|
28
|
-
*
|
|
93
|
+
* Redis-backed implementation of RateLimitStore for distributed rate limit
|
|
94
|
+
* synchronization: workers sharing one Redis share every limit, reserve
|
|
95
|
+
* allowance atomically, and merge responses atomically.
|
|
29
96
|
*/
|
|
30
97
|
export declare class RedisRateLimitStore implements RateLimitStore {
|
|
31
98
|
#private;
|
|
32
99
|
constructor(options: RedisRateLimitStoreOptions);
|
|
33
100
|
/** The most recent Redis error, if any. */
|
|
34
101
|
get lastError(): unknown;
|
|
35
|
-
/** Whether no Redis operation has failed since construction or the last successful
|
|
102
|
+
/** Whether no Redis operation has failed since construction or the last successful one. */
|
|
36
103
|
isHealthy(): boolean;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getBucket(key: string): Promise<BucketState | undefined>;
|
|
40
|
-
updateBucket(key: string, state: BucketState): Promise<void>;
|
|
104
|
+
/** How limits are being enforced right now. */
|
|
105
|
+
get mode(): CoordinationMode;
|
|
41
106
|
/** Whether the backing client supports atomic reservations. */
|
|
42
107
|
get supportsReservation(): boolean;
|
|
108
|
+
/** The store's current health. */
|
|
109
|
+
diagnostics(): RedisStoreDiagnostics;
|
|
43
110
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* A Redis failure falls back to the local mirror rather than blocking:
|
|
49
|
-
* refusing every request during an outage would stall the bot.
|
|
111
|
+
* Checks that the client can run the store's scripts (some managed Redis
|
|
112
|
+
* services disable `EVAL`). Call it at startup to fail fast.
|
|
113
|
+
* @throws {TypeError} If the client has no `eval`.
|
|
114
|
+
* @throws {RESTError} Of kind `"coordination"`, if the script cannot run.
|
|
50
115
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
116
|
+
verify(): Promise<void>;
|
|
117
|
+
getBucketHash(route: string): Promise<string | undefined>;
|
|
118
|
+
setBucketHash(route: string, hash: string): Promise<void>;
|
|
119
|
+
getBucket(key: string): Promise<BucketState | undefined>;
|
|
120
|
+
updateBucket(key: string, state: BucketState): Promise<void>;
|
|
121
|
+
/** Atomically checks the global limit and consumes one unit of the bucket. */
|
|
122
|
+
reserve?: (key: string, options?: ReserveOptions) => Promise<Reservation>;
|
|
123
|
+
/** Atomically merges a response into the bucket. */
|
|
124
|
+
reconcile?: (key: string, update: BucketUpdate) => Promise<void>;
|
|
125
|
+
/** Atomically extends a global limit, never shortening it. */
|
|
126
|
+
extendGlobalReset?: (scope: string, waitMs: number) => Promise<void>;
|
|
127
|
+
/** Clears a probe placeholder or refunds an unsent request's unit. */
|
|
128
|
+
release?: (key: string, grant: {
|
|
129
|
+
resetAt?: number;
|
|
130
|
+
}, refund: boolean) => Promise<void>;
|
|
131
|
+
/** Whether {@link reserve} checks the global limit itself. */
|
|
132
|
+
readonly reservesGlobal?: boolean;
|
|
133
|
+
getGlobalReset(scope?: string): Promise<number>;
|
|
134
|
+
setGlobalReset(resetAt: number, scope?: string): Promise<void>;
|
|
54
135
|
}
|
package/dist/rest/scheduler.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/** Limits for {@link RequestScheduler}. */
|
|
2
|
+
export interface SchedulerOptions {
|
|
3
|
+
/** Most requests waiting for a slot across all buckets. Default unlimited. */
|
|
4
|
+
maxQueued?: number;
|
|
5
|
+
/** Most requests waiting for a slot on one bucket. Default unlimited. */
|
|
6
|
+
maxQueuedPerBucket?: number;
|
|
7
|
+
}
|
|
1
8
|
/**
|
|
2
9
|
* Serialises work per rate-limit bucket.
|
|
3
10
|
*
|
|
@@ -5,13 +12,18 @@
|
|
|
5
12
|
* limit state its predecessor's response wrote. The queue slot is released on
|
|
6
13
|
* every exit path — including an abort raised while still waiting — because a
|
|
7
14
|
* slot that is never released wedges its bucket for the process lifetime.
|
|
15
|
+
* Buckets queue independently: a slow bucket never holds up another.
|
|
8
16
|
*/
|
|
9
17
|
export declare class RequestScheduler {
|
|
10
18
|
#private;
|
|
11
19
|
/** Maximum times a request may be moved to a different bucket queue. */
|
|
12
20
|
static readonly MAX_REMAPS = 2;
|
|
21
|
+
/** Creates an empty scheduler. @param options Queue limits. */
|
|
22
|
+
constructor(options?: SchedulerOptions);
|
|
13
23
|
/** Number of bucket keys currently holding a queue slot. */
|
|
14
24
|
get size(): number;
|
|
25
|
+
/** Number of requests waiting for a slot. */
|
|
26
|
+
get queued(): number;
|
|
15
27
|
/**
|
|
16
28
|
* Runs `task` under the bucket key that `resolveKey` reports *at the moment
|
|
17
29
|
* the slot is acquired*, not at enqueue time.
|
|
@@ -37,6 +49,7 @@ export declare class RequestScheduler {
|
|
|
37
49
|
* @param path Request path, used for cancellation error context.
|
|
38
50
|
* @param task Work to run while holding the slot, given an idempotent
|
|
39
51
|
* callback that frees the slot before the task settles.
|
|
52
|
+
* @throws {RESTError} Of kind `"overloaded"` when a queue limit is reached.
|
|
40
53
|
*/
|
|
41
54
|
run<T>(key: string, signal: AbortSignal | undefined, path: string, task: (release: () => void) => Promise<T>): Promise<T>;
|
|
42
55
|
}
|
package/dist/rest/store.d.ts
CHANGED
|
@@ -1,20 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rate-limit bucket as stored: units `remaining` in the window and when it
|
|
3
|
+
* resets (`resetAt`, ms since the epoch, in the store's clock).
|
|
4
|
+
*/
|
|
1
5
|
export interface BucketState {
|
|
2
6
|
remaining: number;
|
|
3
7
|
resetAt: number;
|
|
8
|
+
/** The bucket's limit per window, once a response has told us. */
|
|
9
|
+
limit?: number;
|
|
10
|
+
/** `X-RateLimit-Reset` of the window this state describes, in epoch ms. */
|
|
11
|
+
window?: number;
|
|
12
|
+
/**
|
|
13
|
+
* `resetAt` is an estimate, not a server value: the window was opened by a
|
|
14
|
+
* reservation after the last one elapsed. A server reset replaces it.
|
|
15
|
+
*/
|
|
16
|
+
provisional?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Placeholder for a bucket nobody knows yet: one request (the probe) is
|
|
19
|
+
* on its way to discover the limit, and everyone else waits for it.
|
|
20
|
+
*/
|
|
21
|
+
probe?: boolean;
|
|
4
22
|
}
|
|
5
23
|
/**
|
|
6
24
|
* Outcome of an atomic reservation.
|
|
7
25
|
*
|
|
8
26
|
* `granted` means one unit of the bucket's allowance now belongs to the caller
|
|
9
27
|
* and to nobody else — the distinction that a read-then-write cannot make when
|
|
10
|
-
* several workers share a store.
|
|
28
|
+
* several workers share a store. `known` says whether the bucket's limit is
|
|
29
|
+
* known (so more requests may follow concurrently) or this request is the
|
|
30
|
+
* probe that discovers it; `resetAt` identifies the window the unit came from.
|
|
31
|
+
* A refusal says how long to wait and why.
|
|
11
32
|
*/
|
|
12
33
|
export type Reservation = {
|
|
13
34
|
granted: true;
|
|
35
|
+
known?: boolean;
|
|
36
|
+
resetAt?: number;
|
|
14
37
|
} | {
|
|
15
38
|
granted: false;
|
|
16
39
|
retryAfterMs: number;
|
|
40
|
+
reason?: "bucket" | "global" | "probe";
|
|
17
41
|
};
|
|
42
|
+
/** Options for {@link RateLimitStore.reserve}. */
|
|
43
|
+
export interface ReserveOptions {
|
|
44
|
+
/** Global-limit scope to check in the same atomic step, if any. */
|
|
45
|
+
global?: string;
|
|
46
|
+
/** How long a probe may hold an unknown bucket before others may try (ms). */
|
|
47
|
+
probeMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Most requests per second the `global` scope may send, counted in the
|
|
50
|
+
* same atomic step (Discord's global limit, 50 for most bots). Omitted or
|
|
51
|
+
* 0: only a recorded global 429 is enforced.
|
|
52
|
+
*/
|
|
53
|
+
globalLimit?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Span of the proactive global limit's sliding log (ms): at most the limit is
|
|
57
|
+
* sent in any span this long. A second plus a margin, so latency jitter
|
|
58
|
+
* cannot squeeze more than the limit into one second as Discord counts it.
|
|
59
|
+
*/
|
|
60
|
+
export declare const GLOBAL_WINDOW_MS = 1050;
|
|
61
|
+
/**
|
|
62
|
+
* Takes one slot from a sliding log of send times, or says how long until
|
|
63
|
+
* one frees. `log` holds the send times still inside the window, oldest first.
|
|
64
|
+
*/
|
|
65
|
+
export declare function takeGlobalSlot(log: number[], now: number, limit: number, window?: number): number;
|
|
66
|
+
/** What a response said about a bucket, for {@link RateLimitStore.reconcile}. */
|
|
67
|
+
export interface BucketUpdate {
|
|
68
|
+
/** `X-RateLimit-Remaining`. */
|
|
69
|
+
remaining?: number;
|
|
70
|
+
/** `X-RateLimit-Reset-After`, in ms from now. */
|
|
71
|
+
resetAfterMs?: number;
|
|
72
|
+
/** `X-RateLimit-Limit`. */
|
|
73
|
+
limit?: number;
|
|
74
|
+
/** `X-RateLimit-Reset`, epoch ms: identifies the window. */
|
|
75
|
+
window?: number;
|
|
76
|
+
/** A bucket 429's wait, in ms: the bucket is spent until then. */
|
|
77
|
+
retryAfterMs?: number;
|
|
78
|
+
}
|
|
18
79
|
/** Interface for distributed or local rate limit synchronization. */
|
|
19
80
|
export interface RateLimitStore {
|
|
20
81
|
/** Gets the server bucket hash for a normalized route. */
|
|
@@ -23,21 +84,47 @@ export interface RateLimitStore {
|
|
|
23
84
|
setBucketHash(route: string, hash: string): Promise<void> | void;
|
|
24
85
|
/** Gets the current state for a bucket key. */
|
|
25
86
|
getBucket(key: string): Promise<BucketState | undefined> | BucketState | undefined;
|
|
26
|
-
/**
|
|
87
|
+
/** Replaces the state for a bucket key. */
|
|
27
88
|
updateBucket(key: string, state: BucketState): Promise<void> | void;
|
|
28
|
-
/** Gets the global reset timestamp. */
|
|
29
|
-
getGlobalReset(): Promise<number> | number;
|
|
30
|
-
/** Sets the global reset timestamp. */
|
|
31
|
-
setGlobalReset(resetAt: number): Promise<void> | void;
|
|
89
|
+
/** Gets the global reset timestamp (of `scope`, when the store keeps several). */
|
|
90
|
+
getGlobalReset(scope?: string): Promise<number> | number;
|
|
91
|
+
/** Sets the global reset timestamp (of `scope`, when the store keeps several). */
|
|
92
|
+
setGlobalReset(resetAt: number, scope?: string): Promise<void> | void;
|
|
32
93
|
/**
|
|
33
94
|
* Atomically consumes one unit of a bucket's allowance.
|
|
34
95
|
*
|
|
35
96
|
* Optional: a store that cannot do this atomically must omit it, and the
|
|
36
97
|
* limiter falls back to waiting on observed state. Implementations must
|
|
37
|
-
*
|
|
38
|
-
*
|
|
98
|
+
* never refuse forever: an unknown bucket is discovered by sending, so
|
|
99
|
+
* one caller (the probe) must be granted.
|
|
39
100
|
*/
|
|
40
|
-
reserve?(key: string): Promise<Reservation> | Reservation;
|
|
101
|
+
reserve?(key: string, options?: ReserveOptions): Promise<Reservation> | Reservation;
|
|
102
|
+
/**
|
|
103
|
+
* Whether {@link reserve} also checks `options.global` atomically. When
|
|
104
|
+
* it does not, the limiter reads the global reset itself first.
|
|
105
|
+
*/
|
|
106
|
+
readonly reservesGlobal?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Atomically merges what a response said into a bucket, so a late or
|
|
109
|
+
* out-of-order response cannot hand back allowance already reserved.
|
|
110
|
+
* Optional: without it the limiter reads, merges and writes.
|
|
111
|
+
*/
|
|
112
|
+
reconcile?(key: string, update: BucketUpdate): Promise<void> | void;
|
|
113
|
+
/**
|
|
114
|
+
* Atomically extends a global limit to at least `waitMs` from now, never
|
|
115
|
+
* shortening a longer one. Optional: without it the limiter reads and
|
|
116
|
+
* writes, which a concurrent writer can shorten.
|
|
117
|
+
*/
|
|
118
|
+
extendGlobalReset?(scope: string, waitMs: number): Promise<void> | void;
|
|
119
|
+
/**
|
|
120
|
+
* Gives back what a granted reservation held, when that is certain to be
|
|
121
|
+
* safe: a probe placeholder is cleared (so the next request may probe),
|
|
122
|
+
* and with `refund` — the request was never sent — the unit is returned
|
|
123
|
+
* to the window it came from, if that window is still current.
|
|
124
|
+
*/
|
|
125
|
+
release?(key: string, grant: {
|
|
126
|
+
resetAt?: number;
|
|
127
|
+
}, refund: boolean): Promise<void> | void;
|
|
41
128
|
/**
|
|
42
129
|
* Evicts expired bucket entries. Optional: stores with native key
|
|
43
130
|
* expiry (e.g. Redis) can omit it. `maxAge` (ms) additionally drops
|
|
@@ -46,9 +133,38 @@ export interface RateLimitStore {
|
|
|
46
133
|
*/
|
|
47
134
|
prune?(maxAge?: number): Promise<void> | void;
|
|
48
135
|
}
|
|
136
|
+
/** Longest a refused waiter sleeps on a placeholder before asking again (ms). */
|
|
137
|
+
export declare const PROBE_POLL_MS = 50;
|
|
138
|
+
/** Default time a probe may hold an unknown bucket (ms). */
|
|
139
|
+
export declare const DEFAULT_PROBE_MS = 16000;
|
|
140
|
+
/** How long a bucket's limit is remembered after its window resets (ms). */
|
|
141
|
+
export declare const BUCKET_LINGER_MS = 60000;
|
|
142
|
+
/**
|
|
143
|
+
* The reservation decision, shared by every built-in store (the Redis store
|
|
144
|
+
* runs a Lua port of it). Returns the new state to write, if any.
|
|
145
|
+
*/
|
|
146
|
+
export declare function reserveState(state: BucketState | undefined, now: number, probeMs: number): {
|
|
147
|
+
reservation: Reservation;
|
|
148
|
+
next?: BucketState;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The merge of a response into a bucket, shared by every built-in store.
|
|
152
|
+
*
|
|
153
|
+
* Within a window `remaining` only ever goes down: the stored value already
|
|
154
|
+
* counts reservations still in flight, the header counts what the server
|
|
155
|
+
* has seen, and the lower of the two is the one that cannot over-send. A
|
|
156
|
+
* response from an older window than the one stored is ignored. An estimated
|
|
157
|
+
* reset is replaced by the server's.
|
|
158
|
+
*
|
|
159
|
+
* Returns the state to write, `null` to delete, or `undefined` for no change.
|
|
160
|
+
*/
|
|
161
|
+
export declare function reconcileState(state: BucketState | undefined, update: BucketUpdate, now: number): BucketState | null | undefined;
|
|
49
162
|
/** Local memory implementation of RateLimitStore. */
|
|
50
163
|
export declare class MemoryRateLimitStore implements RateLimitStore {
|
|
51
164
|
#private;
|
|
165
|
+
readonly reservesGlobal = true;
|
|
166
|
+
/** Creates an empty instance. */
|
|
167
|
+
constructor();
|
|
52
168
|
getBucketHash(route: string): string | undefined;
|
|
53
169
|
setBucketHash(route: string, hash: string): void;
|
|
54
170
|
getBucket(key: string): BucketState | undefined;
|
|
@@ -58,10 +174,17 @@ export declare class MemoryRateLimitStore implements RateLimitStore {
|
|
|
58
174
|
* Single-process reservation is trivially atomic: JavaScript will not
|
|
59
175
|
* interleave this method with another caller's copy of it.
|
|
60
176
|
*/
|
|
61
|
-
reserve(key: string): Reservation;
|
|
177
|
+
reserve(key: string, options?: ReserveOptions): Reservation;
|
|
178
|
+
reconcile(key: string, update: BucketUpdate): void;
|
|
179
|
+
release(key: string, grant: {
|
|
180
|
+
resetAt?: number;
|
|
181
|
+
}, refund: boolean): void;
|
|
62
182
|
updateBucket(key: string, state: BucketState): void;
|
|
63
183
|
/** Removes buckets whose reset is in the past (or older than `maxAge` ms). */
|
|
64
184
|
prune(maxAge?: number): void;
|
|
65
|
-
getGlobalReset(): number;
|
|
66
|
-
setGlobalReset(resetAt: number): void;
|
|
185
|
+
getGlobalReset(scope?: string): number;
|
|
186
|
+
setGlobalReset(resetAt: number, scope?: string): void;
|
|
187
|
+
extendGlobalReset(scope: string, waitMs: number): void;
|
|
188
|
+
/** Number of buckets held (for diagnostics and leak checks). */
|
|
189
|
+
get size(): number;
|
|
67
190
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the REST pipeline reports while it works. Every field is bounded and
|
|
3
|
+
* free of secrets: routes are normalized (ids become `:id`, tokens `:token`,
|
|
4
|
+
* reaction emoji `:emoji`), and no token, fingerprint or body is included.
|
|
5
|
+
*/
|
|
6
|
+
export type RESTTelemetryEvent =
|
|
7
|
+
/** One HTTP attempt got a response. */
|
|
8
|
+
{
|
|
9
|
+
type: "response";
|
|
10
|
+
method: string;
|
|
11
|
+
route: string;
|
|
12
|
+
status: number;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
}
|
|
15
|
+
/** A request settled (after every attempt), successfully or not. */
|
|
16
|
+
| {
|
|
17
|
+
type: "request";
|
|
18
|
+
method: string;
|
|
19
|
+
route: string;
|
|
20
|
+
status: number;
|
|
21
|
+
attempts: number;
|
|
22
|
+
durationMs: number;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Discord answered 429. */
|
|
26
|
+
| {
|
|
27
|
+
type: "rateLimited";
|
|
28
|
+
route: string;
|
|
29
|
+
scope: "user" | "global" | "shared" | "unknown";
|
|
30
|
+
retryAfterMs: number;
|
|
31
|
+
}
|
|
32
|
+
/** A request waited for a limit before it could be sent. */
|
|
33
|
+
| {
|
|
34
|
+
type: "wait";
|
|
35
|
+
reason: "bucket" | "global" | "probe";
|
|
36
|
+
durationMs: number;
|
|
37
|
+
}
|
|
38
|
+
/** A reservation was asked of the store. */
|
|
39
|
+
| {
|
|
40
|
+
type: "reservation";
|
|
41
|
+
granted: boolean;
|
|
42
|
+
known: boolean;
|
|
43
|
+
latencyMs: number;
|
|
44
|
+
}
|
|
45
|
+
/** A request is being sent again. */
|
|
46
|
+
| {
|
|
47
|
+
type: "retry";
|
|
48
|
+
route: string;
|
|
49
|
+
reason: "rateLimited" | "server" | "network";
|
|
50
|
+
attempt: number;
|
|
51
|
+
}
|
|
52
|
+
/** A request was cancelled. */
|
|
53
|
+
| {
|
|
54
|
+
type: "cancelled";
|
|
55
|
+
stage: "queued" | "waiting" | "transport" | "retry";
|
|
56
|
+
}
|
|
57
|
+
/** A request was refused because a queue was full. */
|
|
58
|
+
| {
|
|
59
|
+
type: "overloaded";
|
|
60
|
+
}
|
|
61
|
+
/** Discord answered 401 on a bot route: later requests with the token are refused. */
|
|
62
|
+
| {
|
|
63
|
+
type: "tokenRejected";
|
|
64
|
+
}
|
|
65
|
+
/** A transport attempt failed without a response. */
|
|
66
|
+
| {
|
|
67
|
+
type: "networkError";
|
|
68
|
+
timedOut: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** A route learned its bucket hash, or the hash changed. */
|
|
71
|
+
| {
|
|
72
|
+
type: "bucket";
|
|
73
|
+
change: "discovered" | "remapped";
|
|
74
|
+
route: string;
|
|
75
|
+
}
|
|
76
|
+
/** One store command (Redis), with its latency. */
|
|
77
|
+
| {
|
|
78
|
+
type: "store";
|
|
79
|
+
operation: string;
|
|
80
|
+
latencyMs: number;
|
|
81
|
+
ok: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** The store's coordination mode changed. */
|
|
84
|
+
| {
|
|
85
|
+
type: "mode";
|
|
86
|
+
mode: CoordinationMode;
|
|
87
|
+
previous: CoordinationMode;
|
|
88
|
+
}
|
|
89
|
+
/** Queue depth and in-flight requests, after a change. */
|
|
90
|
+
| {
|
|
91
|
+
type: "queue";
|
|
92
|
+
queued: number;
|
|
93
|
+
inFlight: number;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* How a store enforces limits right now.
|
|
97
|
+
*
|
|
98
|
+
* - `distributed`: shared state with atomic reservations.
|
|
99
|
+
* - `observational`: shared state without atomic reservations (a client with
|
|
100
|
+
* no `eval`): workers can race on the last unit.
|
|
101
|
+
* - `degraded-local`: shared state is unreachable and this process is
|
|
102
|
+
* limiting alone, from its last known state.
|
|
103
|
+
* - `unavailable`: shared state is unreachable and requests fail
|
|
104
|
+
* (`failurePolicy: "fail-closed"`).
|
|
105
|
+
* - `local`: a process-local store.
|
|
106
|
+
*/
|
|
107
|
+
export type CoordinationMode = "distributed" | "observational" | "degraded-local" | "unavailable" | "local";
|
|
108
|
+
/** Receives telemetry events. Must be fast and must not throw. */
|
|
109
|
+
export interface RESTTelemetry {
|
|
110
|
+
emit(event: RESTTelemetryEvent): void;
|
|
111
|
+
}
|
|
112
|
+
/** Calls a sink without letting it break the request path. */
|
|
113
|
+
export declare function emitTelemetry(sink: RESTTelemetry | undefined, event: RESTTelemetryEvent): void;
|
|
114
|
+
/** A snapshot of {@link RESTMetrics}. */
|
|
115
|
+
export interface RESTMetricsSnapshot {
|
|
116
|
+
/** `name{label="v",…}` → count. */
|
|
117
|
+
counters: Record<string, number>;
|
|
118
|
+
/** `name` → last value. */
|
|
119
|
+
gauges: Record<string, number>;
|
|
120
|
+
/** `name{…}` → latency distribution in ms. */
|
|
121
|
+
histograms: Record<string, {
|
|
122
|
+
count: number;
|
|
123
|
+
sum: number;
|
|
124
|
+
buckets: Record<string, number>;
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* An in-memory metrics aggregator for {@link RESTTelemetry}: counters,
|
|
129
|
+
* gauges and latency histograms, with a hard cap on distinct label sets so
|
|
130
|
+
* an unexpected route shape cannot grow it without bound.
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* const metrics = new RESTMetrics();
|
|
134
|
+
* const rest = new REST({ token, telemetry: metrics });
|
|
135
|
+
* // later: metrics.snapshot(), or metrics.toPrometheus() for a /metrics endpoint
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare class RESTMetrics implements RESTTelemetry {
|
|
139
|
+
#private;
|
|
140
|
+
/** @param options `maxRoutes`: distinct route labels kept before the rest become `other` (default 500). */
|
|
141
|
+
constructor(options?: {
|
|
142
|
+
maxRoutes?: number;
|
|
143
|
+
});
|
|
144
|
+
emit(event: RESTTelemetryEvent): void;
|
|
145
|
+
/** The current values. */
|
|
146
|
+
snapshot(): RESTMetricsSnapshot;
|
|
147
|
+
/** Clears every value. */
|
|
148
|
+
reset(): void;
|
|
149
|
+
/** The values in the Prometheus text exposition format. */
|
|
150
|
+
toPrometheus(prefix?: string): string;
|
|
151
|
+
}
|