halt-rate 0.3.0 → 0.5.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/dist/index.mjs CHANGED
@@ -28,6 +28,7 @@ function normalizePolicy(policy) {
28
28
  algorithm: policy.algorithm ?? "token_bucket" /* TOKEN_BUCKET */,
29
29
  keyStrategy: policy.keyStrategy ?? "ip" /* IP */,
30
30
  burst: policy.burst ?? Math.floor(policy.limit * 1.2),
31
+ slidingPrecision: policy.slidingPrecision ?? 10,
31
32
  cost: policy.cost ?? 1,
32
33
  blockDuration: policy.blockDuration ?? void 0,
33
34
  keyExtractor: policy.keyExtractor ?? void 0,
@@ -43,6 +44,9 @@ function normalizePolicy(policy) {
43
44
  if (normalized.cost <= 0) {
44
45
  throw new Error("cost must be positive");
45
46
  }
47
+ if (!Number.isInteger(normalized.slidingPrecision) || normalized.slidingPrecision <= 0) {
48
+ throw new Error("slidingPrecision must be a positive integer");
49
+ }
46
50
  if (normalized.burst < normalized.limit) {
47
51
  throw new Error("burst must be >= limit");
48
52
  }
@@ -484,21 +488,22 @@ var RateLimiter = class {
484
488
  return decision2;
485
489
  }
486
490
  const store = this.store;
487
- let algorithm = this.algorithmCache.get(policy.name);
491
+ const cacheKey = `${policy.name}|${policy.algorithm}|${policy.limit}|${policy.window}|${policy.burst}|${policy.slidingPrecision}`;
492
+ let algorithm = this.algorithmCache.get(cacheKey);
488
493
  if (!algorithm) {
489
494
  if (policy.algorithm === "token_bucket" /* TOKEN_BUCKET */) {
490
495
  algorithm = new TokenBucket(policy.burst, policy.limit, policy.window);
491
496
  } else if (policy.algorithm === "fixed_window" /* FIXED_WINDOW */) {
492
497
  algorithm = new FixedWindow(policy.limit, policy.window);
493
498
  } else if (policy.algorithm === "sliding_window" /* SLIDING_WINDOW */) {
494
- algorithm = new SlidingWindow(policy.limit, policy.window);
499
+ algorithm = new SlidingWindow(policy.limit, policy.window, policy.slidingPrecision);
495
500
  } else if (policy.algorithm === "leaky_bucket" /* LEAKY_BUCKET */) {
496
501
  const leakRate = policy.limit / policy.window;
497
502
  algorithm = new LeakyBucket(policy.burst, leakRate, policy.window);
498
503
  } else {
499
504
  throw new Error(`Algorithm ${policy.algorithm} not implemented`);
500
505
  }
501
- this.algorithmCache.set(policy.name, algorithm);
506
+ this.algorithmCache.set(cacheKey, algorithm);
502
507
  }
503
508
  const span = this.otelTracer?.startSpan?.("halt.check", { attributes: { policy: policy.name, key } });
504
509
  const state = store.get(storageKey);
@@ -1093,6 +1098,70 @@ function getPlanPolicy(planName) {
1093
1098
  return PLAN_TIERS[normalized];
1094
1099
  }
1095
1100
 
1101
+ // src/core/registry.ts
1102
+ var PolicyRegistry = class {
1103
+ constructor(initial = []) {
1104
+ this.policies = /* @__PURE__ */ new Map();
1105
+ for (const p of initial) this.register(p);
1106
+ }
1107
+ /** Add or replace a policy (keyed by `policy.name`). */
1108
+ register(policy) {
1109
+ this.policies.set(policy.name, policy);
1110
+ return this;
1111
+ }
1112
+ get(name) {
1113
+ return this.policies.get(name);
1114
+ }
1115
+ has(name) {
1116
+ return this.policies.has(name);
1117
+ }
1118
+ /** Mutate fields of an existing policy at runtime (e.g. `{ limit: 500 }`). */
1119
+ update(name, patch) {
1120
+ const existing = this.policies.get(name);
1121
+ if (!existing) throw new Error(`Unknown policy: ${name}`);
1122
+ const updated = { ...existing, ...patch, name: existing.name };
1123
+ if (patch.limit !== void 0 && patch.burst === void 0) {
1124
+ delete updated.burst;
1125
+ }
1126
+ this.policies.set(name, updated);
1127
+ return updated;
1128
+ }
1129
+ remove(name) {
1130
+ return this.policies.delete(name);
1131
+ }
1132
+ list() {
1133
+ return [...this.policies.values()];
1134
+ }
1135
+ /**
1136
+ * Build a resolver for the limiter's `policy` option.
1137
+ * @param selector maps a request to a registered policy name.
1138
+ */
1139
+ resolver(selector) {
1140
+ return (request) => {
1141
+ const name = selector(request);
1142
+ const policy = this.policies.get(name);
1143
+ if (!policy) throw new Error(`Unknown policy: ${name}`);
1144
+ return policy;
1145
+ };
1146
+ }
1147
+ };
1148
+ function cachedPolicyResolver(loader, options = {}) {
1149
+ const ttlMs = options.ttlMs ?? 5e3;
1150
+ const keyFn = options.key ?? (() => "__default__");
1151
+ const cache = /* @__PURE__ */ new Map();
1152
+ return async (request) => {
1153
+ const key = keyFn(request);
1154
+ const now = Date.now();
1155
+ const hit = cache.get(key);
1156
+ if (hit && hit.expires > now) {
1157
+ return hit.policy;
1158
+ }
1159
+ const policy = await loader(request);
1160
+ cache.set(key, { policy, expires: now + ttlMs });
1161
+ return policy;
1162
+ };
1163
+ }
1164
+
1096
1165
  // src/core/quota.ts
1097
1166
  var QuotaPeriod = /* @__PURE__ */ ((QuotaPeriod2) => {
1098
1167
  QuotaPeriod2["HOURLY"] = "hourly";
@@ -1611,6 +1680,6 @@ var OpenTelemetryMetrics = class {
1611
1680
  }
1612
1681
  };
1613
1682
 
1614
- export { Algorithm, CompositeTelemetry, InMemoryStore, KeyStrategy, LoggingTelemetry, MetricsTelemetry, OpenTelemetryMetrics, PENALTY_LENIENT, PENALTY_MODERATE, PENALTY_STRICT, PenaltyManager, QUOTA_ENTERPRISE_MONTHLY, QUOTA_FREE_DAILY, QUOTA_FREE_MONTHLY, QUOTA_PRO_DAILY, QUOTA_PRO_MONTHLY, QuotaManager, QuotaPeriod, RateLimiter, RedisStore, StatsCollector, extractors_exports as extractors, isAtomicStore, normalizePolicy, presets_exports as presets, toHeaders };
1683
+ export { Algorithm, CompositeTelemetry, InMemoryStore, KeyStrategy, LoggingTelemetry, MetricsTelemetry, OpenTelemetryMetrics, PENALTY_LENIENT, PENALTY_MODERATE, PENALTY_STRICT, PenaltyManager, PolicyRegistry, QUOTA_ENTERPRISE_MONTHLY, QUOTA_FREE_DAILY, QUOTA_FREE_MONTHLY, QUOTA_PRO_DAILY, QUOTA_PRO_MONTHLY, QuotaManager, QuotaPeriod, RateLimiter, RedisStore, StatsCollector, cachedPolicyResolver, extractors_exports as extractors, isAtomicStore, normalizePolicy, presets_exports as presets, toHeaders };
1615
1684
  //# sourceMappingURL=index.mjs.map
1616
1685
  //# sourceMappingURL=index.mjs.map