RohanVashisht commited on
Commit
09c5b79
·
1 Parent(s): aef2282

fixed stuff

Browse files
Files changed (3) hide show
  1. bun.lock +2 -4
  2. package.json +1 -1
  3. src/index.ts +314 -178
bun.lock CHANGED
@@ -12,13 +12,11 @@
12
  },
13
  },
14
  "packages": {
15
- "@types/bun": ["@types/[email protected].9", "", { "dependencies": { "bun-types": "1.2.9" } }, "sha512-epShhLGQYc4Bv/aceHbmBhOz1XgUnuTZgcxjxk+WXwNyDXavv5QHD1QEFV0FwbTSQtNq6g4ZcV6y0vZakTjswg=="],
16
 
17
  "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
18
 
19
- "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
20
-
21
- "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="],
22
 
23
  "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
24
 
 
12
  },
13
  },
14
  "packages": {
15
+ "@types/bun": ["@types/[email protected].14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
16
 
17
  "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
18
 
19
+ "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
 
 
20
 
21
  "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
22
 
package.json CHANGED
@@ -7,6 +7,6 @@
7
  "@types/bun": "latest"
8
  },
9
  "peerDependencies": {
10
- "typescript": "^5"
11
  }
12
  }
 
7
  "@types/bun": "latest"
8
  },
9
  "peerDependencies": {
10
+ "typescript": "^5.8.3"
11
  }
12
  }
src/index.ts CHANGED
@@ -1,250 +1,386 @@
1
  import { serve } from "bun";
2
  import { type Repo } from "./types.ts";
3
 
4
- const packagesUrls = [
5
- "https://raw.githubusercontent.com/Zigistry/database/refs/heads/main/database/packages.json",
6
- ];
 
 
7
 
8
- const programsUrl =
 
 
 
9
  "https://raw.githubusercontent.com/Zigistry/database/refs/heads/main/database/programs.json";
10
 
11
- let packages: Repo[] = [];
12
- let programs: Repo[] = [];
13
-
14
- async function loadData() {
15
- const packagesFile = await Promise.all(packagesUrls.map((url) => fetch(url)));
16
- packages = (await Promise.all(packagesFile.map((file) => file.json())))
17
- .flat() as Repo[];
18
-
19
- const programsFile = await fetch(programsUrl);
20
- programs = (await programsFile.json()) as Repo[];
21
- console.log("Data loaded");
22
- }
23
-
24
- await loadData();
25
-
26
- // Refresh the data every 30 minutes
27
- setInterval(loadData, 60 * 60 * 500);
28
-
29
  // --- CORS Headers ---
30
- const corsHeaders: Record<string, string> = {
31
  "Content-Type": "application/json",
32
  "Access-Control-Allow-Origin": "*",
33
  };
34
 
35
- // --- Sorting Helpers ---
36
- const sortByDate = (a: Repo, b: Repo) =>
37
- new Date(b.created_at ?? "").getTime() - new Date(a.created_at ?? "").getTime();
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- const sortByUsage = (a: Repo, b: Repo) =>
40
- (b.stargazers_count ?? 0) - (a.stargazers_count ?? 0);
 
 
 
 
 
41
 
42
- // --- Helper to remove readme content ---
 
 
 
 
 
 
 
43
  function removeReadmeContent(repos: Repo[]): Repo[] {
44
- return repos.map(repo => ({
45
- ...repo,
46
- readme_content: ""
47
- }));
48
  }
49
 
50
- // --- Pre-sorted Data ---
51
- function getSorted() {
52
- const packagesArray = packages as Repo[];
53
- const programsArray = programs as Repo[];
54
-
55
- return {
56
- packages: {
57
- latest: [...packagesArray].sort(sortByDate),
58
- mostUsed: [...packagesArray].sort(sortByUsage),
59
- },
60
- programs: {
61
- latest: [...programsArray].sort(sortByDate),
62
- mostUsed: [...programsArray].sort(sortByUsage),
63
- },
64
- };
65
- }
 
 
66
 
67
- // --- Filtering ---
 
 
 
 
 
 
68
  function filterItems(
69
  items: Repo[],
70
  q: string | null,
71
  filter: string | null
72
  ): Repo[] {
73
- return items.filter(({ name, full_name, description, topics, readme_content }) => {
74
- if (filter && !topics?.some((t) => t.toLowerCase() === filter)) return false;
75
- if (!q) return true;
76
- const lowerQ = q.toLowerCase();
77
- return [name, full_name, description, ...(topics ?? []), readme_content].some((field) =>
78
- field?.toLowerCase().includes(lowerQ)
79
- );
80
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
 
83
- // --- Pagination ---
84
- function getPaginated(items: Repo[], page = 0, size = 10): Repo[] {
 
 
 
 
 
 
85
  const start = page * size;
86
  return items.slice(start, start + size);
87
  }
88
 
89
- // --- Find by Owner/Repo ---
90
- function findItem(items: Repo[], owner: string, repo: string): Repo | undefined {
91
- return items.find(
92
- ({ full_name }) => full_name?.toLowerCase() === `${owner}/${repo}`
93
- );
94
- }
95
-
96
- // --- Parse Range ---
97
  function parseRange(str: string | null, max: number): [number, number] {
98
  const match = str?.match(/^(\d+)\.\.(\d+)$/);
99
- const [start, end] = match
100
- ? [parseInt(match[1] ?? "0", 10), parseInt(match[2] ?? "10", 10)]
101
- : [0, 10];
 
 
 
 
 
102
  return [Math.max(0, start), Math.min(max, end)];
103
  }
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  // --- Server ---
106
  serve({
107
- port: 7860,
108
  async fetch(req) {
109
  const url = new URL(req.url);
110
  const { pathname, searchParams } = url;
111
- const q = searchParams.get("q")?.trim().toLowerCase() ?? null;
112
- const filter = searchParams.get("filter")?.trim().toLowerCase() ?? null;
113
- const sorted = getSorted();
114
 
115
  // Handle CORS preflight
116
  if (req.method === "OPTIONS") {
117
  return new Response(null, {
118
  status: 204,
119
  headers: {
120
- ...corsHeaders,
121
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
122
  "Access-Control-Allow-Headers": "Content-Type",
123
  },
124
  });
125
  }
126
 
127
- // Search endpoints
128
- if (pathname === "/api/searchPackages") {
129
- const result = filterItems(packages, q, filter).slice(0, 25);
130
- return Response.json(removeReadmeContent(result), { headers: corsHeaders });
131
- }
132
- if (pathname === "/api/searchPrograms") {
133
- const result = filterItems(programs, q, filter).slice(0, 25);
134
- return Response.json(removeReadmeContent(result), { headers: corsHeaders });
135
- }
136
 
137
- // Infinite scroll endpoints
138
- if (pathname === "/api/infiniteScrollPackages") {
139
- const page = parseInt(searchParams.get("pageNumber") || "0", 10);
140
- if (isNaN(page) || page < 0)
141
- return Response.json(
142
- { error: "Invalid page number" },
143
- { status: 400, headers: corsHeaders }
144
- );
145
- const result = getPaginated(packages, page);
146
- return Response.json(removeReadmeContent(result), {
147
- headers: corsHeaders,
148
- });
149
- }
150
- if (pathname === "/api/infiniteScrollPrograms") {
151
- const page = parseInt(searchParams.get("pageNumber") || "0", 10);
152
- if (isNaN(page) || page < 0)
153
- return Response.json(
154
- { error: "Invalid page number" },
155
- { status: 400, headers: corsHeaders }
156
- );
157
- const result = getPaginated(programs, page);
158
- return Response.json(removeReadmeContent(result), {
159
- headers: corsHeaders,
160
- });
161
  }
162
 
163
- // Get single program by repo_from/owner/repo
164
- const programMatch = pathname.match(/^\/api\/programs\/([^/]+)\/([^/]+)\/([^/]+)$/);
 
 
165
  if (programMatch) {
166
  const repo_from = programMatch[1]?.toLowerCase() ?? "";
167
  const owner = programMatch[2]?.toLowerCase() ?? "";
168
  const repo = programMatch[3]?.toLowerCase() ?? "";
169
- const found = programs.find(
170
- (item) =>
171
- item.repo_from?.toLowerCase() === repo_from &&
172
- item.full_name?.toLowerCase() === `${owner}/${repo}`
173
- );
174
- if (found) {
175
- const result = { ...found, readme_content: "" };
176
- return Response.json(result, {
177
- status: 200,
178
- headers: corsHeaders,
179
- });
180
- }
181
- return Response.json({ error: "Program not found" }, {
182
- status: 404,
183
- headers: corsHeaders,
184
- });
185
  }
186
 
187
- // Get single package by repo_from/owner/repo
188
- const packageMatch = pathname.match(/^\/api\/packages\/([^/]+)\/([^/]+)\/([^/]+)$/);
 
189
  if (packageMatch) {
190
  const repo_from = packageMatch[1]?.toLowerCase() ?? "";
191
  const owner = packageMatch[2]?.toLowerCase() ?? "";
192
  const repo = packageMatch[3]?.toLowerCase() ?? "";
193
- const found = packages.find(
194
- (item) =>
195
- item.repo_from?.toLowerCase() === repo_from &&
196
- item.full_name?.toLowerCase() === `${owner}/${repo}`
197
- );
198
- if (found) {
199
- const result = { ...found, readme_content: "" };
200
- return Response.json(result, {
201
- status: 200,
202
- headers: corsHeaders,
203
- });
204
- }
205
- return Response.json({ error: "Package not found" }, {
206
- status: 404,
207
- headers: corsHeaders,
208
- });
209
- }
210
-
211
-
212
- // Index details endpoints
213
- if (pathname === "/api/indexDetailsPackages") {
214
- const section = searchParams.get("section");
215
- if (section !== "latestRepos" && section !== "mostUsed") {
216
- return Response.json(
217
- { error: "Invalid section" },
218
- { status: 400, headers: corsHeaders }
219
- );
220
- }
221
- const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
222
- const data = sorted.packages[sortKey as keyof typeof sorted.packages] ?? packages;
223
- const [start, end] = parseRange(searchParams.get("range"), data.length);
224
- const result = data.slice(start, end);
225
- return Response.json(removeReadmeContent(result), { headers: corsHeaders });
226
- }
227
- if (pathname === "/api/indexDetailsPrograms") {
228
- const section = searchParams.get("section");
229
- if (section !== "latestRepos" && section !== "mostUsed") {
230
- return Response.json(
231
- { error: "Invalid section" },
232
- { status: 400, headers: corsHeaders }
233
- );
234
- }
235
- const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
236
- const data = sorted.programs[sortKey as keyof typeof sorted.programs] ?? programs;
237
- const [start, end] = parseRange(searchParams.get("range"), data.length);
238
- const result = data.slice(start, end);
239
- return Response.json(removeReadmeContent(result), { headers: corsHeaders });
240
  }
241
 
242
  // Not found
243
  return new Response("Not Found", {
244
  status: 404,
245
- headers: corsHeaders,
246
  });
247
  },
248
  });
249
 
250
- console.log("Server running on http://localhost:7860");
 
1
  import { serve } from "bun";
2
  import { type Repo } from "./types.ts";
3
 
4
+ // --- Configuration Constants ---
5
+ const PORT = 7860;
6
+ const REFRESH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes in milliseconds
7
+ const MAX_SEARCH_RESULTS = 25;
8
+ const DEFAULT_PAGINATION_SIZE = 10;
9
 
10
+ // Changed to a single URL for packages
11
+ const PACKAGE_URL =
12
+ "https://raw.githubusercontent.com/Zigistry/database/refs/heads/main/database/packages.json";
13
+ const PROGRAMS_URL =
14
  "https://raw.githubusercontent.com/Zigistry/database/refs/heads/main/database/programs.json";
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  // --- CORS Headers ---
17
+ const CORS_HEADERS: Record<string, string> = {
18
  "Content-Type": "application/json",
19
  "Access-Control-Allow-Origin": "*",
20
  };
21
 
22
+ // --- Data Store ---
23
+ interface DataStore {
24
+ packages: Repo[];
25
+ programs: Repo[];
26
+ sortedPackages: {
27
+ latest: Repo[];
28
+ mostUsed: Repo[];
29
+ };
30
+ sortedPrograms: {
31
+ latest: Repo[];
32
+ mostUsed: Repo[];
33
+ };
34
+ lastLoaded: number; // Timestamp of last successful data load
35
+ }
36
 
37
+ const dataStore: DataStore = {
38
+ packages: [],
39
+ programs: [],
40
+ sortedPackages: { latest: [], mostUsed: [] },
41
+ sortedPrograms: { latest: [], mostUsed: [] },
42
+ lastLoaded: 0,
43
+ };
44
 
45
+ // --- Helper Functions ---
46
+
47
+ /**
48
+ * Removes the 'readme_content' field from an array of Repo objects.
49
+ * Creates a shallow copy of each repo to avoid modifying the original data.
50
+ * @param repos - An array of Repo objects.
51
+ * @returns A new array of Repo objects without the 'readme_content'.
52
+ */
53
  function removeReadmeContent(repos: Repo[]): Repo[] {
54
+ return repos.map((repo) => {
55
+ const { readme_content, ...rest } = repo; // Destructure to exclude readme_content
56
+ return rest;
57
+ });
58
  }
59
 
60
+ /**
61
+ * Sorts repositories by creation date (newest first).
62
+ * @param a - First Repo object.
63
+ * @param b - Second Repo object.
64
+ * @returns A number indicating sort order.
65
+ */
66
+ const sortByDate = (a: Repo, b: Repo) =>
67
+ new Date(b.created_at ?? "").getTime() -
68
+ new Date(a.created_at ?? "").getTime();
69
+
70
+ /**
71
+ * Sorts repositories by stargazers count (most used first).
72
+ * @param a - First Repo object.
73
+ * @param b - Second Repo object.
74
+ * @returns A number indicating sort order.
75
+ */
76
+ const sortByUsage = (a: Repo, b: Repo) =>
77
+ (b.stargazers_count ?? 0) - (a.stargazers_count ?? 0);
78
 
79
+ /**
80
+ * Filters an array of Repo objects based on a search query and a topic filter.
81
+ * @param items - The array of Repo objects to filter.
82
+ * @param q - The search query string (case-insensitive).
83
+ * @param filter - The topic filter string (case-insensitive).
84
+ * @returns A new array of filtered Repo objects.
85
+ */
86
  function filterItems(
87
  items: Repo[],
88
  q: string | null,
89
  filter: string | null
90
  ): Repo[] {
91
+ const lowerQ = q?.toLowerCase();
92
+ const lowerFilter = filter?.toLowerCase();
93
+
94
+ return items.filter(
95
+ ({ name, full_name, description, topics, readme_content }) => {
96
+ // Apply topic filter first
97
+ if (
98
+ lowerFilter &&
99
+ !topics?.some((t) => t.toLowerCase() === lowerFilter)
100
+ ) {
101
+ return false;
102
+ }
103
+
104
+ // If no search query, all items passing the filter are included
105
+ if (!lowerQ) {
106
+ return true;
107
+ }
108
+
109
+ // Check if any relevant field includes the search query
110
+ return [name, full_name, description, ...(topics ?? []), readme_content].some(
111
+ (field) => field?.toLowerCase().includes(lowerQ)
112
+ );
113
+ }
114
+ );
115
  }
116
 
117
+ /**
118
+ * Paginates an array of items.
119
+ * @param items - The array of items to paginate.
120
+ * @param page - The page number (0-indexed).
121
+ * @param size - The number of items per page.
122
+ * @returns A new array containing items for the specified page.
123
+ */
124
+ function getPaginated<T>(items: T[], page = 0, size = DEFAULT_PAGINATION_SIZE): T[] {
125
  const start = page * size;
126
  return items.slice(start, start + size);
127
  }
128
 
129
+ /**
130
+ * Parses a range string (e.g., "0..10") into start and end numbers.
131
+ * Ensures the range is within valid bounds.
132
+ * @param str - The range string.
133
+ * @param max - The maximum possible value for the end of the range.
134
+ * @returns A tuple [start, end].
135
+ */
 
136
  function parseRange(str: string | null, max: number): [number, number] {
137
  const match = str?.match(/^(\d+)\.\.(\d+)$/);
138
+ let start = 0;
139
+ let end = DEFAULT_PAGINATION_SIZE; // Default range if no match
140
+
141
+ if (match) {
142
+ start = parseInt(match[1] ?? "0", 10);
143
+ end = parseInt(match[2] ?? String(DEFAULT_PAGINATION_SIZE), 10);
144
+ }
145
+
146
  return [Math.max(0, start), Math.min(max, end)];
147
  }
148
 
149
+ // --- Data Loading and Caching ---
150
+
151
+ /**
152
+ * Fetches data from a given URL and parses it as JSON.
153
+ * @param url - The URL to fetch.
154
+ * @returns A Promise that resolves with the parsed JSON data, or null if an error occurs.
155
+ */
156
+ async function fetchData<T>(url: string): Promise<T | null> {
157
+ try {
158
+ const response = await fetch(url);
159
+ if (!response.ok) {
160
+ console.error(`Failed to fetch ${url}: ${response.statusText}`);
161
+ return null;
162
+ }
163
+ return (await response.json()) as T;
164
+ } catch (error) {
165
+ console.error(`Error fetching or parsing ${url}:`, error);
166
+ return null;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Loads and processes all repository data (packages and programs).
172
+ * Updates the global dataStore and pre-sorts the data.
173
+ */
174
+ async function loadData() {
175
+ console.log("Attempting to load data...");
176
+ try {
177
+ // Fetch packages (now from a single URL)
178
+ const newPackages = await fetchData<Repo[]>(PACKAGE_URL);
179
+
180
+ // Fetch programs
181
+ const newPrograms = await fetchData<Repo[]>(PROGRAMS_URL);
182
+
183
+ if (newPackages && newPrograms) {
184
+ dataStore.packages = newPackages;
185
+ dataStore.programs = newPrograms;
186
+
187
+ // Pre-sort data after successful load
188
+ dataStore.sortedPackages.latest = [...newPackages].sort(sortByDate);
189
+ dataStore.sortedPackages.mostUsed = [...newPackages].sort(sortByUsage);
190
+ dataStore.sortedPrograms.latest = [...newPrograms].sort(sortByDate);
191
+ dataStore.sortedPrograms.mostUsed = [...newPrograms].sort(sortByUsage);
192
+
193
+ dataStore.lastLoaded = Date.now();
194
+ console.log("Data loaded successfully.");
195
+ } else {
196
+ console.warn("Failed to load all data. Retaining old data if available.");
197
+ }
198
+ } catch (error) {
199
+ console.error("Critical error during data loading:", error);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Checks if data needs to be refreshed and triggers loadData if necessary.
205
+ */
206
+ async function ensureDataLoaded() {
207
+ if (Date.now() - dataStore.lastLoaded > REFRESH_INTERVAL_MS) {
208
+ console.log("Data is stale, refreshing...");
209
+ await loadData();
210
+ } else if (dataStore.lastLoaded === 0) {
211
+ // Initial load if not already done
212
+ console.log("Initial data load...");
213
+ await loadData();
214
+ }
215
+ }
216
+
217
+ // Perform initial data load immediately on server start
218
+ await loadData();
219
+
220
+ // Set up interval for refreshing data
221
+ setInterval(ensureDataLoaded, REFRESH_INTERVAL_MS / 2); // Check more frequently than refresh interval
222
+
223
+ // --- API Route Handlers ---
224
+
225
+ /**
226
+ * Handles search requests for packages or programs.
227
+ * @param items - The array of Repo items to search.
228
+ * @param searchParams - URLSearchParams object from the request.
229
+ * @returns A Response object with filtered and truncated results.
230
+ */
231
+ function handleSearch(items: Repo[], searchParams: URLSearchParams): Response {
232
+ const q = searchParams.get("q")?.trim() ?? null;
233
+ const filter = searchParams.get("filter")?.trim() ?? null;
234
+ const result = filterItems(items, q, filter).slice(0, MAX_SEARCH_RESULTS);
235
+ return Response.json(removeReadmeContent(result), { headers: CORS_HEADERS });
236
+ }
237
+
238
+ /**
239
+ * Handles infinite scroll requests for packages or programs.
240
+ * @param items - The array of Repo items to paginate.
241
+ * @param searchParams - URLSearchParams object from the request.
242
+ * @returns A Response object with paginated results.
243
+ */
244
+ function handleInfiniteScroll(items: Repo[], searchParams: URLSearchParams): Response {
245
+ const page = parseInt(searchParams.get("pageNumber") || "0", 10);
246
+ if (isNaN(page) || page < 0) {
247
+ return Response.json(
248
+ { error: "Invalid page number. Must be a non-negative integer." },
249
+ { status: 400, headers: CORS_HEADERS }
250
+ );
251
+ }
252
+ const result = getPaginated(items, page);
253
+ return Response.json(removeReadmeContent(result), { headers: CORS_HEADERS });
254
+ }
255
+
256
+ /**
257
+ * Handles requests for a single package or program by owner/repo name.
258
+ * @param items - The array of Repo items to search within.
259
+ * @param owner - The owner part of the full_name.
260
+ * @param repo - The repo part of the full_name.
261
+ * @param repoFrom - The repo_from identifier.
262
+ * @returns A Response object with the found item or a 404 error.
263
+ */
264
+ function handleSingleItem(
265
+ items: Repo[],
266
+ owner: string,
267
+ repo: string,
268
+ repoFrom: string | null = null
269
+ ): Response {
270
+ const found = items.find(
271
+ (item) =>
272
+ item.full_name?.toLowerCase() === `${owner}/${repo}` &&
273
+ (repoFrom === null || item.repo_from?.toLowerCase() === repoFrom)
274
+ );
275
+
276
+ if (found) {
277
+ // Return the full item, including readme_content, for single item requests
278
+ return Response.json(found, { status: 200, headers: CORS_HEADERS });
279
+ }
280
+ return Response.json(
281
+ { error: "Item not found" },
282
+ { status: 404, headers: CORS_HEADERS }
283
+ );
284
+ }
285
+
286
+ /**
287
+ * Handles requests for index details (latest or most used).
288
+ * @param sortedData - The pre-sorted data object (e.g., dataStore.sortedPackages).
289
+ * @param searchParams - URLSearchParams object from the request.
290
+ * @returns A Response object with sliced sorted results.
291
+ */
292
+ function handleIndexDetails(
293
+ sortedData: { latest: Repo[]; mostUsed: Repo[] },
294
+ searchParams: URLSearchParams
295
+ ): Response {
296
+ const section = searchParams.get("section");
297
+ if (section !== "latestRepos" && section !== "mostUsed") {
298
+ return Response.json(
299
+ { error: "Invalid section. Must be 'latestRepos' or 'mostUsed'." },
300
+ { status: 400, headers: CORS_HEADERS }
301
+ );
302
+ }
303
+
304
+ const sortKey = section === "latestRepos" ? "latest" : "mostUsed";
305
+ const data = sortedData[sortKey];
306
+ const [start, end] = parseRange(searchParams.get("range"), data.length);
307
+ const result = data.slice(start, end);
308
+ return Response.json(removeReadmeContent(result), { headers: CORS_HEADERS });
309
+ }
310
+
311
+ // --- Router Mapping ---
312
+ const routes: Record<
313
+ string,
314
+ (url: URL) => Promise<Response> | Response
315
+ > = {
316
+ "/api/searchPackages": (url) =>
317
+ handleSearch(dataStore.packages, url.searchParams),
318
+ "/api/searchPrograms": (url) =>
319
+ handleSearch(dataStore.programs, url.searchParams),
320
+ "/api/infiniteScrollPackages": (url) =>
321
+ handleInfiniteScroll(dataStore.packages, url.searchParams),
322
+ "/api/infiniteScrollPrograms": (url) =>
323
+ handleInfiniteScroll(dataStore.programs, url.searchParams),
324
+ "/api/indexDetailsPackages": (url) =>
325
+ handleIndexDetails(dataStore.sortedPackages, url.searchParams),
326
+ "/api/indexDetailsPrograms": (url) =>
327
+ handleIndexDetails(dataStore.sortedPrograms, url.searchParams),
328
+ };
329
+
330
  // --- Server ---
331
  serve({
332
+ port: PORT,
333
  async fetch(req) {
334
  const url = new URL(req.url);
335
  const { pathname, searchParams } = url;
 
 
 
336
 
337
  // Handle CORS preflight
338
  if (req.method === "OPTIONS") {
339
  return new Response(null, {
340
  status: 204,
341
  headers: {
342
+ ...CORS_HEADERS,
343
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
344
  "Access-Control-Allow-Headers": "Content-Type",
345
  },
346
  });
347
  }
348
 
349
+ // Ensure data is loaded/refreshed before handling requests
350
+ await ensureDataLoaded();
 
 
 
 
 
 
 
351
 
352
+ // Direct route match
353
+ if (routes[pathname]) {
354
+ return routes[pathname](url);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  }
356
 
357
+ // Regex-based routes for single items
358
+ const programMatch = pathname.match(
359
+ /^\/api\/programs\/([^/]+)\/([^/]+)\/([^/]+)$/
360
+ );
361
  if (programMatch) {
362
  const repo_from = programMatch[1]?.toLowerCase() ?? "";
363
  const owner = programMatch[2]?.toLowerCase() ?? "";
364
  const repo = programMatch[3]?.toLowerCase() ?? "";
365
+ return handleSingleItem(dataStore.programs, owner, repo, repo_from);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  }
367
 
368
+ const packageMatch = pathname.match(
369
+ /^\/api\/packages\/([^/]+)\/([^/]+)\/([^/]+)$/
370
+ );
371
  if (packageMatch) {
372
  const repo_from = packageMatch[1]?.toLowerCase() ?? "";
373
  const owner = packageMatch[2]?.toLowerCase() ?? "";
374
  const repo = packageMatch[3]?.toLowerCase() ?? "";
375
+ return handleSingleItem(dataStore.packages, owner, repo, repo_from);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
 
378
  // Not found
379
  return new Response("Not Found", {
380
  status: 404,
381
+ headers: CORS_HEADERS,
382
  });
383
  },
384
  });
385
 
386
+ console.log(`Server running on http://localhost:${PORT}`);