-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace RateLimiter with RequestCounter
- Loading branch information
1 parent
78f7eb9
commit ca9419a
Showing
15 changed files
with
214 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { RequestCounter } from './request_counter' | ||
export { RequestCounter }; | ||
|
||
|
||
interface Env { | ||
REQUEST_COUNTER: DurableObjectNamespace<RequestCounter>; | ||
} | ||
|
||
export default { | ||
async fetch(request, env, ctx): Promise<Response> { | ||
const ip = request.headers.get("CF-Connecting-IP"); | ||
if (ip === null) { | ||
return errorResponse(400, "Could not determine client IP"); | ||
} | ||
|
||
try { | ||
const id = env.REQUEST_COUNTER.idFromName(ip); | ||
const stub = env.REQUEST_COUNTER.get(id); | ||
const stats = await stub.increment(); | ||
|
||
return new Response(JSON.stringify(stats)); | ||
} catch (e) { | ||
return errorResponse(500, "Internal server error"); | ||
} | ||
}, | ||
} satisfies ExportedHandler<Env>; | ||
|
||
|
||
|
||
function errorResponse(status: number, message: string): Response { | ||
return new Response(JSON.stringify({ message }), { | ||
status, | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { DurableObject } from "cloudflare:workers"; | ||
|
||
|
||
interface State { | ||
minutely: Period; | ||
hourly: Period; | ||
daily: Period; | ||
totalCount: number; | ||
} | ||
|
||
function initState(): State { | ||
return { | ||
minutely: minutelyPeriod(), | ||
hourly: hourlyPeriod(), | ||
daily: dailyPeriod(), | ||
totalCount: 0, | ||
}; | ||
} | ||
|
||
export class RequestCounter extends DurableObject { | ||
async fetch(request: Request): Promise<Response> { | ||
const stats = await this.increment(); | ||
return new Response(JSON.stringify(stats), { status: 200 }); | ||
} | ||
|
||
async increment(): Promise<RequestStats> { | ||
const now = Date.now(); | ||
|
||
let state = await this.ctx.storage.get<State>("state") | ||
if (!state) { | ||
state = initState(); | ||
} | ||
|
||
incrementPeriodRequest(state.minutely, now); | ||
incrementPeriodRequest(state.hourly, now); | ||
incrementPeriodRequest(state.daily, now); | ||
state.totalCount++; | ||
|
||
await this.ctx.storage.put("state", state); | ||
|
||
return { | ||
minutely: getPeriodStats(state.minutely, now), | ||
hourly: getPeriodStats(state.hourly, now), | ||
daily: getPeriodStats(state.daily, now), | ||
totalCount: state.totalCount, | ||
}; | ||
} | ||
} | ||
|
||
interface Period { | ||
startTimestamp: number | ||
count: number; | ||
duration: number; | ||
} | ||
|
||
function minutelyPeriod(): Period { | ||
return { | ||
startTimestamp: 0, | ||
count: 0, | ||
duration: 60 * 1000, | ||
}; | ||
} | ||
|
||
function hourlyPeriod(): Period { | ||
return { | ||
startTimestamp: 0, | ||
count: 0, | ||
duration: 60 * 60 * 1000, | ||
}; | ||
} | ||
|
||
function dailyPeriod(): Period { | ||
return { | ||
startTimestamp: 0, | ||
count: 0, | ||
duration: 24 * 60 * 60 * 1000, | ||
}; | ||
} | ||
|
||
function incrementPeriodRequest(period: Period, now: number): void { | ||
const elapsedTime = now - period.startTimestamp; | ||
const timeUntilReset = period.duration - elapsedTime; | ||
|
||
if (timeUntilReset <= 0) { | ||
period.startTimestamp = now; | ||
period.count = 0; | ||
} | ||
|
||
period.count++; | ||
} | ||
|
||
export interface RequestStats { | ||
minutely: PeriodStats; | ||
hourly: PeriodStats; | ||
daily: PeriodStats; | ||
totalCount: number; | ||
} | ||
|
||
export interface PeriodStats { | ||
count: number; | ||
timeUntilReset: number; | ||
} | ||
|
||
|
||
function getPeriodStats(period: Period, now: number): PeriodStats { | ||
const elapsedTime = now - period.startTimestamp; | ||
const timeUntilReset = period.duration - elapsedTime; | ||
|
||
return { | ||
count: period.count, | ||
timeUntilReset, | ||
}; | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.