halt-rate 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/adapters/fastify.d.mts +29 -0
- package/dist/adapters/fastify.d.ts +29 -0
- package/dist/adapters/fastify.js +38 -0
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/fastify.mjs +35 -0
- package/dist/adapters/fastify.mjs.map +1 -0
- package/dist/adapters/graphql.d.mts +22 -0
- package/dist/adapters/graphql.d.ts +22 -0
- package/dist/adapters/graphql.js +51 -0
- package/dist/adapters/graphql.js.map +1 -0
- package/dist/adapters/graphql.mjs +49 -0
- package/dist/adapters/graphql.mjs.map +1 -0
- package/dist/adapters/hono.d.mts +30 -0
- package/dist/adapters/hono.d.ts +30 -0
- package/dist/adapters/hono.js +56 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/hono.mjs +54 -0
- package/dist/adapters/hono.mjs.map +1 -0
- package/dist/adapters/next.js +3 -2
- package/dist/adapters/next.js.map +1 -1
- package/dist/adapters/next.mjs +3 -2
- package/dist/adapters/next.mjs.map +1 -1
- package/dist/index.d.mts +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +69 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +66 -11
package/dist/index.d.mts
CHANGED
|
@@ -130,6 +130,54 @@ declare namespace extractors {
|
|
|
130
130
|
export { extractors_HEALTH_CHECK_PATHS as HEALTH_CHECK_PATHS, extractors_extractApiKey as extractApiKey, extractors_extractIp as extractIp, extractors_extractPath as extractPath, extractors_extractUserId as extractUserId, extractors_isHealthCheck as isHealthCheck, extractors_isPrivateIp as isPrivateIp };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Dynamic limits — change policies at runtime without restarting.
|
|
135
|
+
*
|
|
136
|
+
* `PolicyRegistry` holds named policies you can mutate live. Use it as the
|
|
137
|
+
* limiter's `policy` via `registry.resolver(selector)`:
|
|
138
|
+
*
|
|
139
|
+
* const registry = new PolicyRegistry([presets.PUBLIC_API]);
|
|
140
|
+
* const limiter = new RateLimiter({
|
|
141
|
+
* store,
|
|
142
|
+
* policy: registry.resolver((req) => 'public_api'),
|
|
143
|
+
* });
|
|
144
|
+
* // later, no restart:
|
|
145
|
+
* registry.update('public_api', { limit: 500 });
|
|
146
|
+
*
|
|
147
|
+
* For limits stored in Redis/DB/config (and shared across a fleet), use
|
|
148
|
+
* `cachedPolicyResolver` with a loader that reads that shared state.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
declare class PolicyRegistry {
|
|
152
|
+
private policies;
|
|
153
|
+
constructor(initial?: Policy[]);
|
|
154
|
+
/** Add or replace a policy (keyed by `policy.name`). */
|
|
155
|
+
register(policy: Policy): this;
|
|
156
|
+
get(name: string): Policy | undefined;
|
|
157
|
+
has(name: string): boolean;
|
|
158
|
+
/** Mutate fields of an existing policy at runtime (e.g. `{ limit: 500 }`). */
|
|
159
|
+
update(name: string, patch: Partial<Omit<Policy, 'name'>>): Policy;
|
|
160
|
+
remove(name: string): boolean;
|
|
161
|
+
list(): Policy[];
|
|
162
|
+
/**
|
|
163
|
+
* Build a resolver for the limiter's `policy` option.
|
|
164
|
+
* @param selector maps a request to a registered policy name.
|
|
165
|
+
*/
|
|
166
|
+
resolver(selector: (request: any) => string): (request: any) => Policy;
|
|
167
|
+
}
|
|
168
|
+
interface CachedPolicyResolverOptions {
|
|
169
|
+
/** Cache entry lifetime in ms (default 5000). */
|
|
170
|
+
ttlMs?: number;
|
|
171
|
+
/** Cache key for a request (default: the loaded policy is shared under one key). */
|
|
172
|
+
key?: (request: any) => string;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Wrap a (possibly async) loader with a per-key TTL cache. The loader reads your
|
|
176
|
+
* source of truth (Redis/DB/config), so limits propagate across a fleet and
|
|
177
|
+
* refresh live — without restarting. Use the returned function as `policy`.
|
|
178
|
+
*/
|
|
179
|
+
declare function cachedPolicyResolver(loader: (request: any) => Policy | Promise<Policy>, options?: CachedPolicyResolverOptions): (request: any) => Promise<Policy>;
|
|
180
|
+
|
|
133
181
|
/**
|
|
134
182
|
* In-process statistics collector for rate-limit observability.
|
|
135
183
|
*
|
|
@@ -253,4 +301,4 @@ declare class OpenTelemetryMetrics implements TelemetryHooks {
|
|
|
253
301
|
onViolation(_identifier: string, _penalty: Penalty, severity: number): void;
|
|
254
302
|
}
|
|
255
303
|
|
|
256
|
-
export { AtomicStore, Decision, EvaluateInput, type FailMode, type OTelCounterLike, type OTelMeterLike, OpenTelemetryMetrics, Penalty, Policy, Quota, RedisClientLike, RedisStore, type RedisStoreOptions, StatsCollector, type StatsCollectorOptions, type StatsSnapshot, TelemetryHooks, extractors, index as presets };
|
|
304
|
+
export { AtomicStore, type CachedPolicyResolverOptions, Decision, EvaluateInput, type FailMode, type OTelCounterLike, type OTelMeterLike, OpenTelemetryMetrics, Penalty, Policy, PolicyRegistry, Quota, RedisClientLike, RedisStore, type RedisStoreOptions, StatsCollector, type StatsCollectorOptions, type StatsSnapshot, TelemetryHooks, cachedPolicyResolver, extractors, index as presets };
|
package/dist/index.d.ts
CHANGED
|
@@ -130,6 +130,54 @@ declare namespace extractors {
|
|
|
130
130
|
export { extractors_HEALTH_CHECK_PATHS as HEALTH_CHECK_PATHS, extractors_extractApiKey as extractApiKey, extractors_extractIp as extractIp, extractors_extractPath as extractPath, extractors_extractUserId as extractUserId, extractors_isHealthCheck as isHealthCheck, extractors_isPrivateIp as isPrivateIp };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Dynamic limits — change policies at runtime without restarting.
|
|
135
|
+
*
|
|
136
|
+
* `PolicyRegistry` holds named policies you can mutate live. Use it as the
|
|
137
|
+
* limiter's `policy` via `registry.resolver(selector)`:
|
|
138
|
+
*
|
|
139
|
+
* const registry = new PolicyRegistry([presets.PUBLIC_API]);
|
|
140
|
+
* const limiter = new RateLimiter({
|
|
141
|
+
* store,
|
|
142
|
+
* policy: registry.resolver((req) => 'public_api'),
|
|
143
|
+
* });
|
|
144
|
+
* // later, no restart:
|
|
145
|
+
* registry.update('public_api', { limit: 500 });
|
|
146
|
+
*
|
|
147
|
+
* For limits stored in Redis/DB/config (and shared across a fleet), use
|
|
148
|
+
* `cachedPolicyResolver` with a loader that reads that shared state.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
declare class PolicyRegistry {
|
|
152
|
+
private policies;
|
|
153
|
+
constructor(initial?: Policy[]);
|
|
154
|
+
/** Add or replace a policy (keyed by `policy.name`). */
|
|
155
|
+
register(policy: Policy): this;
|
|
156
|
+
get(name: string): Policy | undefined;
|
|
157
|
+
has(name: string): boolean;
|
|
158
|
+
/** Mutate fields of an existing policy at runtime (e.g. `{ limit: 500 }`). */
|
|
159
|
+
update(name: string, patch: Partial<Omit<Policy, 'name'>>): Policy;
|
|
160
|
+
remove(name: string): boolean;
|
|
161
|
+
list(): Policy[];
|
|
162
|
+
/**
|
|
163
|
+
* Build a resolver for the limiter's `policy` option.
|
|
164
|
+
* @param selector maps a request to a registered policy name.
|
|
165
|
+
*/
|
|
166
|
+
resolver(selector: (request: any) => string): (request: any) => Policy;
|
|
167
|
+
}
|
|
168
|
+
interface CachedPolicyResolverOptions {
|
|
169
|
+
/** Cache entry lifetime in ms (default 5000). */
|
|
170
|
+
ttlMs?: number;
|
|
171
|
+
/** Cache key for a request (default: the loaded policy is shared under one key). */
|
|
172
|
+
key?: (request: any) => string;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Wrap a (possibly async) loader with a per-key TTL cache. The loader reads your
|
|
176
|
+
* source of truth (Redis/DB/config), so limits propagate across a fleet and
|
|
177
|
+
* refresh live — without restarting. Use the returned function as `policy`.
|
|
178
|
+
*/
|
|
179
|
+
declare function cachedPolicyResolver(loader: (request: any) => Policy | Promise<Policy>, options?: CachedPolicyResolverOptions): (request: any) => Promise<Policy>;
|
|
180
|
+
|
|
133
181
|
/**
|
|
134
182
|
* In-process statistics collector for rate-limit observability.
|
|
135
183
|
*
|
|
@@ -253,4 +301,4 @@ declare class OpenTelemetryMetrics implements TelemetryHooks {
|
|
|
253
301
|
onViolation(_identifier: string, _penalty: Penalty, severity: number): void;
|
|
254
302
|
}
|
|
255
303
|
|
|
256
|
-
export { AtomicStore, Decision, EvaluateInput, type FailMode, type OTelCounterLike, type OTelMeterLike, OpenTelemetryMetrics, Penalty, Policy, Quota, RedisClientLike, RedisStore, type RedisStoreOptions, StatsCollector, type StatsCollectorOptions, type StatsSnapshot, TelemetryHooks, extractors, index as presets };
|
|
304
|
+
export { AtomicStore, type CachedPolicyResolverOptions, Decision, EvaluateInput, type FailMode, type OTelCounterLike, type OTelMeterLike, OpenTelemetryMetrics, Penalty, Policy, PolicyRegistry, Quota, RedisClientLike, RedisStore, type RedisStoreOptions, StatsCollector, type StatsCollectorOptions, type StatsSnapshot, TelemetryHooks, cachedPolicyResolver, extractors, index as presets };
|
package/dist/index.js
CHANGED
|
@@ -486,7 +486,8 @@ var RateLimiter = class {
|
|
|
486
486
|
return decision2;
|
|
487
487
|
}
|
|
488
488
|
const store = this.store;
|
|
489
|
-
|
|
489
|
+
const cacheKey = `${policy.name}|${policy.algorithm}|${policy.limit}|${policy.window}|${policy.burst}`;
|
|
490
|
+
let algorithm = this.algorithmCache.get(cacheKey);
|
|
490
491
|
if (!algorithm) {
|
|
491
492
|
if (policy.algorithm === "token_bucket" /* TOKEN_BUCKET */) {
|
|
492
493
|
algorithm = new TokenBucket(policy.burst, policy.limit, policy.window);
|
|
@@ -500,7 +501,7 @@ var RateLimiter = class {
|
|
|
500
501
|
} else {
|
|
501
502
|
throw new Error(`Algorithm ${policy.algorithm} not implemented`);
|
|
502
503
|
}
|
|
503
|
-
this.algorithmCache.set(
|
|
504
|
+
this.algorithmCache.set(cacheKey, algorithm);
|
|
504
505
|
}
|
|
505
506
|
const span = this.otelTracer?.startSpan?.("halt.check", { attributes: { policy: policy.name, key } });
|
|
506
507
|
const state = store.get(storageKey);
|
|
@@ -1095,6 +1096,70 @@ function getPlanPolicy(planName) {
|
|
|
1095
1096
|
return PLAN_TIERS[normalized];
|
|
1096
1097
|
}
|
|
1097
1098
|
|
|
1099
|
+
// src/core/registry.ts
|
|
1100
|
+
var PolicyRegistry = class {
|
|
1101
|
+
constructor(initial = []) {
|
|
1102
|
+
this.policies = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (const p of initial) this.register(p);
|
|
1104
|
+
}
|
|
1105
|
+
/** Add or replace a policy (keyed by `policy.name`). */
|
|
1106
|
+
register(policy) {
|
|
1107
|
+
this.policies.set(policy.name, policy);
|
|
1108
|
+
return this;
|
|
1109
|
+
}
|
|
1110
|
+
get(name) {
|
|
1111
|
+
return this.policies.get(name);
|
|
1112
|
+
}
|
|
1113
|
+
has(name) {
|
|
1114
|
+
return this.policies.has(name);
|
|
1115
|
+
}
|
|
1116
|
+
/** Mutate fields of an existing policy at runtime (e.g. `{ limit: 500 }`). */
|
|
1117
|
+
update(name, patch) {
|
|
1118
|
+
const existing = this.policies.get(name);
|
|
1119
|
+
if (!existing) throw new Error(`Unknown policy: ${name}`);
|
|
1120
|
+
const updated = { ...existing, ...patch, name: existing.name };
|
|
1121
|
+
if (patch.limit !== void 0 && patch.burst === void 0) {
|
|
1122
|
+
delete updated.burst;
|
|
1123
|
+
}
|
|
1124
|
+
this.policies.set(name, updated);
|
|
1125
|
+
return updated;
|
|
1126
|
+
}
|
|
1127
|
+
remove(name) {
|
|
1128
|
+
return this.policies.delete(name);
|
|
1129
|
+
}
|
|
1130
|
+
list() {
|
|
1131
|
+
return [...this.policies.values()];
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Build a resolver for the limiter's `policy` option.
|
|
1135
|
+
* @param selector maps a request to a registered policy name.
|
|
1136
|
+
*/
|
|
1137
|
+
resolver(selector) {
|
|
1138
|
+
return (request) => {
|
|
1139
|
+
const name = selector(request);
|
|
1140
|
+
const policy = this.policies.get(name);
|
|
1141
|
+
if (!policy) throw new Error(`Unknown policy: ${name}`);
|
|
1142
|
+
return policy;
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
function cachedPolicyResolver(loader, options = {}) {
|
|
1147
|
+
const ttlMs = options.ttlMs ?? 5e3;
|
|
1148
|
+
const keyFn = options.key ?? (() => "__default__");
|
|
1149
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1150
|
+
return async (request) => {
|
|
1151
|
+
const key = keyFn(request);
|
|
1152
|
+
const now = Date.now();
|
|
1153
|
+
const hit = cache.get(key);
|
|
1154
|
+
if (hit && hit.expires > now) {
|
|
1155
|
+
return hit.policy;
|
|
1156
|
+
}
|
|
1157
|
+
const policy = await loader(request);
|
|
1158
|
+
cache.set(key, { policy, expires: now + ttlMs });
|
|
1159
|
+
return policy;
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1098
1163
|
// src/core/quota.ts
|
|
1099
1164
|
var QuotaPeriod = /* @__PURE__ */ ((QuotaPeriod2) => {
|
|
1100
1165
|
QuotaPeriod2["HOURLY"] = "hourly";
|
|
@@ -1624,6 +1689,7 @@ exports.PENALTY_LENIENT = PENALTY_LENIENT;
|
|
|
1624
1689
|
exports.PENALTY_MODERATE = PENALTY_MODERATE;
|
|
1625
1690
|
exports.PENALTY_STRICT = PENALTY_STRICT;
|
|
1626
1691
|
exports.PenaltyManager = PenaltyManager;
|
|
1692
|
+
exports.PolicyRegistry = PolicyRegistry;
|
|
1627
1693
|
exports.QUOTA_ENTERPRISE_MONTHLY = QUOTA_ENTERPRISE_MONTHLY;
|
|
1628
1694
|
exports.QUOTA_FREE_DAILY = QUOTA_FREE_DAILY;
|
|
1629
1695
|
exports.QUOTA_FREE_MONTHLY = QUOTA_FREE_MONTHLY;
|
|
@@ -1634,6 +1700,7 @@ exports.QuotaPeriod = QuotaPeriod;
|
|
|
1634
1700
|
exports.RateLimiter = RateLimiter;
|
|
1635
1701
|
exports.RedisStore = RedisStore;
|
|
1636
1702
|
exports.StatsCollector = StatsCollector;
|
|
1703
|
+
exports.cachedPolicyResolver = cachedPolicyResolver;
|
|
1637
1704
|
exports.extractors = extractors_exports;
|
|
1638
1705
|
exports.isAtomicStore = isAtomicStore;
|
|
1639
1706
|
exports.normalizePolicy = normalizePolicy;
|