@lunora/ratelimit 1.0.0-alpha.139 → 1.0.0-alpha.140

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.d.mts CHANGED
@@ -92,6 +92,18 @@ interface RateLimitStore {
92
92
  * (`createDbStore`: every call is awaited I/O) must have it.
93
93
  */
94
94
  setIf?: (storageKey: string, value: RateLimitValue, prior: RateLimitValue | undefined) => Promise<boolean>;
95
+ /**
96
+ * Optional eviction hint, called by the limiter after each successful write.
97
+ * Examine up to `limit` entries from the store's oldest end and remove those
98
+ * whose `ts` is at or before `olderThanTs`, except entries with a negative
99
+ * `value`: a reserved debt is kept, whatever its age. `limit` caps entries
100
+ * examined, not entries removed. Omit it when the store's rows must persist.
101
+ *
102
+ * `owns` says whether a storage key belongs to the limiter asking for the
103
+ * sweep. A store shared with another limiter must leave the other limiter's
104
+ * keys alone: its horizon is not this one's. Absent means every key is owned.
105
+ */
106
+ sweep?: (olderThanTs: number, limit: number, owns?: (storageKey: string) => boolean) => void;
95
107
  }
96
108
  /** Inputs to {@link evaluate}. */
97
109
  interface EvaluateOptions {
@@ -162,7 +174,10 @@ declare class RateLimiter<Names extends string = string> {
162
174
  private readonly denyList;
163
175
  private readonly normalize;
164
176
  private readonly now;
177
+ private readonly retention;
165
178
  private readonly store;
179
+ /** Whether a stored key is one of this limiter's names; a sweep only evicts those. */
180
+ private readonly owns;
166
181
  constructor(options: RateLimiterOptions<Names>);
167
182
  /** Peek at whether a request would be permitted without consuming. */
168
183
  check(name: Names, args?: Omit<RateLimitArgs, "reserve" | "throws">): Promise<RateLimitStatus>;
@@ -251,6 +266,9 @@ declare const rateLimit: <Context>(limiter: LimiterResolver<Context>, name: stri
251
266
  * In-memory store. State lives for the lifetime of the process (or, inside a
252
267
  * Durable Object, the instance) — adequate for single-DO limits but not shared
253
268
  * across instances. Use {@link createSqlStore} for durable per-DO state.
269
+ *
270
+ * A store may be shared by several limiters: each sweep evicts only the keys its
271
+ * limiter owns, by that limiter's horizon (see `RateLimitStore.sweep`).
254
272
  */
255
273
  declare const createMemoryStore: () => RateLimitStore;
256
274
  /**
package/dist/index.d.ts CHANGED
@@ -92,6 +92,18 @@ interface RateLimitStore {
92
92
  * (`createDbStore`: every call is awaited I/O) must have it.
93
93
  */
94
94
  setIf?: (storageKey: string, value: RateLimitValue, prior: RateLimitValue | undefined) => Promise<boolean>;
95
+ /**
96
+ * Optional eviction hint, called by the limiter after each successful write.
97
+ * Examine up to `limit` entries from the store's oldest end and remove those
98
+ * whose `ts` is at or before `olderThanTs`, except entries with a negative
99
+ * `value`: a reserved debt is kept, whatever its age. `limit` caps entries
100
+ * examined, not entries removed. Omit it when the store's rows must persist.
101
+ *
102
+ * `owns` says whether a storage key belongs to the limiter asking for the
103
+ * sweep. A store shared with another limiter must leave the other limiter's
104
+ * keys alone: its horizon is not this one's. Absent means every key is owned.
105
+ */
106
+ sweep?: (olderThanTs: number, limit: number, owns?: (storageKey: string) => boolean) => void;
95
107
  }
96
108
  /** Inputs to {@link evaluate}. */
97
109
  interface EvaluateOptions {
@@ -162,7 +174,10 @@ declare class RateLimiter<Names extends string = string> {
162
174
  private readonly denyList;
163
175
  private readonly normalize;
164
176
  private readonly now;
177
+ private readonly retention;
165
178
  private readonly store;
179
+ /** Whether a stored key is one of this limiter's names; a sweep only evicts those. */
180
+ private readonly owns;
166
181
  constructor(options: RateLimiterOptions<Names>);
167
182
  /** Peek at whether a request would be permitted without consuming. */
168
183
  check(name: Names, args?: Omit<RateLimitArgs, "reserve" | "throws">): Promise<RateLimitStatus>;
@@ -251,6 +266,9 @@ declare const rateLimit: <Context>(limiter: LimiterResolver<Context>, name: stri
251
266
  * In-memory store. State lives for the lifetime of the process (or, inside a
252
267
  * Durable Object, the instance) — adequate for single-DO limits but not shared
253
268
  * across instances. Use {@link createSqlStore} for durable per-DO state.
269
+ *
270
+ * A store may be shared by several limiters: each sweep evicts only the keys its
271
+ * limiter owns, by that limiter's horizon (see `RateLimitStore.sweep`).
254
272
  */
255
273
  declare const createMemoryStore: () => RateLimitStore;
256
274
  /**
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-BSKYrouo.mjs";import{default as i}from"./packem_shared/dbRateLimit-B1Jf_kBi.mjs";import{default as l}from"./packem_shared/RateLimitError-9Y5OLxpl.mjs";import{rateLimit as x}from"./packem_shared/rateLimit-CWZur6XE.mjs";import{ratelimitPlugin as c}from"./packem_shared/ratelimitPlugin-CFnDs9rj.mjs";import{RateLimiter as u}from"./packem_shared/RateLimiter-Cr80iRjQ.mjs";import{createDbStore as b,createMemoryStore as n,createReadOnlyDbStore as L,createSqlStore as s}from"./packem_shared/createDbStore-k6ol27eT.mjs";import{tokenBudget as v}from"./packem_shared/tokenBudget-BRlWn1im.mjs";const e="0.0.0";export{l as RateLimitError,u as RateLimiter,e as VERSION,o as availableAt,b as createDbStore,n as createMemoryStore,L as createReadOnlyDbStore,s as createSqlStore,i as dbRateLimit,a as evaluate,x as rateLimit,c as ratelimitPlugin,v as tokenBudget};
1
+ import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-BhJ1lMBZ.mjs";import{default as i}from"./packem_shared/dbRateLimit-BgxKsJmX.mjs";import{default as l}from"./packem_shared/RateLimitError-9Y5OLxpl.mjs";import{rateLimit as x}from"./packem_shared/rateLimit-CWZur6XE.mjs";import{ratelimitPlugin as c}from"./packem_shared/ratelimitPlugin-CFnDs9rj.mjs";import{RateLimiter as u}from"./packem_shared/RateLimiter-CCSZUABq.mjs";import{createDbStore as b,createMemoryStore as n,createReadOnlyDbStore as L,createSqlStore as s}from"./packem_shared/createDbStore-Dma2vpJG.mjs";import{tokenBudget as v}from"./packem_shared/tokenBudget-CvkodZ0R.mjs";const e="0.0.0";export{l as RateLimitError,u as RateLimiter,e as VERSION,o as availableAt,b as createDbStore,n as createMemoryStore,L as createReadOnlyDbStore,s as createSqlStore,i as dbRateLimit,a as evaluate,x as rateLimit,c as ratelimitPlugin,v as tokenBudget};
@@ -0,0 +1 @@
1
+ import{LunoraError as h}from"@lunora/errors";import{retentionMs as p,availableAt as y,evaluate as v}from"./availableAt-BhJ1lMBZ.mjs";import f from"./RateLimitError-9Y5OLxpl.mjs";import{createMemoryStore as I}from"./createDbStore-Dma2vpJG.mjs";const b=/^[\d.:a-f]+$/iu,d=r=>{const t=r.indexOf("%"),e=t===-1?r:r.slice(0,t);if(!e.includes(":")||!b.test(e))return r;let s;try{s=new URL(`http://[${e}]/`).hostname.slice(1,-1)}catch{return r}const[i="",n]=s.split("::"),a=i===""?[]:i.split(":"),l=n===void 0||n===""?[]:n.split(":"),c=[...a,...Array.from({length:8-a.length-l.length},()=>"0"),...l].map(o=>Number.parseInt(o,16));if(c.slice(0,5).every(o=>o===0)&&c[5]===65535){const[o=0,m=0]=c.slice(6);return[o>>8,o&255,m>>8,m&255].join(".")}return`${c.slice(0,4).map(o=>o.toString(16)).join(":")}::/64`},u=(r,t)=>t===void 0?encodeURIComponent(r):`${encodeURIComponent(r)}:${encodeURIComponent(t)}`,N=16,w=32,g=r=>typeof r?.then=="function",L=r=>{let t=Number.NEGATIVE_INFINITY;return()=>(t=Math.max(t,r()),t)},R=r=>r?t=>d(r(t)):d,T="@lunora/ratelimit: new RateLimiter() was built with no explicit `store`, so it falls back to `createMemoryStore()` — an in-process Map. That Map is durable and correctly shared only for calls that land on the same Durable Object instance; it is NOT shared across `.shardBy(...)` shards or `.global()` replicas, and it resets when the DO instance is evicted/restarted. For a limit that must hold across any of those, pass a durable `store` (`createDbStore` or `createSqlStore`) instead. Pass `store: createMemoryStore()` explicitly once you've confirmed the in-memory default is correct here, to silence this warning.";class M{config;denyList;normalize;now;retention;store;owns;constructor(t){this.config=structuredClone(t.config);const e=new Set(Object.keys(this.config).map(s=>encodeURIComponent(s)));this.owns=s=>e.has(s.split(":")[0]??""),this.normalize=R(t.normalize),this.denyList=new Set([...t.denyList??[]].flatMap(s=>[s,this.normalize(s)])),this.now=L(t.now??Date.now),t.store===void 0&&console.warn(T),this.store=t.store??I(),this.retention=p(Object.values(this.config));for(const[s,i]of Object.entries(this.config)){if(Object.hasOwn(i,"shards"))throw new h("INTERNAL",`rate limit "${s}": \`shards\` is no longer supported — remove it; the limit now enforces its full \`rate\` (lower \`rate\` if you raised it to offset the split)`);if(!Number.isFinite(i.period)||i.period<=0)throw new h("INTERNAL",`rate limit "${s}": period must be a positive number`);if(!Number.isFinite(i.rate)||i.rate<=0)throw new h("INTERNAL",`rate limit "${s}": rate must be a positive number`);if(i.capacity!==void 0&&(!Number.isFinite(i.capacity)||i.capacity<0))throw new h("INTERNAL",`rate limit "${s}": capacity must be a non-negative number`)}}async check(t,e={}){return this.run(t,e,!1)}async getValue(t,e={}){const s=this.resolve(t),i=this.now(),n=u(t,this.normalizeKey(e.key)),a=y(s,await this.store.get(n),i);return{config:s,ts:a.ts,value:a.value}}async limit(t,e={}){return this.run(t,e,!0)}async reset(t,e={}){this.resolve(t),await Promise.resolve(this.store.delete(u(t,this.normalizeKey(e.key))))}async decide(t,e,s){for(let i=0;i<N;i+=1){const n=this.store.get(e),a=g(n)?await n:n,l=this.now(),{status:c,value:o}=v(t,a,{...s,now:l});if(o===void 0)return c;if(this.store.setIf===void 0)return await this.store.set(e,o),this.store.sweep?.(l-this.retention,w,this.owns),c;if(await this.store.setIf(e,o,a))return this.store.sweep?.(l-this.retention,w,this.owns),c}return{ok:!1,reason:"rate",retryAfter:0}}normalizeKey(t){return t===void 0?void 0:this.normalize(t)}resolve(t){const e=this.config[t];if(!e)throw new h("INTERNAL",`rate limit "${t}" is not configured`);return e}async run(t,e,s){const i=this.resolve(t),n=this.normalizeKey(e.key);if(n!==void 0&&(this.denyList.has(n)||this.denyList.has(e.key))){const o={ok:!1,reason:"deny",retryAfter:Number.POSITIVE_INFINITY};if(e.throws)throw new f(o);return o}const a=e.count??1;if(!Number.isInteger(a)||a<=0)throw new h("INTERNAL",`rate limit "${t}": count must be a positive integer`);const l=u(t,n),c=await this.decide(i,l,{consume:s,count:a,reserve:e.reserve??!1});if(!c.ok&&e.throws)throw new f(c);return c}}export{M as RateLimiter};
@@ -0,0 +1 @@
1
+ import{LunoraError as b}from"@lunora/errors";const o=t=>t.capacity??t.rate,S=t=>t.kind==="sliding window"?t.rate:o(t),i=(t,a)=>{throw new b("INTERNAL",`@lunora/ratelimit: requested count ${String(t)} exceeds the limiter capacity ${String(a)}`)},M=(t,a,e)=>{const u=o(t),r=t.rate/t.period,s=a??{ts:e,value:u},c=Math.max(0,e-s.ts);return{available:Math.min(u,s.value+c*r),capacity:u,ratePerMs:r}},w=(t,a,e)=>{const u=t.start??0,r=u+Math.floor((e-u)/t.period)*t.period;if(!a)return{ts:r,value:o(t)};if(a.ts<r){let s=0,c=1;return(a.value<0||t.capacity!==void 0)&&(s=a.value,c=Math.max(1,Math.ceil((r-a.ts)/t.period))),{ts:r,value:Math.min(o(t),s+c*t.rate)}}return{ts:a.ts,value:a.value}},x=(t,a,e)=>{const u=t.start??0,r=u+Math.floor((e-u)/t.period)*t.period,s=e-r,c=(t.period-s)/t.period;let n=0,l=0;return a?.ts===r?(n=a.prev??0,l=a.value):a?.ts===r-t.period&&(n=a.value),{currentCount:l,elapsed:s,previousCount:n,weight:c,windowStart:r}},W=(t,a,e)=>{const{available:u,capacity:r,ratePerMs:s}=M(t,a,e.now);if(u>=e.count){const l={ts:e.now,value:u-e.count};return{status:{ok:!0,retryAfter:0},value:e.consume?l:void 0}}const c=e.count-u,n=Math.ceil(c/s);return e.consume&&e.reserve&&e.count<=r?{status:{ok:!0,retryAfter:n},value:{ts:e.now,value:u-e.count}}:(e.count>r&&i(e.count,r),{status:{ok:!1,reason:"rate",retryAfter:n},value:void 0})},y=(t,a,e,u,r,s)=>{const c=d=>w(t,a,e+d*t.period).value>=r;let l=u<0||t.capacity!==void 0?Math.max(1,Math.ceil((r-u)/t.rate)):1;for(let d=0;d<4&&l>1&&c(l-1);d+=1)l-=1;for(let d=0;d<4&&!c(l);d+=1)l+=1;return e+l*t.period-s},N=(t,a,e)=>{const u=o(t),r=w(t,a,e.now);if(r.value>=e.count){const s={ts:r.ts,value:r.value-e.count};return{status:{ok:!0,retryAfter:0},value:e.consume?s:void 0}}if(e.consume&&e.reserve&&e.count<=u){const s=r.value-e.count;return{status:{ok:!0,retryAfter:y(t,{ts:r.ts,value:s},r.ts,s,0,e.now)},value:{ts:r.ts,value:s}}}return e.count>u&&i(e.count,u),{status:{ok:!1,reason:"rate",retryAfter:y(t,a,r.ts,r.value,e.count,e.now)},value:void 0}},j=(t,a,e)=>{const u=t.rate,{period:r}=t,{currentCount:s,elapsed:c,previousCount:n,weight:l,windowStart:d}=x(t,a,e.now),m=n*l+s+e.count<=u,h=v=>{const k=u-v-e.count;if(n>0&&k>=0)return Math.ceil(r-c-k*r/n);const A=u-e.count,C=v>0?Math.max(0,r-A*r/v):0;return Math.ceil(r-c+C)};if(m||e.consume&&e.reserve&&e.count<=u){const v={prev:n,ts:d,value:s+e.count};return{status:{ok:!0,retryAfter:m?0:h(v.value)},value:e.consume?v:void 0}}return e.count>u&&i(e.count,u),{status:{ok:!1,reason:"rate",retryAfter:h(s)},value:void 0}},B=(t,a,e)=>{if(t.kind==="token bucket")return{ts:e,value:M(t,a,e).available};if(t.kind==="sliding window"){const{currentCount:u,previousCount:r,weight:s,windowStart:c}=x(t,a,e);return{ts:c,value:Math.max(0,t.rate-(r*s+u))}}return w(t,a,e)},L=t=>Math.max(0,...t.map(a=>a.period*(2+S(a)/a.rate))),P=(t,a,e)=>t.kind==="token bucket"?W(t,a,e):t.kind==="sliding window"?j(t,a,e):N(t,a,e);export{B as availableAt,S as enforcedCapacity,P as evaluate,L as retentionMs};
@@ -0,0 +1 @@
1
+ import{LunoraError as E}from"@lunora/errors";const m=()=>{const r=new Map;return{delete:e=>{r.delete(e)},get:e=>r.get(e),set:(e,c)=>{r.delete(e),r.set(e,c)},sweep:(e,c,s)=>{let n=0;for(const[l,o]of r){if(n>=c)break;n+=1,r.delete(l),(o.value<0||o.ts>e||s?.(l)===!1)&&r.set(l,o)}}}},u=(r,e,...c)=>r.exec.call(r,e,...c).toArray(),L=r=>{const{sql:e}=r,c=r.table??"_lunora_rate_limits";return u(e,`CREATE TABLE IF NOT EXISTS "${c}" (k TEXT PRIMARY KEY, value REAL NOT NULL, ts INTEGER NOT NULL, prev REAL)`),{delete:s=>{u(e,`DELETE FROM "${c}" WHERE k = ?`,s)},get:s=>{const l=u(e,`SELECT value, ts, prev FROM "${c}" WHERE k = ?`,s)[0];if(!l)return;const o={ts:l.ts,value:l.value};return l.prev!==null&&(o.prev=l.prev),o},set:(s,n)=>{u(e,`INSERT INTO "${c}" (k, value, ts, prev) VALUES (?, ?, ?, ?) ON CONFLICT(k) DO UPDATE SET value = excluded.value, ts = excluded.ts, prev = excluded.prev`,s,n.value,n.ts,n.prev??null)}}},b=r=>r.toString(16).padStart(2,"0"),S=async(r,e)=>{const s=[...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(`${r}\0${e}`))).subarray(0,16)];s[6]=128+s[6]%16,s[8]=128+s[8]%64;const n=s.map(l=>b(l)).join("");return`${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`},T=r=>r?.code==="CONFLICT",h=r=>{const{db:e}=r,c=r.table??"rateLimits",s=r.keyField??"key",n=new Map,l=async t=>{const a=await e.findFirst(c,{where:{[s]:t}});return n.set(t,a?a._id:void 0),a},o=async t=>(n.has(t)||await l(t),n.get(t)),v=(t,a)=>{const i={[s]:t,ts:a.ts,value:a.value};return a.prev!==void 0&&(i.prev=a.prev),i},f=async(t,a)=>{try{return n.set(t,await e.insert(c,a,{clientId:await S(c,t)})),!0}catch(i){if(T(i))return n.delete(t),!1;throw i}};return{delete:async t=>{const a=await o(t);a!==void 0&&await e.delete(a),n.delete(t)},get:async t=>{const a=await l(t);if(!a)return;const i={ts:a.ts,value:a.value};return a.prev!==null&&a.prev!==void 0&&(i.prev=a.prev),i},set:async(t,a)=>{const i=v(t,a);let d=await o(t);if(d===void 0){if(await f(t,i))return;await l(t),d=n.get(t)}d!==void 0&&await e.patchIf(c,d,{patch:i,where:{}})},setIf:async(t,a,i)=>{const d=v(t,a);if(i===void 0)return f(t,d);const w=await o(t);if(w===void 0)return!1;const{applied:p}=await e.patchIf(c,w,{patch:d,where:{prev:i.prev??null,ts:i.ts,value:i.value}});return p||n.delete(t),p}}},R=r=>{const e=s=>{throw new E("INTERNAL",`@lunora/ratelimit: \`${s}\` needs a writable \`ctx.db\`, but this store was created with \`createReadOnlyDbStore\` (a query context). Use \`createDbStore\` from a mutation or action; a query can only call \`getValue\`/\`check\`.`)},c=h({...r,db:{delete:()=>e("delete"),findFirst:r.db.findFirst.bind(r.db),insert:()=>e("insert"),patchIf:()=>e("patchIf")}});return{delete:()=>e("reset"),get:c.get,set:()=>e("limit"),setIf:()=>e("limit")}};export{h as createDbStore,m as createMemoryStore,R as createReadOnlyDbStore,L as createSqlStore};
@@ -1 +1 @@
1
- import{rateLimit as o}from"./rateLimit-CWZur6XE.mjs";import{RateLimiter as m}from"./RateLimiter-Cr80iRjQ.mjs";import{createDbStore as i}from"./createDbStore-k6ol27eT.mjs";const f=(e,r,t={})=>o(a=>new m({config:e,store:i({db:a.db,...t.store})}),r,t);export{f as default};
1
+ import{rateLimit as o}from"./rateLimit-CWZur6XE.mjs";import{RateLimiter as m}from"./RateLimiter-CCSZUABq.mjs";import{createDbStore as i}from"./createDbStore-Dma2vpJG.mjs";const f=(e,r,t={})=>o(a=>new m({config:e,store:i({db:a.db,...t.store})}),r,t);export{f as default};
@@ -1 +1 @@
1
- import{enforcedCapacity as n}from"./availableAt-BSKYrouo.mjs";const h=(t,r)=>({check:async c=>t.check(r,{key:c}),record:async(c,a)=>{if(!Number.isFinite(a)||a<=0)return t.check(r,{key:c});const{config:e}=await t.getValue(r,{key:c}),o=Math.max(1,Math.floor(n(e)));return t.limit(r,{count:Math.min(Math.ceil(a),o),key:c,reserve:!0})}});export{h as tokenBudget};
1
+ import{enforcedCapacity as n}from"./availableAt-BhJ1lMBZ.mjs";const h=(t,r)=>({check:async c=>t.check(r,{key:c}),record:async(c,a)=>{if(!Number.isFinite(a)||a<=0)return t.check(r,{key:c});const{config:e}=await t.getValue(r,{key:c}),o=Math.max(1,Math.floor(n(e)));return t.limit(r,{count:Math.min(Math.ceil(a),o),key:c,reserve:!0})}});export{h as tokenBudget};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/ratelimit",
3
- "version": "1.0.0-alpha.139",
3
+ "version": "1.0.0-alpha.140",
4
4
  "description": "Rate limiting: token-bucket / fixed-window / sliding-window algorithms, deny list, pluggable stores, and procedure middleware",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- import{LunoraError as h}from"@lunora/errors";import{availableAt as w,evaluate as p}from"./availableAt-BSKYrouo.mjs";import m from"./RateLimitError-9Y5OLxpl.mjs";import{createMemoryStore as y}from"./createDbStore-k6ol27eT.mjs";const v=/^[\d.:a-f]+$/iu,d=i=>{const t=i.indexOf("%"),e=t===-1?i:i.slice(0,t);if(!e.includes(":")||!v.test(e))return i;let r;try{r=new URL(`http://[${e}]/`).hostname.slice(1,-1)}catch{return i}const[c="",o]=r.split("::"),s=c===""?[]:c.split(":"),l=o===void 0||o===""?[]:o.split(":"),n=[...s,...Array.from({length:8-s.length-l.length},()=>"0"),...l].map(a=>Number.parseInt(a,16));if(n.slice(0,5).every(a=>a===0)&&n[5]===65535){const[a=0,f=0]=n.slice(6);return[a>>8,a&255,f>>8,f&255].join(".")}return`${n.slice(0,4).map(a=>a.toString(16)).join(":")}::/64`},u=(i,t)=>t===void 0?encodeURIComponent(i):`${encodeURIComponent(i)}:${encodeURIComponent(t)}`,b=16,N=i=>typeof i?.then=="function",g=i=>i?t=>d(i(t)):d,I="@lunora/ratelimit: new RateLimiter() was built with no explicit `store`, so it falls back to `createMemoryStore()` — an in-process Map. That Map is durable and correctly shared only for calls that land on the same Durable Object instance; it is NOT shared across `.shardBy(...)` shards or `.global()` replicas, and it resets when the DO instance is evicted/restarted. For a limit that must hold across any of those, pass a durable `store` (`createDbStore` or `createSqlStore`) instead. Pass `store: createMemoryStore()` explicitly once you've confirmed the in-memory default is correct here, to silence this warning.";class E{config;denyList;normalize;now;store;constructor(t){this.config=t.config,this.normalize=g(t.normalize),this.denyList=new Set([...t.denyList??[]].flatMap(e=>[e,this.normalize(e)])),this.now=t.now??Date.now,t.store===void 0&&console.warn(I),this.store=t.store??y();for(const[e,r]of Object.entries(this.config)){if(Object.hasOwn(r,"shards"))throw new h("INTERNAL",`rate limit "${e}": \`shards\` is no longer supported — remove it; the limit now enforces its full \`rate\` (lower \`rate\` if you raised it to offset the split)`);if(!Number.isFinite(r.period)||r.period<=0)throw new h("INTERNAL",`rate limit "${e}": period must be a positive number`);if(!Number.isFinite(r.rate)||r.rate<=0)throw new h("INTERNAL",`rate limit "${e}": rate must be a positive number`);if(r.capacity!==void 0&&(!Number.isFinite(r.capacity)||r.capacity<0))throw new h("INTERNAL",`rate limit "${e}": capacity must be a non-negative number`)}}async check(t,e={}){return this.run(t,e,!1)}async getValue(t,e={}){const r=this.resolve(t),c=this.now(),o=u(t,this.normalizeKey(e.key)),s=w(r,await this.store.get(o),c);return{config:r,ts:s.ts,value:s.value}}async limit(t,e={}){return this.run(t,e,!0)}async reset(t,e={}){this.resolve(t),await Promise.resolve(this.store.delete(u(t,this.normalizeKey(e.key))))}async decide(t,e,r){for(let c=0;c<b;c+=1){const o=this.store.get(e),s=N(o)?await o:o,{status:l,value:n}=p(t,s,{...r,now:this.now()});if(n===void 0)return l;if(this.store.setIf===void 0)return await this.store.set(e,n),l;if(await this.store.setIf(e,n,s))return l}return{ok:!1,reason:"rate",retryAfter:0}}normalizeKey(t){return t===void 0?void 0:this.normalize(t)}resolve(t){const e=this.config[t];if(!e)throw new h("INTERNAL",`rate limit "${t}" is not configured`);return e}async run(t,e,r){const c=this.resolve(t),o=this.normalizeKey(e.key);if(o!==void 0&&(this.denyList.has(o)||this.denyList.has(e.key))){const a={ok:!1,reason:"deny",retryAfter:Number.POSITIVE_INFINITY};if(e.throws)throw new m(a);return a}const s=e.count??1;if(!Number.isInteger(s)||s<=0)throw new h("INTERNAL",`rate limit "${t}": count must be a positive integer`);const l=u(t,o),n=await this.decide(c,l,{consume:r,count:s,reserve:e.reserve??!1});if(!n.ok&&e.throws)throw new m(n);return n}}export{E as RateLimiter};
@@ -1 +0,0 @@
1
- import{LunoraError as b}from"@lunora/errors";const o=e=>e.capacity??e.rate,E=e=>e.kind==="sliding window"?e.rate:o(e),i=(e,r)=>{throw new b("INTERNAL",`@lunora/ratelimit: requested count ${String(e)} exceeds the limiter capacity ${String(r)}`)},M=(e,r,t)=>{const u=o(e),a=e.rate/e.period,s=r??{ts:t,value:u},c=Math.max(0,t-s.ts);return{available:Math.min(u,s.value+c*a),capacity:u,ratePerMs:a}},w=(e,r,t)=>{const u=e.start??0,a=u+Math.floor((t-u)/e.period)*e.period;if(!r)return{ts:a,value:o(e)};if(r.ts<a){let s=0,c=1;return(r.value<0||e.capacity!==void 0)&&(s=r.value,c=Math.max(1,Math.ceil((a-r.ts)/e.period))),{ts:a,value:Math.min(o(e),s+c*e.rate)}}return{ts:r.ts,value:r.value}},x=(e,r,t)=>{const u=e.start??0,a=u+Math.floor((t-u)/e.period)*e.period,s=t-a,c=(e.period-s)/e.period;let n=0,l=0;return r?.ts===a?(n=r.prev??0,l=r.value):r?.ts===a-e.period&&(n=r.value),{currentCount:l,elapsed:s,previousCount:n,weight:c,windowStart:a}},S=(e,r,t)=>{const{available:u,capacity:a,ratePerMs:s}=M(e,r,t.now);if(u>=t.count){const l={ts:t.now,value:u-t.count};return{status:{ok:!0,retryAfter:0},value:t.consume?l:void 0}}const c=t.count-u,n=Math.ceil(c/s);return t.consume&&t.reserve&&t.count<=a?{status:{ok:!0,retryAfter:n},value:{ts:t.now,value:u-t.count}}:(t.count>a&&i(t.count,a),{status:{ok:!1,reason:"rate",retryAfter:n},value:void 0})},y=(e,r,t,u,a,s)=>{const c=d=>w(e,r,t+d*e.period).value>=a;let l=u<0||e.capacity!==void 0?Math.max(1,Math.ceil((a-u)/e.rate)):1;for(let d=0;d<4&&l>1&&c(l-1);d+=1)l-=1;for(let d=0;d<4&&!c(l);d+=1)l+=1;return t+l*e.period-s},W=(e,r,t)=>{const u=o(e),a=w(e,r,t.now);if(a.value>=t.count){const s={ts:a.ts,value:a.value-t.count};return{status:{ok:!0,retryAfter:0},value:t.consume?s:void 0}}if(t.consume&&t.reserve&&t.count<=u){const s=a.value-t.count;return{status:{ok:!0,retryAfter:y(e,{ts:a.ts,value:s},a.ts,s,0,t.now)},value:{ts:a.ts,value:s}}}return t.count>u&&i(t.count,u),{status:{ok:!1,reason:"rate",retryAfter:y(e,r,a.ts,a.value,t.count,t.now)},value:void 0}},N=(e,r,t)=>{const u=e.rate,{period:a}=e,{currentCount:s,elapsed:c,previousCount:n,weight:l,windowStart:d}=x(e,r,t.now),m=n*l+s+t.count<=u,h=v=>{const k=u-v-t.count;if(n>0&&k>=0)return Math.ceil(a-c-k*a/n);const A=u-t.count,C=v>0?Math.max(0,a-A*a/v):0;return Math.ceil(a-c+C)};if(m||t.consume&&t.reserve&&t.count<=u){const v={prev:n,ts:d,value:s+t.count};return{status:{ok:!0,retryAfter:m?0:h(v.value)},value:t.consume?v:void 0}}return t.count>u&&i(t.count,u),{status:{ok:!1,reason:"rate",retryAfter:h(s)},value:void 0}},B=(e,r,t)=>{if(e.kind==="token bucket")return{ts:t,value:M(e,r,t).available};if(e.kind==="sliding window"){const{currentCount:u,previousCount:a,weight:s,windowStart:c}=x(e,r,t);return{ts:c,value:Math.max(0,e.rate-(a*s+u))}}return w(e,r,t)},L=(e,r,t)=>e.kind==="token bucket"?S(e,r,t):e.kind==="sliding window"?N(e,r,t):W(e,r,t);export{B as availableAt,E as enforcedCapacity,L as evaluate};
@@ -1 +0,0 @@
1
- import{LunoraError as E}from"@lunora/errors";const m=()=>{const n=new Map;return{delete:t=>{n.delete(t)},get:t=>n.get(t),set:(t,c)=>{n.set(t,c)}}},u=(n,t,...c)=>n.exec.call(n,t,...c).toArray(),L=n=>{const{sql:t}=n,c=n.table??"_lunora_rate_limits";return u(t,`CREATE TABLE IF NOT EXISTS "${c}" (k TEXT PRIMARY KEY, value REAL NOT NULL, ts INTEGER NOT NULL, prev REAL)`),{delete:s=>{u(t,`DELETE FROM "${c}" WHERE k = ?`,s)},get:s=>{const l=u(t,`SELECT value, ts, prev FROM "${c}" WHERE k = ?`,s)[0];if(!l)return;const d={ts:l.ts,value:l.value};return l.prev!==null&&(d.prev=l.prev),d},set:(s,a)=>{u(t,`INSERT INTO "${c}" (k, value, ts, prev) VALUES (?, ?, ?, ?) ON CONFLICT(k) DO UPDATE SET value = excluded.value, ts = excluded.ts, prev = excluded.prev`,s,a.value,a.ts,a.prev??null)}}},b=n=>n.toString(16).padStart(2,"0"),S=async(n,t)=>{const s=[...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(`${n}\0${t}`))).subarray(0,16)];s[6]=128+s[6]%16,s[8]=128+s[8]%64;const a=s.map(l=>b(l)).join("");return`${a.slice(0,8)}-${a.slice(8,12)}-${a.slice(12,16)}-${a.slice(16,20)}-${a.slice(20)}`},T=n=>n?.code==="CONFLICT",h=n=>{const{db:t}=n,c=n.table??"rateLimits",s=n.keyField??"key",a=new Map,l=async e=>{const r=await t.findFirst(c,{where:{[s]:e}});return a.set(e,r?r._id:void 0),r},d=async e=>(a.has(e)||await l(e),a.get(e)),v=(e,r)=>{const i={[s]:e,ts:r.ts,value:r.value};return r.prev!==void 0&&(i.prev=r.prev),i},w=async(e,r)=>{try{return a.set(e,await t.insert(c,r,{clientId:await S(c,e)})),!0}catch(i){if(T(i))return a.delete(e),!1;throw i}};return{delete:async e=>{const r=await d(e);r!==void 0&&await t.delete(r),a.delete(e)},get:async e=>{const r=await l(e);if(!r)return;const i={ts:r.ts,value:r.value};return r.prev!==null&&r.prev!==void 0&&(i.prev=r.prev),i},set:async(e,r)=>{const i=v(e,r);let o=await d(e);if(o===void 0){if(await w(e,i))return;await l(e),o=a.get(e)}o!==void 0&&await t.patchIf(c,o,{patch:i,where:{}})},setIf:async(e,r,i)=>{const o=v(e,r);if(i===void 0)return w(e,o);const f=await d(e);if(f===void 0)return!1;const{applied:p}=await t.patchIf(c,f,{patch:o,where:{prev:i.prev??null,ts:i.ts,value:i.value}});return p||a.delete(e),p}}},R=n=>{const t=s=>{throw new E("INTERNAL",`@lunora/ratelimit: \`${s}\` needs a writable \`ctx.db\`, but this store was created with \`createReadOnlyDbStore\` (a query context). Use \`createDbStore\` from a mutation or action; a query can only call \`getValue\`/\`check\`.`)},c=h({...n,db:{delete:()=>t("delete"),findFirst:n.db.findFirst.bind(n.db),insert:()=>t("insert"),patchIf:()=>t("patchIf")}});return{delete:()=>t("reset"),get:c.get,set:()=>t("limit"),setIf:()=>t("limit")}};export{h as createDbStore,m as createMemoryStore,R as createReadOnlyDbStore,L as createSqlStore};