{
  "version": 3,
  "sources": ["../../../../src/workers/ratelimit/ratelimit-object.worker.ts"],
  "sourcesContent": ["// Durable Object backing the emulated Ratelimit binding.\n//\n// Counters live in `state.storage` (SQLite on disk, see the `ratelimit` plugin)\n// rather than on the heap, because `workerd` evicts idle Durable Objects after\n// ~10s and heap state would take the counters with it, silently resetting the\n// limit part way through a window.\n//\n// Durable storage is still cleared by `deleteAllDurableObjects()`, so\n// vitest-pool-workers' `reset()` clears it for free \u2014 the same mechanism that\n// resets the KV, R2 and D1 simulators.\nimport { drain, get, MiniflareDurableObject, POST } from \"miniflare:shared\";\nimport type { RouteHandler, TypedSql } from \"miniflare:shared\";\n\ninterface LimitRequestBody {\n\tkey: string;\n\tlimit: number;\n\tperiod: number;\n}\n\ninterface LimitResult {\n\tsuccess: boolean;\n}\n\n// Counters are scoped to `(key, period)`, mirroring production: there, an entry\n// is identified by `(account_id, namespace, hash(key), bucket, bucket_start_ts)`\n// and both `bucket` and `bucket_start_ts` are derived from the period, so\n// differing periods never share a counter. See\n// https://gitlab.cfdata.org/cloudflare/egs/doppler/-/blob/main/doppler-lib/src/counts.rs\n//\n// Bindings that share a `namespace_id` still share a counter for a given key,\n// because in the normal case they also share a period. Were `period` left out of\n// the key, two such bindings configured with different periods would instead\n// overwrite each other's row on every call \u2014 each seeing a foreign epoch, so\n// each resetting the count to zero \u2014 and neither would ever limit anything.\n//\n// Note the limit itself is deliberately *not* part of the key, again matching\n// production, where it is a threshold applied to a shared count rather than part\n// of the counter's identity.\ntype BucketRow = {\n\tkey: string;\n\tperiod: number;\n\tepoch: number;\n\tcount: number;\n};\n\nconst SQL_SCHEMA = `\nCREATE TABLE IF NOT EXISTS _mf_ratelimit_buckets (\n  key TEXT NOT NULL,\n  period INTEGER NOT NULL,\n  epoch INTEGER NOT NULL,\n  count INTEGER NOT NULL,\n  PRIMARY KEY (key, period)\n);\n`;\n\nfunction sqlStmts(db: TypedSql) {\n\treturn {\n\t\tgetBucket: db.stmt<Pick<BucketRow, \"key\" | \"period\">, BucketRow>(\n\t\t\t`SELECT key, period, epoch, count FROM _mf_ratelimit_buckets\n        WHERE key = :key AND period = :period`\n\t\t),\n\t\tputBucket: db.stmt<BucketRow>(\n\t\t\t`INSERT OR REPLACE INTO _mf_ratelimit_buckets (key, period, epoch, count)\n        VALUES (:key, :period, :epoch, :count)`\n\t\t),\n\t\t// Windows are aligned to the wall clock, so every key sharing a period\n\t\t// rolls over at the same instant. Clearing them all together matches the\n\t\t// previous `#buckets.clear()` and stops the table growing without bound.\n\t\t// It has to stay scoped to the period, or rolling one period's window over\n\t\t// would discard the counters belonging to the other.\n\t\tdeleteExpired: db.stmt<Pick<BucketRow, \"period\" | \"epoch\">>(\n\t\t\t\"DELETE FROM _mf_ratelimit_buckets WHERE period = :period AND epoch != :epoch\"\n\t\t),\n\t};\n}\n\nexport class RateLimiterObject extends MiniflareDurableObject {\n\t#stmts?: ReturnType<typeof sqlStmts>;\n\tget stmts() {\n\t\tif (this.#stmts === undefined) {\n\t\t\tthis.db.exec(SQL_SCHEMA);\n\t\t\tthis.#stmts = sqlStmts(this.db);\n\t\t}\n\t\treturn this.#stmts;\n\t}\n\n\t@POST(\"/limit\")\n\tlimit: RouteHandler = async (req) => {\n\t\tconst { key, limit, period } = await req.json<LimitRequestBody>();\n\n\t\tconst epoch = Math.floor(Date.now() / (period * 1000));\n\t\tconst bucket = get(this.stmts.getBucket({ key, period }));\n\n\t\tlet count = 0;\n\t\tif (bucket !== undefined && bucket.epoch === epoch) {\n\t\t\tcount = bucket.count;\n\t\t} else {\n\t\t\tdrain(this.stmts.deleteExpired({ period, epoch }));\n\t\t}\n\n\t\tif (count >= limit) {\n\t\t\treturn Response.json({ success: false } satisfies LimitResult);\n\t\t}\n\t\tthis.stmts.putBucket({ key, period, epoch, count: count + 1 });\n\t\treturn Response.json({ success: true } satisfies LimitResult);\n\t};\n}\n"],
  "mappings": ";;;;;;;;;AAUA,SAAS,OAAO,KAAK,wBAAwB,YAAY;AAmCzD,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUnB,SAAS,SAAS,IAAc;AAC/B,SAAO;AAAA,IACN,WAAW,GAAG;AAAA,MACb;AAAA;AAAA,IAED;AAAA,IACA,WAAW,GAAG;AAAA,MACb;AAAA;AAAA,IAED;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,eAAe,GAAG;AAAA,MACjB;AAAA,IACD;AAAA,EACD;AACD;AAEO,IAAM,oBAAN,cAAgC,uBAAuB;AAAA,EAC7D;AAAA,EACA,IAAI,QAAQ;AACX,WAAI,KAAK,WAAW,WACnB,KAAK,GAAG,KAAK,UAAU,GACvB,KAAK,SAAS,SAAS,KAAK,EAAE,IAExB,KAAK;AAAA,EACb;AAAA,EAGA,QAAsB,OAAO,QAAQ;AACpC,QAAM,EAAE,KAAK,OAAO,OAAO,IAAI,MAAM,IAAI,KAAuB,GAE1D,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,SAAS,IAAK,GAC/C,SAAS,IAAI,KAAK,MAAM,UAAU,EAAE,KAAK,OAAO,CAAC,CAAC,GAEpD,QAAQ;AAOZ,WANI,WAAW,UAAa,OAAO,UAAU,QAC5C,QAAQ,OAAO,QAEf,MAAM,KAAK,MAAM,cAAc,EAAE,QAAQ,MAAM,CAAC,CAAC,GAG9C,SAAS,QACL,SAAS,KAAK,EAAE,SAAS,GAAM,CAAuB,KAE9D,KAAK,MAAM,UAAU,EAAE,KAAK,QAAQ,OAAO,OAAO,QAAQ,EAAE,CAAC,GACtD,SAAS,KAAK,EAAE,SAAS,GAAK,CAAuB;AAAA,EAC7D;AACD;AAnBC;AAAA,EADC,KAAK,QAAQ;AAAA,GAVF,kBAWZ;",
  "names": []
}
