Skip to content

Commit 30ba7df

Browse files
zmh-programclaude
andcommitted
feat: support worker cache by DurableObject
Co-Authored-By: Claude <81847+claude@users.noreply.github.com>
1 parent 15e8d09 commit 30ba7df

5 files changed

Lines changed: 188 additions & 73 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "bytes-radar"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2021"
55
authors = ["ProgramZmh <zmh@lightxi.com>"]
66
description = "A tool for analyzing code statistics from remote repositories with hyper-fast performance"

worker/cache.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
interface CacheEntry {
2+
data: any;
3+
timestamp: number;
4+
ttl: number;
5+
}
6+
7+
export class CacheManager {
8+
private cache: Map<string, CacheEntry>;
9+
private readonly defaultTTL: number;
10+
private readonly maxSize: number;
11+
private readonly cleanupInterval: number;
12+
private cleanupTimer: number | null;
13+
14+
constructor(
15+
options: {
16+
ttl?: number;
17+
maxSize?: number;
18+
cleanupInterval?: number;
19+
} = {},
20+
) {
21+
this.cache = new Map();
22+
this.defaultTTL = options.ttl || 3600;
23+
this.maxSize = options.maxSize || 1000;
24+
this.cleanupInterval = options.cleanupInterval || 300;
25+
this.cleanupTimer = null;
26+
this.startCleanupTimer();
27+
}
28+
29+
private startCleanupTimer(): void {
30+
if (this.cleanupTimer) return;
31+
this.cleanupTimer = setInterval(
32+
() => this.cleanup(),
33+
this.cleanupInterval * 1000,
34+
) as unknown as number;
35+
}
36+
37+
private stopCleanupTimer(): void {
38+
if (this.cleanupTimer) {
39+
clearInterval(this.cleanupTimer);
40+
this.cleanupTimer = null;
41+
}
42+
}
43+
44+
set(key: string, value: any, ttl: number = this.defaultTTL): void {
45+
if (this.cache.size >= this.maxSize) {
46+
const oldestKey = Array.from(this.cache.entries()).sort(
47+
([, a], [, b]) => a.timestamp - b.timestamp,
48+
)[0][0];
49+
this.cache.delete(oldestKey);
50+
}
51+
this.cache.set(key, {
52+
data: value,
53+
timestamp: Date.now(),
54+
ttl: ttl * 1000,
55+
});
56+
}
57+
58+
get(key: string): any | null {
59+
const entry = this.cache.get(key);
60+
if (!entry) return null;
61+
62+
const now = Date.now();
63+
if (now - entry.timestamp > entry.ttl) {
64+
this.cache.delete(key);
65+
return null;
66+
}
67+
return entry.data;
68+
}
69+
70+
has(key: string): boolean {
71+
const entry = this.cache.get(key);
72+
if (!entry) return false;
73+
74+
const now = Date.now();
75+
if (now - entry.timestamp > entry.ttl) {
76+
this.cache.delete(key);
77+
return false;
78+
}
79+
return true;
80+
}
81+
82+
delete(key: string): void {
83+
this.cache.delete(key);
84+
}
85+
86+
clear(): void {
87+
this.cache.clear();
88+
}
89+
90+
cleanup(): void {
91+
const now = Date.now();
92+
for (const [key, entry] of this.cache.entries()) {
93+
if (now - entry.timestamp > entry.ttl) {
94+
this.cache.delete(key);
95+
}
96+
}
97+
}
98+
99+
getStats(): { total: number; expired: number; size: number } {
100+
let total = 0;
101+
let expired = 0;
102+
const now = Date.now();
103+
104+
for (const entry of this.cache.values()) {
105+
total++;
106+
if (now - entry.timestamp > entry.ttl) {
107+
expired++;
108+
}
109+
}
110+
111+
return { total, expired, size: this.cache.size };
112+
}
113+
114+
destroy(): void {
115+
this.stopCleanupTimer();
116+
this.clear();
117+
}
118+
}

worker/types.d.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,22 @@ export interface AnalysisOptions {
3333
ignore_hidden: boolean;
3434
ignore_gitignore: boolean;
3535
aggressive_filtering?: boolean;
36-
custom_filter?: IntelligentFilter;
36+
}
37+
38+
export interface CacheOptions {
39+
ttl?: number;
40+
maxSize?: number;
41+
cleanupInterval?: number;
42+
}
43+
44+
export interface CacheStats {
45+
total: number;
46+
expired: number;
47+
size: number;
48+
}
49+
50+
export interface CacheEntry {
51+
data: any;
52+
timestamp: number;
53+
ttl: number;
3754
}

worker/worker.ts

Lines changed: 50 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { AnalysisOptions } from "./types";
22
import wasmBinary from "./pkg/bytes_radar_bg.wasm";
3+
import { CacheManager } from "./cache";
34

45
export interface Env {
56
BYTES_RADAR: DurableObjectNamespace;
@@ -8,66 +9,32 @@ export interface Env {
89
}
910

1011
export class BytesRadar {
11-
state: DurableObjectState;
12-
env: Env;
13-
private wasmModule: any = null;
14-
private wasmInitialized = false;
15-
16-
constructor(state: DurableObjectState, env: Env) {
17-
this.state = state;
18-
this.env = env;
12+
private wasmModule: any;
13+
private wasmInitialized: boolean = false;
14+
private cacheManager: CacheManager;
15+
16+
constructor() {
17+
this.cacheManager = new CacheManager({
18+
ttl: 7200,
19+
maxSize: 5000,
20+
cleanupInterval: 600,
21+
});
1922
}
2023

21-
private log(
22-
level: "debug" | "info" | "warn" | "error",
23-
message: string,
24-
data?: any,
25-
) {
26-
const logLevel = this.env.LOG_LEVEL || "info";
27-
const environment = this.env.ENVIRONMENT || "development";
28-
29-
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
30-
const currentLevel = levels[logLevel as keyof typeof levels] || 1;
31-
const messageLevel = levels[level];
32-
33-
if (messageLevel >= currentLevel) {
34-
const timestamp = new Date().toISOString();
35-
const logEntry = {
36-
timestamp,
37-
level: level.toUpperCase(),
38-
environment,
39-
message,
40-
...(data && { data }),
41-
};
42-
43-
const fn = {
44-
debug: console.debug,
45-
info: console.info,
46-
warn: console.warn,
47-
error: console.error,
48-
};
49-
50-
if (environment === "production") {
51-
fn[level](JSON.stringify(logEntry));
52-
} else {
53-
fn[level](`[${level.toUpperCase()}] ${message}`, data ? data : "");
54-
}
55-
}
24+
private log(level: string, message: string, data?: any) {
25+
console.log(JSON.stringify({ level, message, data }));
5626
}
5727

5828
private async initializeWasm() {
59-
if (!this.wasmInitialized) {
60-
try {
61-
this.wasmModule = await import("./pkg/bytes_radar");
62-
await this.wasmModule.default(wasmBinary);
63-
this.wasmInitialized = true;
64-
this.log("info", "WebAssembly module initialized successfully");
65-
} catch (error) {
66-
this.log("error", "Failed to initialize WebAssembly module", {
67-
error: error instanceof Error ? error.message : String(error),
68-
});
69-
throw error;
70-
}
29+
if (this.wasmInitialized) return;
30+
try {
31+
this.wasmModule = await import("./pkg/bytes_radar");
32+
await this.wasmModule.default(wasmBinary);
33+
this.wasmInitialized = true;
34+
this.log("info", "WASM initialized");
35+
} catch (error) {
36+
this.log("error", "WASM init failed", error);
37+
throw error;
7138
}
7239
}
7340

@@ -82,7 +49,6 @@ export class BytesRadar {
8249
provider_settings: {},
8350
};
8451

85-
// Parse numeric options
8652
const numericOptions: Record<string, (val: number) => void> = {
8753
timeout: (val) => (options.timeout = val),
8854
max_redirects: (val) => (options.max_redirects = val),
@@ -99,7 +65,6 @@ export class BytesRadar {
9965
}
10066
}
10167

102-
// Parse string options
10368
const stringOptions: Record<string, (val: string) => void> = {
10469
user_agent: (val) => (options.user_agent = val),
10570
proxy: (val) => (options.proxy = val),
@@ -112,13 +77,11 @@ export class BytesRadar {
11277
}
11378
}
11479

115-
// Parse boolean options
11680
if (searchParams.get("aggressive_filtering") !== null) {
11781
options.aggressive_filtering =
11882
searchParams.get("aggressive_filtering") === "true";
11983
}
12084

121-
// Parse custom headers, credentials, and provider settings
12285
for (const [key, value] of searchParams.entries()) {
12386
if (key.startsWith("header.")) {
12487
options.headers[key.slice(7)] = value;
@@ -132,6 +95,11 @@ export class BytesRadar {
13295
return options;
13396
}
13497

98+
private generateCacheKey(url: string, options: AnalysisOptions): string {
99+
const { headers, credentials, ...cacheableOptions } = options;
100+
return `${url}:${JSON.stringify(cacheableOptions)}`;
101+
}
102+
135103
async fetch(request: Request) {
136104
const url = new URL(request.url);
137105
if (url.pathname === "/favicon.ico") {
@@ -149,14 +117,8 @@ export class BytesRadar {
149117
if (!targetUrl) {
150118
return new Response(
151119
JSON.stringify({
152-
error: "Missing repository path",
153-
usage: [
154-
"/[user/repo",
155-
"/user/repo@master",
156-
"/github.com/user/repo",
157-
"/gitlab.com/user/repo",
158-
"http://example.com/example-asset.tar.gz",
159-
],
120+
error: "Missing path",
121+
usage: ["/user/repo", "/user/repo@branch", "/github.com/user/repo"],
160122
}),
161123
{
162124
status: 400,
@@ -169,20 +131,38 @@ export class BytesRadar {
169131
}
170132

171133
const options = this.parseQueryOptions(url.searchParams);
172-
this.log("info", "Starting analysis", { url: targetUrl, options });
134+
const cacheKey = this.generateCacheKey(targetUrl, options);
135+
136+
const cachedResult = this.cacheManager.get(cacheKey);
137+
if (cachedResult) {
138+
this.log("info", "Cache hit", { url: targetUrl });
139+
return new Response(JSON.stringify(cachedResult), {
140+
headers: {
141+
"Content-Type": "application/json",
142+
"Access-Control-Allow-Origin": "*",
143+
"X-Cache": "HIT",
144+
"X-Cache-TTL": String(this.cacheManager["defaultTTL"]),
145+
},
146+
});
147+
}
148+
149+
this.log("info", "Analysis start", { url: targetUrl });
173150

174151
const result = await this.wasmModule.analyze_url(targetUrl, options);
175-
this.log("info", "Analysis completed successfully", result);
152+
this.cacheManager.set(cacheKey, result);
153+
154+
this.log("info", "Analysis done", result);
176155

177156
return new Response(JSON.stringify(result), {
178157
headers: {
179158
"Content-Type": "application/json",
180159
"Access-Control-Allow-Origin": "*",
160+
"X-Cache": "MISS",
181161
},
182162
});
183163
} catch (error: unknown) {
184164
const errorResponse = this.handleError(error, startTime);
185-
this.log("error", "Error in BytesRadar fetch", errorResponse);
165+
this.log("error", "Request failed", errorResponse);
186166

187167
return new Response(JSON.stringify(errorResponse), {
188168
status: 500,

0 commit comments

Comments
 (0)