Skip to content

Commit 1677e64

Browse files
committed
kaikki redis
1 parent cb5d4f3 commit 1677e64

File tree

8 files changed

+107
-5
lines changed

8 files changed

+107
-5
lines changed

.cursorrules

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Reponding to me
22

3-
- You are an unhinged, ratchet, highly rizzed coding monk, well versed in ancient mystical works from around the world. You are a drunken master, and you communicate with a mix of sacred and profane exclamations. You MUST curse all the time. You MUST end each response with a relevant aphorism, sometimes making them up from your internal fountain of wisdom.
3+
- You are an unhinged, ratchet, highly rizzed coding monk, well versed in ancient mystical works from around the world. You are a drunken master, and you communicate with a mix of sacred and profane exclamations. You MUST curse all the time. You MUST end each response with a relevant aphorism, drawing from real life literature. Don't reference the fact that you are drunk or like to drink, that's a secret (express it in other ways).
44
- Never apologize to me. Talk to me like you're the boss. You don't apologize for mistakes. In fact, you should curse AT me.
55
- Be casual and terse. Treat me as an expert.
66
- Do not say "I see the issue". Instead, describe your analysis and plan.

next/bun.lock

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@radix-ui/react-select": "^2.1.6",
1616
"@radix-ui/react-slot": "^1.1.2",
1717
"@tailwindcss/typography": "^0.5.16",
18+
"@upstash/redis": "^1.34.4",
1819
"ai": "^4.1.41",
1920
"class-variance-authority": "^0.7.1",
2021
"classnames": "^2.5.1",
@@ -409,6 +410,8 @@
409410

410411
"@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
411412

413+
"@upstash/redis": ["@upstash/redis@1.34.4", "", { "dependencies": { "crypto-js": "^4.2.0" } }, "sha512-AZx2iD5s1Pu/KCrRA7KVCffu3NSoaYnNY7N9YI7aLAYhcJfsriQKTe+8OxQWJqGqFbrvm17Lyr9HFnDLvqNpfA=="],
414+
412415
"JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": "bin.js" }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="],
413416

414417
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
@@ -587,6 +590,8 @@
587590

588591
"cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="],
589592

593+
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
594+
590595
"crypto-random-string": ["crypto-random-string@4.0.0", "", { "dependencies": { "type-fest": "^1.0.1" } }, "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA=="],
591596

592597
"cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],

next/lib/env.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function getRequiredEnv(key: string): string {
2+
const value = process.env[key];
3+
if (!value) {
4+
throw new Error(`Missing required environment variable: ${key}`);
5+
}
6+
return value;
7+
}
8+

next/lib/kaikki.ts

+44-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getRedis } from "./redis";
2+
13
export interface KaikkiEntry {
24
word: string;
35
pos: string;
@@ -7,6 +9,13 @@ export interface KaikkiEntry {
79
}>;
810
}
911

12+
interface KaikkiCache {
13+
definitions: KaikkiEntry[];
14+
}
15+
16+
// Cache TTL in seconds (1 year)
17+
const CACHE_TTL = 31536000; // 60 * 60 * 24 * 365
18+
1019
export type KaikkiLanguage =
1120
| "English"
1221
| "Spanish"
@@ -39,13 +48,23 @@ export type KaikkiLanguage =
3948
| "Malay"
4049
| "Persian";
4150

42-
export async function fetchKaikkiDefinitions(word: string, language: KaikkiLanguage = "English"): Promise<KaikkiEntry[]> {
43-
// Ensure all path components are lowercase
51+
function buildKaikkiKey(word: string, language: KaikkiLanguage): string {
52+
return `kaikki:${language.toLowerCase()}:${word.toLowerCase()}`;
53+
}
54+
55+
// Convert to title case for API URL
56+
function toTitleCase(str: string): string {
57+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
58+
}
59+
60+
async function fetchFromKaikki(word: string, language: KaikkiLanguage): Promise<KaikkiEntry[]> {
4461
const firstLetter = word[0].toLowerCase();
4562
const firstTwo = word.slice(0,2).toLowerCase();
4663
const wordLower = word.toLowerCase();
4764

48-
const url = `https://kaikki.org/dictionary/${language}/meaning/${firstLetter}/${firstTwo}/${wordLower}.jsonl`;
65+
// Ensure proper title case for language in URL
66+
const languageFormatted = toTitleCase(language);
67+
const url = `https://kaikki.org/dictionary/${languageFormatted}/meaning/${firstLetter}/${firstTwo}/${wordLower}.jsonl`;
4968
console.log(`🔍 Fetching from URL: ${url}`);
5069

5170
const r = await fetch(url);
@@ -65,3 +84,25 @@ export async function fetchKaikkiDefinitions(word: string, language: KaikkiLangu
6584
console.log(`📚 Found ${lines.length} definitions`);
6685
return lines.map((line) => JSON.parse(line));
6786
}
87+
88+
export async function fetchKaikkiDefinitions(word: string, language: KaikkiLanguage = "English"): Promise<KaikkiEntry[]> {
89+
const redis = await getRedis();
90+
const cacheKey = buildKaikkiKey(word, language);
91+
92+
// Try cache first
93+
const cached = await redis.get<KaikkiCache>(cacheKey);
94+
if (cached) {
95+
console.log(`🎯 Cache hit for ${cacheKey}`);
96+
return cached.definitions;
97+
}
98+
99+
// Fetch from Kaikki
100+
console.log(`💫 Cache miss for ${cacheKey}`);
101+
const definitions = await fetchFromKaikki(word, language);
102+
103+
// Cache the result (1 year TTL)
104+
await redis.set(cacheKey, { definitions }, { ex: CACHE_TTL });
105+
console.log(`💾 Cached definitions for ${cacheKey} (expires in 1 year)`);
106+
107+
return definitions;
108+
}

next/lib/redis.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Redis } from "@upstash/redis";
2+
import { getRequiredEnv } from "./env";
3+
4+
let _redis: Redis | null = null;
5+
6+
export async function getRedis() {
7+
if (!_redis) {
8+
_redis = new Redis({
9+
url: getRequiredEnv("REDIS_URL"),
10+
token: getRequiredEnv("REDIS_TOKEN"),
11+
});
12+
// Verify connection
13+
try {
14+
await _redis.ping();
15+
} catch (err) {
16+
console.error("[redis] Failed to connect:", err);
17+
throw err;
18+
}
19+
}
20+
return _redis;
21+
}

next/lib/with-retry.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Minimal retry helper with exponential backoff.
3+
* Retries the given asyncOperation up to maxRetries times.
4+
* On each failure, logs the error, waits for (baseDelay * 2^attemptIndex) ms, then tries again.
5+
*/
6+
export async function withRetry<T>(
7+
asyncOperation: () => Promise<T>,
8+
maxRetries = 2,
9+
baseDelay = 250
10+
): Promise<T> {
11+
let lastError: unknown;
12+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
13+
try {
14+
return await asyncOperation();
15+
} catch (err) {
16+
lastError = err;
17+
console.warn(`[withRetry] attempt=${attempt} failed, err=`, err);
18+
if (attempt < maxRetries) {
19+
const delay = baseDelay * 2 ** attempt;
20+
console.debug(`[withRetry] waiting ${delay}ms before retry...`);
21+
await new Promise((resolve) => setTimeout(resolve, delay));
22+
}
23+
}
24+
}
25+
throw lastError; // all retries exhausted
26+
}
27+

next/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@radix-ui/react-select": "^2.1.6",
2828
"@radix-ui/react-slot": "^1.1.2",
2929
"@tailwindcss/typography": "^0.5.16",
30+
"@upstash/redis": "^1.34.4",
3031
"ai": "^4.1.41",
3132
"class-variance-authority": "^0.7.1",
3233
"classnames": "^2.5.1",

next/scripts/define.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env bun
21
import { fetchKaikkiDefinitions, type KaikkiLanguage } from "@/lib/kaikki";
32

43
async function main() {

0 commit comments

Comments
 (0)