File size: 9,442 Bytes
7a5aa35 9013903 68037ed 09c5b79 68037ed 09c5b79 68037ed 09c5b79 33ffc9c 09c5b79 7f60362 09c5b79 7f60362 09c5b79 9206294 0b3e194 09c5b79 0b3e194 9206294 09c5b79 33ffc9c 68037ed 9013903 68037ed 9013903 09c5b79 33ffc9c 09c5b79 e4feb9d 33ffc9c e4feb9d 09c5b79 33ffc9c cecb963 09c5b79 68037ed 7a5aa35 09c5b79 68037ed 7a5aa35 33ffc9c 7a5aa35 68037ed e4feb9d 7a5aa35 09c5b79 7a5aa35 e4feb9d 7a5aa35 09c5b79 7a5aa35 09c5b79 cecb963 09c5b79 cecb963 aef2282 09c5b79 cecb963 aef2282 09c5b79 cecb963 aef2282 09c5b79 cecb963 68037ed e4feb9d 09c5b79 e4feb9d 7a5aa35 33ffc9c 09c5b79 |
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 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
import { serve } from "bun";
import { type Repo } from "./types.ts";
// --- Configuration Constants ---
const PORT = 7860;
const REFRESH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes in milliseconds
const MAX_SEARCH_RESULTS = 25;
const DEFAULT_PAGINATION_SIZE = 10;
// Changed to a single URL for packages
const PACKAGE_URL =
"https://raw.githubusercontent.com/Zigistry/database/refs/heads/main/database/packages.json";
const PROGRAMS_URL =
"https://raw.githubusercontent.com/Zigistry/database/refs/heads/main/database/programs.json";
// --- CORS Headers ---
const CORS_HEADERS: Record<string, string> = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
};
// --- Data Store ---
interface DataStore {
packages: Repo[];
programs: Repo[];
sortedPackages: {
latest: Repo[];
mostUsed: Repo[];
};
sortedPrograms: {
latest: Repo[];
mostUsed: Repo[];
};
lastLoaded: number; // Timestamp of last successful data load
}
const dataStore: DataStore = {
packages: [],
programs: [],
sortedPackages: { latest: [], mostUsed: [] },
sortedPrograms: { latest: [], mostUsed: [] },
lastLoaded: 0,
};
// --- Helper Functions ---
function removeReadmeContent(repos: Repo[]): Repo[] {
return (repos.map((repo) => {
const { readme_content, ...rest } = repo; // Destructure to exclude readme_content
return rest;
}) as Repo[]);
}
const sortByDate = (a: Repo, b: Repo) =>
new Date(b.created_at ?? "").getTime() -
new Date(a.created_at ?? "").getTime();
const sortByUsage = (a: Repo, b: Repo) =>
(b.stargazers_count ?? 0) - (a.stargazers_count ?? 0);
function filterItems(
items: Repo[],
q: string | null,
filter: string | null
): Repo[] {
const lowerQ = q?.toLowerCase();
const lowerFilter = filter?.toLowerCase();
return items.filter(
({ name, full_name, description, topics, readme_content }) => {
// Apply topic filter first
if (
lowerFilter &&
!topics?.some((t) => t.toLowerCase() === lowerFilter)
) {
return false;
}
// If no search query, all items passing the filter are included
if (!lowerQ) {
return true;
}
// Check if any relevant field includes the search query
return [name, full_name, description, ...(topics ?? []), readme_content].some(
(field) => field?.toLowerCase().includes(lowerQ)
);
}
);
}
function getPaginated<T>(items: T[], page = 0, size = DEFAULT_PAGINATION_SIZE): T[] {
const start = page * size;
return items.slice(start, start + size);
}
function parseRange(str: string | null, max: number): [number, number] {
const match = str?.match(/^(\d+)\.\.(\d+)$/);
let start = 0;
let end = DEFAULT_PAGINATION_SIZE; // Default range if no match
if (match) {
start = parseInt(match[1] ?? "0", 10);
end = parseInt(match[2] ?? String(DEFAULT_PAGINATION_SIZE), 10);
}
return [Math.max(0, start), Math.min(max, end)];
}
// --- Data Loading and Caching ---
async function fetchData<T>(url: string): Promise<T | null> {
try {
const response = await fetch(url);
if (!response.ok) {
console.error(`Failed to fetch ${url}: ${response.statusText}`);
return null;
}
return (await response.json()) as T;
} catch (error) {
console.error(`Error fetching or parsing ${url}:`, error);
return null;
}
}
async function loadData() {
console.log("Attempting to load data...");
try {
// Fetch packages (now from a single URL)
const newPackages = await fetchData<Repo[]>(PACKAGE_URL);
// Fetch programs
const newPrograms = await fetchData<Repo[]>(PROGRAMS_URL);
if (newPackages && newPrograms) {
dataStore.packages = newPackages;
dataStore.programs = newPrograms;
// Pre-sort data after successful load
dataStore.sortedPackages.latest = [...newPackages].sort(sortByDate);
dataStore.sortedPackages.mostUsed = [...newPackages].sort(sortByUsage);
dataStore.sortedPrograms.latest = [...newPrograms].sort(sortByDate);
dataStore.sortedPrograms.mostUsed = [...newPrograms].sort(sortByUsage);
dataStore.lastLoaded = Date.now();
console.log("Data loaded successfully.");
} else {
console.warn("Failed to load all data. Retaining old data if available.");
}
} catch (error) {
console.error("Critical error during data loading:", error);
}
}
async function ensureDataLoaded() {
if (Date.now() - dataStore.lastLoaded > REFRESH_INTERVAL_MS) {
console.log("Data is stale, refreshing...");
await loadData();
} else if (dataStore.lastLoaded === 0) {
// Initial load if not already done
console.log("Initial data load...");
await loadData();
}
}
// Perform initial data load immediately on server start
await loadData();
// Set up interval for refreshing data
setInterval(ensureDataLoaded, REFRESH_INTERVAL_MS / 2); // Check more frequently than refresh interval
// --- API Route Handlers ---
function handleSearch(items: Repo[], searchParams: URLSearchParams): Response {
const q = searchParams.get("q")?.trim() ?? null;
const filter = searchParams.get("filter")?.trim() ?? null;
const result = filterItems(items, q, filter).slice(0, MAX_SEARCH_RESULTS);
return Response.json(removeReadmeContent(result), { headers: CORS_HEADERS });
}
function handleInfiniteScroll(items: Repo[], searchParams: URLSearchParams): Response {
const page = parseInt(searchParams.get("pageNumber") || "0", 10);
if (isNaN(page) || page < 0) {
return Response.json(
{ error: "Invalid page number. Must be a non-negative integer." },
{ status: 400, headers: CORS_HEADERS }
);
}
const result = getPaginated(items, page);
return Response.json(removeReadmeContent(result), { headers: CORS_HEADERS });
}
function handleSingleItem(
items: Repo[],
owner: string,
repo: string,
repoFrom: string | null = null
): Response {
const found = items.find(
(item) =>
item.full_name?.toLowerCase() === `${owner}/${repo}` &&
(repoFrom === null || item.repo_from?.toLowerCase() === repoFrom)
);
if (found) {
// Return the full item, including readme_content, for single item requests
return Response.json(found, { status: 200, headers: CORS_HEADERS });
}
return Response.json(
{ error: "Item not found" },
{ status: 404, headers: CORS_HEADERS }
);
}
function handleIndexDetails(
sortedData: { latest: Repo[]; mostUsed: Repo[] },
searchParams: URLSearchParams
): Response {
const section = searchParams.get("section");
if (section !== "latestRepos" && section !== "mostUsed") {
return Response.json(
{ error: "Invalid section. Must be 'latestRepos' or 'mostUsed'." },
{ status: 400, headers: CORS_HEADERS }
);
}
const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
const data = sortedData[sortKey];
const [start, end] = parseRange(searchParams.get("range"), data.length);
const result = data.slice(start, end);
return Response.json(removeReadmeContent(result), { headers: CORS_HEADERS });
}
// --- Router Mapping ---
const routes: Record<
string,
(url: URL) => Promise<Response> | Response
> = {
"/api/searchPackages": (url) =>
handleSearch(dataStore.packages, url.searchParams),
"/api/searchPrograms": (url) =>
handleSearch(dataStore.programs, url.searchParams),
"/api/infiniteScrollPackages": (url) =>
handleInfiniteScroll(dataStore.packages, url.searchParams),
"/api/infiniteScrollPrograms": (url) =>
handleInfiniteScroll(dataStore.programs, url.searchParams),
"/api/indexDetailsPackages": (url) =>
handleIndexDetails(dataStore.sortedPackages, url.searchParams),
"/api/indexDetailsPrograms": (url) =>
handleIndexDetails(dataStore.sortedPrograms, url.searchParams),
};
// --- Server ---
serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url);
const { pathname, searchParams } = url;
// Handle CORS preflight
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
...CORS_HEADERS,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
// Ensure data is loaded/refreshed before handling requests
await ensureDataLoaded();
// Direct route match
if (routes[pathname]) {
return routes[pathname](url);
}
// Regex-based routes for single items
const programMatch = pathname.match(
/^\/api\/programs\/([^/]+)\/([^/]+)\/([^/]+)$/
);
if (programMatch) {
const repo_from = programMatch[1]?.toLowerCase() ?? "";
const owner = programMatch[2]?.toLowerCase() ?? "";
const repo = programMatch[3]?.toLowerCase() ?? "";
return handleSingleItem(dataStore.programs, owner, repo, repo_from);
}
const packageMatch = pathname.match(
/^\/api\/packages\/([^/]+)\/([^/]+)\/([^/]+)$/
);
if (packageMatch) {
const repo_from = packageMatch[1]?.toLowerCase() ?? "";
const owner = packageMatch[2]?.toLowerCase() ?? "";
const repo = packageMatch[3]?.toLowerCase() ?? "";
return handleSingleItem(dataStore.packages, owner, repo, repo_from);
}
// Not found
return new Response("Not Found", {
status: 404,
headers: CORS_HEADERS,
});
},
});
console.log(`Server running on http://localhost:${PORT}`);
|