Skip to content

Commit 94bbd59

Browse files
author
DevBot
committed
fix(reader): catch all handler errors, proper Uint8Array wrappers
1 parent f14be41 commit 94bbd59

1 file changed

Lines changed: 113 additions & 76 deletions

File tree

  • examples/deno-desktop-reader

examples/deno-desktop-reader/main.ts

Lines changed: 113 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,119 +2,156 @@
22
* openElement Desktop Reader — HTTP server.
33
*
44
* Serves the SPA client, API endpoints, and PDF files.
5-
* PDF text indexing is lazy (on first /api/search request) to avoid
6-
* blocking Deno.serve() startup in desktop mode.
5+
* All handler errors are caught to avoid breaking the desktop webview.
76
*/
87

9-
import {
10-
indexBook,
11-
loadSearchIndex,
12-
saveSearchIndex,
13-
search,
14-
} from "./app/search.ts";
8+
import { indexBook, search } from "./app/search.ts";
159

1610
// Cache paths
1711
const HOME = Deno.env.get("HOME") ?? ".";
1812
const CACHE_DIR = `${HOME}/.open-reader`;
1913
const BOOKS_DIR = `${CACHE_DIR}/books`;
2014
const FIXTURES_DIR = new URL("./fixtures/books/", import.meta.url).pathname;
21-
const BOOKS_JSON = new URL("./fixtures/books.json", import.meta.url);
15+
const BOOKS_JSON_URL = new URL("./fixtures/books.json", import.meta.url);
16+
const APP_DIR = new URL("./app/", import.meta.url);
17+
const DIST_DIR = new URL("./dist/", import.meta.url);
2218

2319
let searchIndexReady = false;
2420

25-
function serveHtml(): Response {
26-
const html = Deno.readTextFileSync(
27-
new URL("./app/index.html", import.meta.url),
28-
);
29-
return new Response(html, {
30-
headers: { "content-type": "text/html" },
31-
});
21+
function readTextSafe(url: URL): string | null {
22+
try {
23+
return Deno.readTextFileSync(url);
24+
} catch {
25+
return null;
26+
}
27+
}
28+
29+
function readFileSafe(url: URL): Uint8Array | null {
30+
try {
31+
return Deno.readFileSync(url);
32+
} catch {
33+
return null;
34+
}
3235
}
3336

34-
function serveJson(data: unknown): Response {
37+
function statSafe(path: string): boolean {
38+
try {
39+
Deno.statSync(path);
40+
return true;
41+
} catch {
42+
return false;
43+
}
44+
}
45+
46+
function html(body: string): Response {
47+
return new Response(body, { headers: { "content-type": "text/html" } });
48+
}
49+
50+
function json(data: unknown): Response {
3551
return new Response(JSON.stringify(data), {
3652
headers: { "content-type": "application/json" },
3753
});
3854
}
3955

40-
function serve404(): Response {
56+
function pdf(bytes: Uint8Array): Response {
57+
return new Response(new Uint8Array(bytes), {
58+
headers: { "content-type": "application/pdf" },
59+
});
60+
}
61+
62+
function serveFile(bytes: Uint8Array, ext: string): Response {
63+
const mime: Record<string, string> = {
64+
".js": "application/javascript",
65+
".css": "text/css",
66+
".html": "text/html",
67+
};
68+
return new Response(new Uint8Array(bytes), {
69+
headers: { "content-type": mime[ext] ?? "application/octet-stream" },
70+
});
71+
}
72+
73+
function notFound(): Response {
4174
return new Response("Not Found", { status: 404 });
4275
}
4376

44-
/** Lazy-init the search index on first /api/search call */
77+
function serverError(msg: string): Response {
78+
return new Response(msg, {
79+
status: 500,
80+
headers: { "content-type": "text/plain" },
81+
});
82+
}
83+
4584
async function ensureSearchIndex(): Promise<void> {
4685
if (searchIndexReady) return;
4786
searchIndexReady = true;
48-
87+
const raw = readTextSafe(BOOKS_JSON_URL);
88+
if (!raw) return;
4989
try {
50-
const books = JSON.parse(Deno.readTextFileSync(BOOKS_JSON));
90+
const books = JSON.parse(raw);
5191
for (const book of books) {
5292
const path = `${FIXTURES_DIR}/${book.fileName}`;
93+
if (!statSafe(path)) continue;
5394
try {
54-
Deno.statSync(path);
5595
await indexBook(path, book.id, CACHE_DIR);
56-
} catch {
57-
// PDF not found — skip
58-
}
96+
} catch { /* skip */ }
5997
}
60-
console.log("[reader] Search index ready");
61-
} catch (err) {
62-
console.warn("[reader] Failed to build search index:", err);
63-
}
98+
} catch { /* skip */ }
6499
}
65100

66-
Deno.serve(async (req: Request) => {
67-
const url = new URL(req.url);
68-
69-
// API: books list
70-
if (url.pathname === "/api/books") {
71-
const books = JSON.parse(Deno.readTextFileSync(BOOKS_JSON));
72-
return serveJson(books);
73-
}
101+
Deno.serve((req: Request) => {
102+
try {
103+
const url = new URL(req.url);
104+
const pathname = url.pathname;
105+
const ext = pathname.slice(pathname.lastIndexOf("."));
74106

75-
// API: search (lazy-init index)
76-
if (url.pathname === "/api/search") {
77-
const q = url.searchParams.get("q");
78-
if (!q) return serveJson([]);
79-
await ensureSearchIndex();
80-
return serveJson(search(q, CACHE_DIR));
81-
}
107+
// API: books
108+
if (pathname === "/api/books") {
109+
const raw = readTextSafe(BOOKS_JSON_URL);
110+
return raw ? json(JSON.parse(raw)) : json([]);
111+
}
82112

83-
// PDF files — try cache, fallback fixtures
84-
if (url.pathname.startsWith("/books/")) {
85-
const fileName = url.pathname.slice("/books/".length);
86-
for (const dir of [BOOKS_DIR, FIXTURES_DIR]) {
113+
// API: search
114+
if (pathname === "/api/search") {
115+
const q = url.searchParams.get("q");
116+
if (!q) return json([]);
117+
ensureSearchIndex(); // fire-and-forget
87118
try {
88-
const file = Deno.readFileSync(`${dir}/${fileName}`);
89-
return new Response(file, {
90-
headers: { "content-type": "application/pdf" },
91-
});
92-
} catch { /* try next */ }
119+
return json(search(q, CACHE_DIR));
120+
} catch {
121+
return json([]);
122+
}
93123
}
94-
return serve404();
95-
}
96124

97-
// Static assets
98-
const ext = url.pathname.slice(url.pathname.lastIndexOf("."));
99-
const mime: Record<string, string> = {
100-
".js": "application/javascript",
101-
".css": "text/css",
102-
".html": "text/html",
103-
".ts": "application/javascript",
104-
};
105-
if (url.pathname.startsWith("/dist/") || url.pathname.startsWith("/app/")) {
106-
try {
107-
const file = Deno.readFileSync(
108-
new URL(`.${url.pathname}`, import.meta.url),
109-
);
110-
return new Response(file, {
111-
headers: { "content-type": mime[ext] ?? "application/octet-stream" },
112-
});
113-
} catch {
114-
return serve404();
125+
// PDF files
126+
if (pathname.startsWith("/books/")) {
127+
const name = pathname.slice("/books/".length);
128+
for (const dir of [BOOKS_DIR, FIXTURES_DIR]) {
129+
const p = `${dir}/${name}`;
130+
if (!statSafe(p)) continue;
131+
try {
132+
return pdf(Deno.readFileSync(p));
133+
} catch { /* try next */ }
134+
}
135+
return notFound();
115136
}
116-
}
117137

118-
// SPA fallback
119-
return serveHtml();
138+
// Static assets from dist/
139+
if (pathname.startsWith("/dist/")) {
140+
const file = readFileSafe(new URL(`.${pathname}`, DIST_DIR));
141+
return file ? serveFile(file, ext) : notFound();
142+
}
143+
144+
// Static assets from app/
145+
if (pathname.startsWith("/app/")) {
146+
const file = readFileSafe(new URL(`.${pathname}`, APP_DIR));
147+
return file ? serveFile(file, ext) : notFound();
148+
}
149+
150+
// SPA fallback
151+
const indexHtml = readTextSafe(new URL("./index.html", APP_DIR));
152+
return indexHtml ? html(indexHtml) : serverError("index.html not found");
153+
} catch (err) {
154+
console.error("[reader] Handler error:", err);
155+
return serverError(String(err));
156+
}
120157
});

0 commit comments

Comments
 (0)