File size: 7,549 Bytes
7a5aa35 7f60362 33ffc9c 7f60362 e4feb9d 33ffc9c e4feb9d 33ffc9c e4feb9d 33ffc9c 7f60362 33ffc9c 7f60362 33ffc9c 7f60362 33ffc9c 7f60362 33ffc9c 7f60362 33ffc9c 7f60362 e4feb9d 33ffc9c 7f60362 e4feb9d 33ffc9c 7f60362 e4feb9d 33ffc9c 7f60362 33ffc9c e4feb9d 33ffc9c cecb963 7f60362 7a5aa35 7f60362 e4feb9d 7a5aa35 33ffc9c 7a5aa35 7f60362 e4feb9d 7a5aa35 33ffc9c 7a5aa35 e4feb9d 7a5aa35 7f60362 e4feb9d 7a5aa35 7f60362 7a5aa35 33ffc9c 7a5aa35 33ffc9c cecb963 7f60362 e4feb9d cecb963 e4feb9d 33ffc9c cecb963 e4feb9d cecb963 e4feb9d 33ffc9c cecb963 7f60362 e4feb9d 7f60362 e4feb9d 7f60362 e4feb9d 7f60362 e4feb9d 33ffc9c e4feb9d 7f60362 33ffc9c cecb963 7f60362 e4feb9d 7a5aa35 33ffc9c 7f60362 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
import { serve } from "bun";
import { readFile } from "fs/promises";
import path from "path";
// Type Definition for Items (Packages/Programs)
interface Item {
name?: string;
full_name?: string;
owner?: string;
description?: string;
topics?: string[];
created_at?: string;
usage_count?: number;
}
// --- CONFIGURATION ---
const DB_PATH = "../database/database";
const PORT = 7860;
const RELOAD_INTERVAL_MS = 3600000; // 1 hour
const corsHeaders = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
};
// --- GLOBAL DATA STORE (in-memory) ---
let packages: Item[] = require(`${DB_PATH}/packages.json`);
let programs: Item[] = require(`${DB_PATH}/programs.json`);
let sorted = getSortedData(packages, programs);
// --- UTILITY FUNCTIONS ---
function getSortedData(packages: Item[], programs: Item[]) {
return {
packages: {
latest: [...packages].sort(sortByDate),
mostUsed: [...packages].sort(sortByUsage),
},
programs: {
latest: [...programs].sort(sortByDate),
mostUsed: [...programs].sort(sortByUsage),
},
};
}
function sortByDate(a: Item, b: Item): number {
return new Date(b.created_at || "").getTime() - new Date(a.created_at || "").getTime();
}
function sortByUsage(a: Item, b: Item): number {
return (b.usage_count || 0) - (a.usage_count || 0);
}
/**
* Filters an array of items by query and topic filter.
*/
function filterItems(items: Item[], query: string | null, topic: string | null): Item[] {
return items.filter(({ name, full_name, description, topics }) => {
if (topic && !topics?.some(t => t.toLowerCase() === topic)) return false;
if (!query) return true;
const lowerQ = query.toLowerCase();
return [name, full_name, description, ...(topics || [])]
.some(field => field?.toLowerCase().includes(lowerQ));
});
}
/**
* Returns a paginated slice of items.
*/
function getPaginated(items: Item[], page: number = 0, size: number = 10): Item[] {
const start = page * size;
return items.slice(start, start + size);
}
/**
* Finds an item by owner and repo name.
*/
function findItem(items: Item[], owner: string, repo: string): Item | undefined {
return items.find(({ owner: o, name, full_name }) =>
(o?.toLowerCase() === owner && name?.toLowerCase() === repo) ||
full_name?.toLowerCase() === `${owner}/${repo}`
);
}
/**
* Parses a range string of the form "start..end" to [start, end].
*/
function parseRange(str: string | null, max: number): [number, number] {
const match = str?.match(/^(\d+)\.\.(\d+)$/);
const [start, end] = match
? [parseInt(match[1]), parseInt(match[2])]
: [0, 10];
return [Math.max(0, start), Math.min(max, end)];
}
/**
* Loads data from disk and updates in-memory database and sorted views.
*/
async function reloadData() {
try {
const pkgPath = path.resolve(__dirname, `${DB_PATH}/packages.json`);
const prgPath = path.resolve(__dirname, `${DB_PATH}/programs.json`);
const [pkgData, prgData] = await Promise.all([
readFile(pkgPath, "utf8"),
readFile(prgPath, "utf8"),
]);
packages = JSON.parse(pkgData);
programs = JSON.parse(prgData);
sorted = getSortedData(packages, programs);
console.log(`[${new Date().toISOString()}] Database reloaded`);
} catch (err) {
console.error("Error reloading database files:", err);
}
}
// Initial data load and set up periodic reload
reloadData();
setInterval(reloadData, RELOAD_INTERVAL_MS);
// --- SERVER DEFINITION ---
serve({
port: PORT,
fetch(req: Request): Response | Promise<Response> {
const url = new URL(req.url);
const { pathname, searchParams } = url;
const q = searchParams.get("q")?.trim().toLowerCase() ?? null;
const filter = searchParams.get("filter")?.trim().toLowerCase() ?? null;
// Handle preflight CORS requests
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
...corsHeaders,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
// --- API ENDPOINTS ---
// Search endpoints
if (pathname === "/api/searchPackages") {
const result = filterItems(packages, q, filter).slice(0, 25);
return Response.json(result, { headers: corsHeaders });
}
if (pathname === "/api/searchPrograms") {
const result = filterItems(programs, q, filter).slice(0, 25);
return Response.json(result, { headers: corsHeaders });
}
// Infinite scroll endpoints
if (pathname === "/api/infiniteScrollPackages") {
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
if (isNaN(page) || page < 0)
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
return Response.json(getPaginated(packages, page), { headers: corsHeaders });
}
if (pathname === "/api/infiniteScrollPrograms") {
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
if (isNaN(page) || page < 0)
return Response.json({ error: "Invalid page number" }, { status: 400, headers: corsHeaders });
return Response.json(getPaginated(programs, page), { headers: corsHeaders });
}
// Item details endpoints (by owner/repo)
const programMatch = pathname.match(/^\/api\/programs\/([^\/]+)\/([^\/]+)$/);
if (programMatch) {
const [_, owner, repo] = programMatch.map(s => s.toLowerCase());
const found = findItem(programs, owner, repo);
return Response.json(found || { error: "Program not found" }, {
status: found ? 200 : 404,
headers: corsHeaders,
});
}
const packageMatch = pathname.match(/^\/api\/packages\/([^\/]+)\/([^\/]+)$/);
if (packageMatch) {
const [_, owner, repo] = packageMatch.map(s => s.toLowerCase());
const found = findItem(packages, owner, repo);
return Response.json(found || { error: "Package not found" }, {
status: found ? 200 : 404,
headers: corsHeaders,
});
}
// Index details endpoints (for homepage sections)
if (pathname === "/api/indexDetailsPackages") {
const section = searchParams.get("section");
if (section !== "latestRepos" && section !== "mostUsed") {
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
}
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
const data = sorted.packages[sortKey as keyof typeof sorted.packages] ?? packages;
const [start, end] = parseRange(searchParams.get("range"), data.length);
return Response.json(data.slice(start, end), { headers: corsHeaders });
}
if (pathname === "/api/indexDetailsPrograms") {
const section = searchParams.get("section");
if (section !== "latestRepos" && section !== "mostUsed") {
return Response.json({ error: "Invalid section" }, { status: 400, headers: corsHeaders });
}
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
const data = sorted.programs[sortKey as keyof typeof sorted.programs] ?? programs;
const [start, end] = parseRange(searchParams.get("range"), data.length);
return Response.json(data.slice(start, end), { headers: corsHeaders });
}
// Unknown endpoint
return new Response("Not Found", {
status: 404,
headers: corsHeaders,
});
},
});
console.log(`Server running on http://localhost:${PORT}`); |