@lunora/ratelimit 1.0.0-alpha.140 → 1.0.0-alpha.141
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 +57 -6
- package/dist/index.d.ts +57 -6
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/{RateLimiter-CCSZUABq.mjs → RateLimiter-DUQPC6Cc.mjs} +1 -1
- package/dist/packem_shared/createDbStore-fSBhI7xO.mjs +1 -0
- package/dist/packem_shared/{dbRateLimit-BgxKsJmX.mjs → dbRateLimit-DYnFEJdq.mjs} +1 -1
- package/package.json +1 -1
- package/dist/packem_shared/createDbStore-Dma2vpJG.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -94,16 +94,20 @@ interface RateLimitStore {
|
|
|
94
94
|
setIf?: (storageKey: string, value: RateLimitValue, prior: RateLimitValue | undefined) => Promise<boolean>;
|
|
95
95
|
/**
|
|
96
96
|
* Optional eviction hint, called by the limiter after each successful write.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
97
|
+
* Remove up to `limit` entries whose `ts` is at or before `olderThanTs`, except
|
|
98
|
+
* entries with a negative `value`: a reserved debt is kept, whatever its age.
|
|
99
|
+
* The memory store examines up to `limit` entries from its oldest end; the
|
|
100
|
+
* durable stores delete at most `limit` matching rows. A returned promise is
|
|
101
|
+
* awaited, so a store whose sweep is I/O (`createDbStore`) finishes it before the
|
|
102
|
+
* consume returns. The durable stores implement it so a table stays bounded
|
|
103
|
+
* without a scheduled job; see `createSqlStore` and `createDbStore` for the
|
|
104
|
+
* table shape.
|
|
101
105
|
*
|
|
102
106
|
* `owns` says whether a storage key belongs to the limiter asking for the
|
|
103
107
|
* sweep. A store shared with another limiter must leave the other limiter's
|
|
104
108
|
* keys alone: its horizon is not this one's. Absent means every key is owned.
|
|
105
109
|
*/
|
|
106
|
-
sweep?: (olderThanTs: number, limit: number, owns?: (storageKey: string) => boolean) => void;
|
|
110
|
+
sweep?: (olderThanTs: number, limit: number, owns?: (storageKey: string) => boolean) => Promise<void> | void;
|
|
107
111
|
}
|
|
108
112
|
/** Inputs to {@link evaluate}. */
|
|
109
113
|
interface EvaluateOptions {
|
|
@@ -210,6 +214,12 @@ declare class RateLimiter<Names extends string = string> {
|
|
|
210
214
|
* than admitted.
|
|
211
215
|
*/
|
|
212
216
|
private decide;
|
|
217
|
+
/**
|
|
218
|
+
* Evict what this limiter's horizon has expired, after a write has landed. The
|
|
219
|
+
* write is already done, so awaiting here does not reopen the read-decide-write
|
|
220
|
+
* window. Only a promise-returning sweep (`createDbStore`) is awaited for I/O.
|
|
221
|
+
*/
|
|
222
|
+
private sweep;
|
|
213
223
|
private normalizeKey;
|
|
214
224
|
private resolve;
|
|
215
225
|
private run;
|
|
@@ -289,6 +299,15 @@ interface SqlStoreOptions {
|
|
|
289
299
|
* SQLite-backed store for durable, per-DO rate-limit state. Persists each
|
|
290
300
|
* `(name, key)` pair as one row so limits survive hibernation and eviction.
|
|
291
301
|
*
|
|
302
|
+
* **Retention:** the limiter calls `sweep` after each write, which deletes up to 32
|
|
303
|
+
* rows older than its horizon. The index on `ts` covers only rows with a
|
|
304
|
+
* non-negative `value`, so a reserved debt (never swept) adds nothing to the scan.
|
|
305
|
+
* The table holds the live keys plus the expired ones not yet swept. Limiters may
|
|
306
|
+
* share the table: each sweep deletes only the keys its own limiter owns, so a
|
|
307
|
+
* shorter-window limiter cannot evict a longer-window one's rows. A sweep reads
|
|
308
|
+
* candidates by `ts` before filtering by owner, so a long run of another limiter's
|
|
309
|
+
* expired rows can use up the window for a while.
|
|
310
|
+
*
|
|
292
311
|
* **Atomicity:** `exec` is synchronous and the limiter does not yield between a
|
|
293
312
|
* synchronous store's read and its write, so one consume's read, decision and
|
|
294
313
|
* write run as a single JavaScript step. No other call on the same isolate can
|
|
@@ -326,6 +345,18 @@ interface RateLimitDatabaseReader {
|
|
|
326
345
|
*/
|
|
327
346
|
interface RateLimitDatabase extends RateLimitDatabaseReader {
|
|
328
347
|
delete: <T extends string>(id: Id<T>) => Promise<void>;
|
|
348
|
+
/**
|
|
349
|
+
* Used by `sweep` only: rows matching `where`, at most `limit`. Optional in the
|
|
350
|
+
* type because the public `DatabaseWriter` does not declare it; the framework's
|
|
351
|
+
* `ctx.db` implements it, and `sweep` throws if an adapter does not.
|
|
352
|
+
*/
|
|
353
|
+
findMany?: (table: string, args: {
|
|
354
|
+
limit?: number;
|
|
355
|
+
orderBy?: Record<string, "asc" | "desc">[];
|
|
356
|
+
where?: Record<string, unknown>;
|
|
357
|
+
}) => Promise<{
|
|
358
|
+
page: Record<string, unknown>[];
|
|
359
|
+
}>;
|
|
329
360
|
insert: <T extends string>(table: T, document: Record<string, unknown>, options?: {
|
|
330
361
|
clientId?: string;
|
|
331
362
|
}) => Promise<Id<T>>;
|
|
@@ -364,9 +395,29 @@ interface ReadOnlyDatabaseStoreOptions extends DatabaseStoreLocation {
|
|
|
364
395
|
* ts: v.number(),
|
|
365
396
|
* value: v.number(),
|
|
366
397
|
* prev: v.optional(v.number()),
|
|
367
|
-
* })
|
|
398
|
+
* })
|
|
399
|
+
* .index("by_key", ["key"])
|
|
400
|
+
* .index("by_ts", ["ts"])
|
|
368
401
|
* ```
|
|
369
402
|
*
|
|
403
|
+
* **Table shape change:** `by_ts` is new. The sweep finds expired rows through it.
|
|
404
|
+
* A table declared without it still works, but each sweep scans every row. Apps
|
|
405
|
+
* that already declared the table add the index. The sweep deletes at most 32
|
|
406
|
+
* expired rows per write; a reserved debt (negative `value`) is never deleted.
|
|
407
|
+
* The ORM index cannot be partial, so debts stay in it: many old debts make each
|
|
408
|
+
* sweep read past them. Reserves are rare, so this is accepted.
|
|
409
|
+
*
|
|
410
|
+
* **Ceiling:** the sweep deletes only a row whose `ts`, `value` and `prev` still match
|
|
411
|
+
* what it read, but the framework resolves that match and deletes in two steps.
|
|
412
|
+
* On a `.global()` table another shard can rewrite the row between them, so the
|
|
413
|
+
* row is deleted live and its bucket resets. Closing that needs a conditional
|
|
414
|
+
* delete on `ctx.db`, which does not exist yet. A shard-local table is not exposed:
|
|
415
|
+
* the Durable Object's input gate holds across the sweep's storage operations.
|
|
416
|
+
*
|
|
417
|
+
* **Soft delete:** the sweep deletes with `ctx.db.delete`, which has no hard-delete
|
|
418
|
+
* option. Declare the table without `.softDelete()`: a tombstoned row keeps its
|
|
419
|
+
* deterministic id, so the next consume for that key conflicts forever.
|
|
420
|
+
*
|
|
370
421
|
* **Atomic against concurrent consumes.** `setIf` writes only if the row still
|
|
371
422
|
* holds the value `get` returned: an existing row through `ctx.db.patchIf` with
|
|
372
423
|
* that value as its `where`, a new row through an `insert` under an id derived
|
package/dist/index.d.ts
CHANGED
|
@@ -94,16 +94,20 @@ interface RateLimitStore {
|
|
|
94
94
|
setIf?: (storageKey: string, value: RateLimitValue, prior: RateLimitValue | undefined) => Promise<boolean>;
|
|
95
95
|
/**
|
|
96
96
|
* Optional eviction hint, called by the limiter after each successful write.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
97
|
+
* Remove up to `limit` entries whose `ts` is at or before `olderThanTs`, except
|
|
98
|
+
* entries with a negative `value`: a reserved debt is kept, whatever its age.
|
|
99
|
+
* The memory store examines up to `limit` entries from its oldest end; the
|
|
100
|
+
* durable stores delete at most `limit` matching rows. A returned promise is
|
|
101
|
+
* awaited, so a store whose sweep is I/O (`createDbStore`) finishes it before the
|
|
102
|
+
* consume returns. The durable stores implement it so a table stays bounded
|
|
103
|
+
* without a scheduled job; see `createSqlStore` and `createDbStore` for the
|
|
104
|
+
* table shape.
|
|
101
105
|
*
|
|
102
106
|
* `owns` says whether a storage key belongs to the limiter asking for the
|
|
103
107
|
* sweep. A store shared with another limiter must leave the other limiter's
|
|
104
108
|
* keys alone: its horizon is not this one's. Absent means every key is owned.
|
|
105
109
|
*/
|
|
106
|
-
sweep?: (olderThanTs: number, limit: number, owns?: (storageKey: string) => boolean) => void;
|
|
110
|
+
sweep?: (olderThanTs: number, limit: number, owns?: (storageKey: string) => boolean) => Promise<void> | void;
|
|
107
111
|
}
|
|
108
112
|
/** Inputs to {@link evaluate}. */
|
|
109
113
|
interface EvaluateOptions {
|
|
@@ -210,6 +214,12 @@ declare class RateLimiter<Names extends string = string> {
|
|
|
210
214
|
* than admitted.
|
|
211
215
|
*/
|
|
212
216
|
private decide;
|
|
217
|
+
/**
|
|
218
|
+
* Evict what this limiter's horizon has expired, after a write has landed. The
|
|
219
|
+
* write is already done, so awaiting here does not reopen the read-decide-write
|
|
220
|
+
* window. Only a promise-returning sweep (`createDbStore`) is awaited for I/O.
|
|
221
|
+
*/
|
|
222
|
+
private sweep;
|
|
213
223
|
private normalizeKey;
|
|
214
224
|
private resolve;
|
|
215
225
|
private run;
|
|
@@ -289,6 +299,15 @@ interface SqlStoreOptions {
|
|
|
289
299
|
* SQLite-backed store for durable, per-DO rate-limit state. Persists each
|
|
290
300
|
* `(name, key)` pair as one row so limits survive hibernation and eviction.
|
|
291
301
|
*
|
|
302
|
+
* **Retention:** the limiter calls `sweep` after each write, which deletes up to 32
|
|
303
|
+
* rows older than its horizon. The index on `ts` covers only rows with a
|
|
304
|
+
* non-negative `value`, so a reserved debt (never swept) adds nothing to the scan.
|
|
305
|
+
* The table holds the live keys plus the expired ones not yet swept. Limiters may
|
|
306
|
+
* share the table: each sweep deletes only the keys its own limiter owns, so a
|
|
307
|
+
* shorter-window limiter cannot evict a longer-window one's rows. A sweep reads
|
|
308
|
+
* candidates by `ts` before filtering by owner, so a long run of another limiter's
|
|
309
|
+
* expired rows can use up the window for a while.
|
|
310
|
+
*
|
|
292
311
|
* **Atomicity:** `exec` is synchronous and the limiter does not yield between a
|
|
293
312
|
* synchronous store's read and its write, so one consume's read, decision and
|
|
294
313
|
* write run as a single JavaScript step. No other call on the same isolate can
|
|
@@ -326,6 +345,18 @@ interface RateLimitDatabaseReader {
|
|
|
326
345
|
*/
|
|
327
346
|
interface RateLimitDatabase extends RateLimitDatabaseReader {
|
|
328
347
|
delete: <T extends string>(id: Id<T>) => Promise<void>;
|
|
348
|
+
/**
|
|
349
|
+
* Used by `sweep` only: rows matching `where`, at most `limit`. Optional in the
|
|
350
|
+
* type because the public `DatabaseWriter` does not declare it; the framework's
|
|
351
|
+
* `ctx.db` implements it, and `sweep` throws if an adapter does not.
|
|
352
|
+
*/
|
|
353
|
+
findMany?: (table: string, args: {
|
|
354
|
+
limit?: number;
|
|
355
|
+
orderBy?: Record<string, "asc" | "desc">[];
|
|
356
|
+
where?: Record<string, unknown>;
|
|
357
|
+
}) => Promise<{
|
|
358
|
+
page: Record<string, unknown>[];
|
|
359
|
+
}>;
|
|
329
360
|
insert: <T extends string>(table: T, document: Record<string, unknown>, options?: {
|
|
330
361
|
clientId?: string;
|
|
331
362
|
}) => Promise<Id<T>>;
|
|
@@ -364,9 +395,29 @@ interface ReadOnlyDatabaseStoreOptions extends DatabaseStoreLocation {
|
|
|
364
395
|
* ts: v.number(),
|
|
365
396
|
* value: v.number(),
|
|
366
397
|
* prev: v.optional(v.number()),
|
|
367
|
-
* })
|
|
398
|
+
* })
|
|
399
|
+
* .index("by_key", ["key"])
|
|
400
|
+
* .index("by_ts", ["ts"])
|
|
368
401
|
* ```
|
|
369
402
|
*
|
|
403
|
+
* **Table shape change:** `by_ts` is new. The sweep finds expired rows through it.
|
|
404
|
+
* A table declared without it still works, but each sweep scans every row. Apps
|
|
405
|
+
* that already declared the table add the index. The sweep deletes at most 32
|
|
406
|
+
* expired rows per write; a reserved debt (negative `value`) is never deleted.
|
|
407
|
+
* The ORM index cannot be partial, so debts stay in it: many old debts make each
|
|
408
|
+
* sweep read past them. Reserves are rare, so this is accepted.
|
|
409
|
+
*
|
|
410
|
+
* **Ceiling:** the sweep deletes only a row whose `ts`, `value` and `prev` still match
|
|
411
|
+
* what it read, but the framework resolves that match and deletes in two steps.
|
|
412
|
+
* On a `.global()` table another shard can rewrite the row between them, so the
|
|
413
|
+
* row is deleted live and its bucket resets. Closing that needs a conditional
|
|
414
|
+
* delete on `ctx.db`, which does not exist yet. A shard-local table is not exposed:
|
|
415
|
+
* the Durable Object's input gate holds across the sweep's storage operations.
|
|
416
|
+
*
|
|
417
|
+
* **Soft delete:** the sweep deletes with `ctx.db.delete`, which has no hard-delete
|
|
418
|
+
* option. Declare the table without `.softDelete()`: a tombstoned row keeps its
|
|
419
|
+
* deterministic id, so the next consume for that key conflicts forever.
|
|
420
|
+
*
|
|
370
421
|
* **Atomic against concurrent consumes.** `setIf` writes only if the row still
|
|
371
422
|
* holds the value `get` returned: an existing row through `ctx.db.patchIf` with
|
|
372
423
|
* that value as its `where`, a new row through an `insert` under an id derived
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-BhJ1lMBZ.mjs";import{default as i}from"./packem_shared/dbRateLimit-
|
|
1
|
+
import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-BhJ1lMBZ.mjs";import{default as i}from"./packem_shared/dbRateLimit-DYnFEJdq.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-DUQPC6Cc.mjs";import{createDbStore as b,createMemoryStore as n,createReadOnlyDbStore as L,createSqlStore as s}from"./packem_shared/createDbStore-fSBhI7xO.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};
|
|
@@ -1 +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-
|
|
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-fSBhI7xO.mjs";const b=/^[\d.:a-f]+$/iu,w=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,g=32,d=r=>typeof r?.then=="function",L=r=>{let t=Number.NEGATIVE_INFINITY;return()=>(t=Math.max(t,r()),t)},R=r=>r?t=>w(r(t)):w,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=d(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),await this.sweep(l),c;if(await this.store.setIf(e,o,a))return await this.sweep(l),c}return{ok:!1,reason:"rate",retryAfter:0}}async sweep(t){try{const e=this.store.sweep?.(t-this.retention,g,this.owns);d(e)&&await e}catch(e){console.warn("@lunora/ratelimit: the retention sweep failed; the consume still stands",e)}}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 L=()=>{const a=new Map;return{delete:e=>{a.delete(e)},get:e=>a.get(e),set:(e,r)=>{a.delete(e),a.set(e,r)},sweep:(e,r,c)=>{let s=0;for(const[l,o]of a){if(s>=r)break;s+=1,a.delete(l),(o.value<0||o.ts>e||c?.(l)===!1)&&a.set(l,o)}}}},v=(a,e,...r)=>a.exec.call(a,e,...r).toArray(),O=a=>{const{sql:e}=a,r=a.table??"_lunora_rate_limits";return v(e,`CREATE TABLE IF NOT EXISTS "${r}" (k TEXT PRIMARY KEY, value REAL NOT NULL, ts INTEGER NOT NULL, prev REAL)`),v(e,`CREATE INDEX IF NOT EXISTS "${r}_ts" ON "${r}" (ts) WHERE value >= 0`),{delete:c=>{v(e,`DELETE FROM "${r}" WHERE k = ?`,c)},sweep:(c,s,l)=>{const o=v(e,`SELECT k FROM "${r}" WHERE ts <= ? AND value >= 0 ORDER BY ts LIMIT ?`,c,s);for(const{k:f}of o)l?.(f)!==!1&&v(e,`DELETE FROM "${r}" WHERE k = ?`,f)},get:c=>{const l=v(e,`SELECT value, ts, prev FROM "${r}" WHERE k = ?`,c)[0];if(!l)return;const o={ts:l.ts,value:l.value};return l.prev!==null&&(o.prev=l.prev),o},set:(c,s)=>{v(e,`INSERT INTO "${r}" (k, value, ts, prev) VALUES (?, ?, ?, ?) ON CONFLICT(k) DO UPDATE SET value = excluded.value, ts = excluded.ts, prev = excluded.prev`,c,s.value,s.ts,s.prev??null)}}},T=a=>a.toString(16).padStart(2,"0"),h=async(a,e)=>{const c=[...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(`${a}\0${e}`))).subarray(0,16)];c[6]=128+c[6]%16,c[8]=128+c[8]%64;const s=c.map(l=>T(l)).join("");return`${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20)}`},I=a=>a?.code==="CONFLICT",R=a=>{const{db:e}=a,r=a.table??"rateLimits",c=a.keyField??"key",s=new Map;let l=!1;const o=async t=>{const n=await e.findFirst(r,{where:{[c]:t}});return s.set(t,n?n._id:void 0),n},f=async t=>(s.has(t)||await o(t),s.get(t)),E=(t,n)=>{const i={[c]:t,ts:n.ts,value:n.value};return n.prev!==void 0&&(i.prev=n.prev),i},p=async(t,n)=>{try{return s.set(t,await e.insert(r,n,{clientId:await h(r,t)})),!0}catch(i){if(I(i))return s.delete(t),!1;throw i}};return{delete:async t=>{const n=await f(t);n!==void 0&&await e.delete(n),s.delete(t)},get:async t=>{const n=await o(t);if(!n)return;const i={ts:n.ts,value:n.value};return n.prev!==null&&n.prev!==void 0&&(i.prev=n.prev),i},set:async(t,n)=>{const i=E(t,n);let u=await f(t);if(u===void 0){if(await p(t,i))return;await o(t),u=s.get(t)}u!==void 0&&await e.patchIf(r,u,{patch:i,where:{}})},setIf:async(t,n,i)=>{const u=E(t,n);if(i===void 0)return p(t,u);const w=await f(t);if(w===void 0)return!1;const{applied:d}=await e.patchIf(r,w,{patch:u,where:{prev:i.prev??null,ts:i.ts,value:i.value}});return d||s.delete(t),d},sweep:async(t,n,i)=>{if(e.findMany===void 0){l||(l=!0,console.warn(`@lunora/ratelimit: table "${r}" is not swept: the adapter has no findMany. Pass the framework's ctx.db.`));return}const{page:u}=await e.findMany(r,{limit:n,orderBy:[{ts:"asc"}],where:{ts:{lte:t},value:{gte:0}}}),w=u.filter(d=>i?.(String(d[c]))!==!1);await Promise.all(w.map(async d=>{await e.findFirst(r,{where:{_id:d._id,prev:d.prev??null,ts:d.ts,value:d.value}})!==null&&await e.delete(d._id)}))}}},y=a=>{const e=c=>{throw new b("INTERNAL",`@lunora/ratelimit: \`${c}\` 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\`.`)},r=R({...a,db:{delete:()=>e("delete"),findFirst:a.db.findFirst.bind(a.db),insert:()=>e("insert"),patchIf:()=>e("patchIf")}});return{delete:()=>e("reset"),get:r.get,set:()=>e("limit"),setIf:()=>e("limit")}};export{R as createDbStore,L as createMemoryStore,y as createReadOnlyDbStore,O as createSqlStore};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{rateLimit as o}from"./rateLimit-CWZur6XE.mjs";import{RateLimiter as m}from"./RateLimiter-
|
|
1
|
+
import{rateLimit as o}from"./rateLimit-CWZur6XE.mjs";import{RateLimiter as m}from"./RateLimiter-DUQPC6Cc.mjs";import{createDbStore as i}from"./createDbStore-fSBhI7xO.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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/ratelimit",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.141",
|
|
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 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};
|