Skip to content

Commit b41b4c6

Browse files
committed
refactor: use sqlite for storage
1 parent 957fd6b commit b41b4c6

File tree

7 files changed

+113
-33
lines changed

7 files changed

+113
-33
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,5 @@ dist
173173

174174
# Finder (MacOS) folder config
175175
.DS_Store
176+
177+
sqlite.db

bun.lockb

-26 Bytes
Binary file not shown.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"typescript": "^5.7.2"
1313
},
1414
"dependencies": {
15-
"file-system-cache": "^3.0.0-alpha.8"
15+
"xxhash-wasm": "^1.1.0"
1616
}
1717
}

src/cache.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import xxhash from "xxhash-wasm";
2+
import { Database } from "bun:sqlite";
3+
4+
const hasher = await xxhash();
5+
6+
const db = new Database("sqlite.db", {
7+
create: true,
8+
strict: true,
9+
});
10+
11+
type CachedResponse = {
12+
key: bigint;
13+
body?: Uint8Array;
14+
timestamp: number;
15+
status: number;
16+
contentType?: string;
17+
};
18+
19+
db.query(
20+
`
21+
CREATE TABLE IF NOT EXISTS cache (
22+
key INTEGER PRIMARY KEY,
23+
body BLOB,
24+
status INTEGER NOT NULL,
25+
timestamp INTEGER NOT NULL,
26+
contentType TEXT
27+
)
28+
`
29+
).run();
30+
31+
const getCachedQuery = db.prepare<CachedResponse, CachedResponse["key"]>(
32+
`SELECT * FROM cache WHERE key = ? AND timestamp > (strftime('%s', 'now') - 60 * 5)`
33+
);
34+
35+
const setCachedQuery = db.prepare<
36+
undefined,
37+
Omit<CachedResponse, "timestamp">
38+
>(`
39+
INSERT OR REPLACE INTO cache (key, body, status, contentType, timestamp)
40+
VALUES ($key, $body, $status, $contentType, strftime('%s', 'now'))
41+
`);
42+
43+
export function getCachedResponse(cacheKey: bigint) {
44+
return getCachedQuery.get(cacheKey);
45+
}
46+
47+
export function setCachedResponse(
48+
cachedResponse: Omit<CachedResponse, "timestamp">
49+
) {
50+
return setCachedQuery.run(cachedResponse);
51+
}
52+
53+
export function createCacheKey(request: Request) {
54+
return hasher.h64(
55+
`${request.method} ${request.url} ${request.headers.get("Authorization")}`
56+
);
57+
}
58+
59+
export function createResponseFromCached(
60+
cached: Omit<CachedResponse, "timestamp" | "key">
61+
) {
62+
return new Response(cached.body, {
63+
headers: {
64+
"Content-Type": cached.contentType ?? "application/octet-stream",
65+
},
66+
status: cached.status,
67+
});
68+
}

src/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { handleApiRequest } from "./routes/api";
22

3-
Bun.serve({
3+
const server = Bun.serve({
44
fetch(request) {
55
const url = new URL(request.url);
66

@@ -10,5 +10,6 @@ Bun.serve({
1010

1111
return new Response("Not found", { status: 404 });
1212
},
13-
port: 3000,
1413
});
14+
15+
console.log(`Listening on :${server.port}`);

src/routes/api.ts

+39-18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { cache, createCacheKey } from "../utils";
1+
import {
2+
createCacheKey,
3+
createResponseFromCached,
4+
getCachedResponse,
5+
setCachedResponse,
6+
} from "../cache";
27

3-
interface CachedResponse {
4-
headers: [string, string][];
5-
body: string | null;
6-
}
8+
const requestMap = new Map<bigint, Promise<Response>>();
79

810
export async function handleApiRequest(request: Request, url: URL) {
911
url.hostname = "discord.com";
@@ -12,7 +14,7 @@ export async function handleApiRequest(request: Request, url: URL) {
1214

1315
request.headers.set("Host", "discord.com");
1416

15-
if (request.method !== "GET") {
17+
if (request.method !== "GET" && request.method !== "HEAD") {
1618
return fetch(url.toString(), {
1719
method: request.method,
1820
headers: request.headers,
@@ -22,28 +24,47 @@ export async function handleApiRequest(request: Request, url: URL) {
2224

2325
const cacheKey = createCacheKey(request);
2426

25-
const existing = (await cache.get(cacheKey)) as CachedResponse | undefined;
27+
const existingCache = getCachedResponse(cacheKey);
2628

27-
if (existing) {
28-
return new Response(existing.body, {
29-
headers: new Headers(existing.headers),
30-
});
29+
if (existingCache) {
30+
return createResponseFromCached(existingCache);
31+
}
32+
33+
const existingRequest = requestMap.get(cacheKey);
34+
35+
if (existingRequest) {
36+
return existingRequest;
3137
}
3238

39+
const promise = makeRequest(cacheKey, url, request).finally(() => {
40+
requestMap.delete(cacheKey);
41+
});
42+
43+
requestMap.set(cacheKey, promise);
44+
45+
return promise;
46+
}
47+
48+
async function makeRequest(cacheKey: bigint, url: URL, request: Request) {
3349
const response = await fetch(url.toString(), {
3450
method: request.method,
3551
headers: request.headers,
3652
body: request.body,
3753
});
3854

39-
const content = response.status === 204 ? null : await response.text();
55+
const content =
56+
response.status === 204
57+
? undefined
58+
: new Uint8Array(await response.arrayBuffer());
4059

41-
await cache.set(cacheKey, {
42-
headers: [...response.headers.entries()],
60+
const cachedResponse = {
61+
key: cacheKey,
4362
body: content,
44-
} satisfies CachedResponse);
63+
contentType: response.headers.get("Content-Type") ?? undefined,
64+
status: response.status,
65+
};
4566

46-
return new Response(content, {
47-
headers: response.headers,
48-
});
67+
setCachedResponse(cachedResponse);
68+
69+
return createResponseFromCached(cachedResponse);
4970
}

src/utils.ts

-12
This file was deleted.

0 commit comments

Comments
 (0)