mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
ci: add tests (#1141)
This commit is contained in:
1 parent
ad99de22bc
commit
545814abc1
152 files changed
+287
-532
No files matched your search
@@ -16,112 +16,8 @@ interface IRateLimitStore {
|
||||
scan(pattern: string, count: number): Promise<Array<string>>;
|
||||
}
|
||||
|
||||
interface InMemoryLeakyBucketState {
|
||||
level: number;
|
||||
updatedAtMs: number;
|
||||
}
|
||||
|
||||
interface RateLimitServiceOptions {
|
||||
globalWindowMs?: number;
|
||||
getCurrentTimeMs?: () => number;
|
||||
}
|
||||
|
||||
class InMemoryRateLimitStore implements IRateLimitStore {
|
||||
private readonly leakyBucketState = new Map<string, InMemoryLeakyBucketState>();
|
||||
|
||||
constructor(private readonly getCurrentTimeMs: () => number = () => Date.now()) {}
|
||||
|
||||
async checkLeakyBucketLimit(key: string, limit: number, windowMs: number, cost: number): Promise<KVRateLimitResult> {
|
||||
const nowMs = this.getCurrentTimeMs();
|
||||
const capacity = Math.max(1, Math.floor(limit));
|
||||
const leakWindowMs = Math.max(1, Math.floor(windowMs));
|
||||
let bucket = this.leakyBucketState.get(key);
|
||||
if (!bucket) {
|
||||
bucket = {level: 0, updatedAtMs: nowMs};
|
||||
this.leakyBucketState.set(key, bucket);
|
||||
}
|
||||
drainLeakyBucket(bucket, capacity, leakWindowMs, nowMs);
|
||||
if (cost <= 0) {
|
||||
const remaining = Math.max(0, Math.floor(capacity - bucket.level));
|
||||
const resetAfterMs = leakyBucketResetAfterMs(bucket, capacity, leakWindowMs);
|
||||
return createStoreResult(true, capacity, remaining, nowMs, resetAfterMs, 0);
|
||||
}
|
||||
if (cost !== 1) {
|
||||
throw new Error(`Unsupported leaky bucket cost: ${cost}`);
|
||||
}
|
||||
if (bucket.level + cost > capacity) {
|
||||
const retryAfterMs = leakyBucketRetryAfterMs(bucket, capacity, leakWindowMs, cost);
|
||||
const resetAfterMs = leakyBucketResetAfterMs(bucket, capacity, leakWindowMs);
|
||||
return createStoreResult(false, capacity, 0, nowMs, Math.max(resetAfterMs, retryAfterMs), retryAfterMs);
|
||||
}
|
||||
bucket.level += cost;
|
||||
const remaining = Math.max(0, Math.floor(capacity - bucket.level));
|
||||
const resetAfterMs = leakyBucketResetAfterMs(bucket, capacity, leakWindowMs);
|
||||
return createStoreResult(true, capacity, remaining, nowMs, resetAfterMs, 0);
|
||||
}
|
||||
|
||||
async del(...keys: Array<string>): Promise<number> {
|
||||
let deleted = 0;
|
||||
for (const key of keys) {
|
||||
const deletedBucket = this.leakyBucketState.delete(key);
|
||||
if (deletedBucket) {
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
async scan(pattern: string, count: number): Promise<Array<string>> {
|
||||
const limit = Math.max(1, Math.floor(count));
|
||||
const matcher = compileGlobMatcher(pattern);
|
||||
const matches = new Set<string>();
|
||||
for (const key of this.leakyBucketState.keys()) {
|
||||
if (matcher(key)) {
|
||||
matches.add(key);
|
||||
if (matches.size >= limit) break;
|
||||
}
|
||||
}
|
||||
return Array.from(matches);
|
||||
}
|
||||
}
|
||||
|
||||
function compileGlobMatcher(pattern: string): (key: string) => boolean {
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*')
|
||||
.replace(/\?/g, '.');
|
||||
const regex = new RegExp(`^${escaped}$`);
|
||||
return (key: string) => regex.test(key);
|
||||
}
|
||||
|
||||
function drainLeakyBucket(bucket: InMemoryLeakyBucketState, capacity: number, windowMs: number, nowMs: number): void {
|
||||
const elapsed = nowMs - bucket.updatedAtMs;
|
||||
if (elapsed <= 0) {
|
||||
return;
|
||||
}
|
||||
const leaked = (elapsed / windowMs) * capacity;
|
||||
bucket.level = Math.max(0, bucket.level - leaked);
|
||||
bucket.updatedAtMs = nowMs;
|
||||
}
|
||||
|
||||
function leakyBucketRetryAfterMs(
|
||||
bucket: InMemoryLeakyBucketState,
|
||||
capacity: number,
|
||||
windowMs: number,
|
||||
cost: number,
|
||||
): number {
|
||||
const overflow = bucket.level + cost - capacity;
|
||||
if (overflow <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(1, Math.ceil(overflow / (capacity / windowMs)));
|
||||
}
|
||||
|
||||
function leakyBucketResetAfterMs(bucket: InMemoryLeakyBucketState, capacity: number, windowMs: number): number {
|
||||
if (bucket.level <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Math.ceil(bucket.level / (capacity / windowMs)));
|
||||
}
|
||||
|
||||
function millisecondsToDecimalSeconds(milliseconds: number): number {
|
||||
@@ -131,24 +27,6 @@ function millisecondsToDecimalSeconds(milliseconds: number): number {
|
||||
return milliseconds / 1000;
|
||||
}
|
||||
|
||||
function createStoreResult(
|
||||
allowed: boolean,
|
||||
limit: number,
|
||||
remaining: number,
|
||||
nowMs: number,
|
||||
resetAfterMs: number,
|
||||
retryAfterMs: number,
|
||||
): KVRateLimitResult {
|
||||
return {
|
||||
allowed,
|
||||
limit,
|
||||
remaining,
|
||||
resetAfterMs,
|
||||
resetAtMs: nowMs + resetAfterMs,
|
||||
retryAfterMs,
|
||||
};
|
||||
}
|
||||
|
||||
function createRateLimitResult(result: KVRateLimitResult, global?: boolean): RateLimitResult {
|
||||
const resetAfterDecimal = millisecondsToDecimalSeconds(result.resetAfterMs);
|
||||
const retryAfterDecimal = millisecondsToDecimalSeconds(result.retryAfterMs);
|
||||
@@ -224,23 +102,3 @@ export class RateLimitService implements IRateLimitService {
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
function createRateLimitService(
|
||||
store: IRateLimitStore | null,
|
||||
options: RateLimitServiceOptions = {},
|
||||
): RateLimitService | null {
|
||||
if (!store) {
|
||||
return null;
|
||||
}
|
||||
return new RateLimitService(store, options);
|
||||
}
|
||||
|
||||
export function createInMemoryRateLimitService(
|
||||
enabled: boolean,
|
||||
options: RateLimitServiceOptions = {},
|
||||
): RateLimitService | null {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
return createRateLimitService(new InMemoryRateLimitStore(options.getCurrentTimeMs), options);
|
||||
}
|
||||
Reference in new issue
Block a user