@lexho111/plainblog 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Article.js CHANGED
@@ -45,12 +45,9 @@ export default class Article {
45
45
  */
46
46
  toJSON() {
47
47
  // Return a plain object with the desired properties
48
- let date = new Date();
49
- try {
50
- date = new Date(this.createdAt).toISOString();
51
- } catch (err) {
52
- console.error(err);
53
- }
48
+ const date = !isNaN(this.createdAt)
49
+ ? new Date(this.createdAt).toISOString()
50
+ : new Date().toISOString();
54
51
  return {
55
52
  id: this.id,
56
53
  title: this.title,
package/Auth.js CHANGED
@@ -16,14 +16,14 @@ export default class Auth {
16
16
  if (!req.headers.cookie) {
17
17
  const time_end = performance.now();
18
18
  const duration = time_end - time_start;
19
- debug_perf("isAuthenticated took " + duration + "ms", duration);
19
+ debug_perf("isAuthenticated", duration);
20
20
  return false;
21
21
  }
22
22
  const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
23
23
  const result = match ? this.sessions.has(match[1]) : false;
24
24
  const time_end = performance.now();
25
25
  const duration = time_end - time_start;
26
- debug_perf("isAuthenticated took " + duration + "ms", duration);
26
+ debug_perf("isAuthenticated", duration);
27
27
  return result;
28
28
  }
29
29
 
package/Blog.js CHANGED
@@ -163,74 +163,88 @@ export default class Blog {
163
163
  if (this.#initPromise) return this.#initPromise;
164
164
 
165
165
  this.#initPromise = (async () => {
166
- await this.assetManager.init();
166
+ try {
167
+ await this.assetManager.init();
168
+
169
+ if (this.#isExternalAPI) {
170
+ console.log("external API");
171
+ await this.#loadFromAPI();
172
+ } else {
173
+ debug(`database: ${this.database.type}`);
174
+ if (!this.#databaseModel) {
175
+ if (this.database.type === "file") {
176
+ this.#databaseModel = new DatabaseModel(
177
+ new WorkerAdapter(this.database),
178
+ );
179
+ } else if (this.database.type === "sqlite") {
180
+ this.#databaseModel = new DatabaseModel(
181
+ new SqliteAdapter(this.database),
182
+ );
183
+ }
184
+ }
185
+ // Timeout nach 60 Sekunden, um Hängenbleiben zu verhindern
186
+ let timeoutHandle;
187
+ const timeout = new Promise((_, reject) => {
188
+ timeoutHandle = setTimeout(
189
+ () => reject(new Error("Database initialization timed out")),
190
+ 60000,
191
+ );
192
+ });
193
+ try {
194
+ await Promise.race([this.#databaseModel.initialize(), timeout]);
195
+ } finally {
196
+ clearTimeout(timeoutHandle); // Timer aufräumen, sobald fertig
197
+ }
167
198
 
168
- if (this.#isExternalAPI) {
169
- console.log("external API");
170
- await this.#loadFromAPI();
171
- } else {
172
- debug(`database: ${this.database.type}`);
173
- if (!this.#databaseModel) {
174
- if (this.database.type === "file") {
175
- this.#databaseModel = new DatabaseModel(
176
- new WorkerAdapter(this.database),
199
+ if (this.#databaseModel.isReady()) {
200
+ console.log(`connected to database`);
201
+ console.log(
202
+ `using ${this.#databaseModel.getType()} database '${this.#databaseModel.getDBName()}'`,
177
203
  );
178
- } else if (this.database.type === "sqlite") {
179
- this.#databaseModel = new DatabaseModel(
180
- new SqliteAdapter(this.database),
204
+ }
205
+ this.#articles.setDatabase(this.#databaseModel);
206
+ const [dbTitle, dbArticles] = await Promise.all([
207
+ this.#databaseModel.getBlogTitle(),
208
+ this.#databaseModel.findAll(-1),
209
+ ]);
210
+ debug("dbArticles.size(): %d", dbArticles.length);
211
+ //log(filename, "dbArticles.size(): " + dbArticles.length);
212
+ //log(filename, "dbArticles.size(): " + dbArticles.length, "Blog.js");
213
+ debug("all articles in Blog after loading from db");
214
+
215
+ // Displays a beautiful table in the console
216
+ //table(dbArticles)
217
+
218
+ if (dbArticles.length == 0) {
219
+ dbArticles.push(
220
+ new Article(
221
+ 1,
222
+ "Sample Entry #1",
223
+ "Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
224
+ new Date(),
225
+ ),
226
+ );
227
+ dbArticles.push(
228
+ new Article(
229
+ 2,
230
+ "Sample Entry #2",
231
+ "Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
232
+ new Date(),
233
+ ),
181
234
  );
182
235
  }
236
+ if (this.assetManager.reloadStylesOnGET)
237
+ console.log("reload scripts and styles on get-request");
238
+ let title = "";
239
+ if (this.#title != null && this.#title.length > 0)
240
+ title = this.#title; // use blog title if set
241
+ else title = dbTitle; // use title from the database
242
+ const responseData = { title: title, articles: dbArticles };
243
+ this.#applyBlogData(responseData);
183
244
  }
184
- const p = this.#databaseModel.initialize();
185
- while (!this.#databaseModel.isReady()) {
186
- await new Promise((resolve) => setTimeout(resolve, 100));
187
- }
188
- if (this.#databaseModel.isReady()) {
189
- console.log(`connected to database`);
190
- console.log(
191
- `using ${this.#databaseModel.getType()} database '${this.#databaseModel.getDBName()}'`,
192
- );
193
- }
194
- await p;
195
- this.#articles.setDatabase(this.#databaseModel);
196
- const [dbTitle, dbArticles] = await Promise.all([
197
- this.#databaseModel.getBlogTitle(),
198
- this.#databaseModel.findAll(-1),
199
- ]);
200
- debug("dbArticles.size(): %d", dbArticles.length);
201
- //log(filename, "dbArticles.size(): " + dbArticles.length);
202
- //log(filename, "dbArticles.size(): " + dbArticles.length, "Blog.js");
203
- debug("all articles in Blog after loading from db");
204
-
205
- // Displays a beautiful table in the console
206
- //table(dbArticles)
207
-
208
- if (dbArticles.length == 0) {
209
- dbArticles.push(
210
- new Article(
211
- 1,
212
- "Sample Entry #1",
213
- "Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.",
214
- new Date(),
215
- ),
216
- );
217
- dbArticles.push(
218
- new Article(
219
- 2,
220
- "Sample Entry #2",
221
- "Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.",
222
- new Date(),
223
- ),
224
- );
225
- }
226
- if (this.assetManager.reloadStylesOnGET)
227
- console.log("reload scripts and styles on get-request");
228
- let title = "";
229
- if (this.#title != null && this.#title.length > 0)
230
- title = this.#title; // use blog title if set
231
- else title = dbTitle; // use title from the database
232
- const responseData = { title: title, articles: dbArticles };
233
- this.#applyBlogData(responseData);
245
+ } catch (err) {
246
+ this.#initPromise = null; // Reset promise on error to allow retry
247
+ throw err;
234
248
  }
235
249
  })();
236
250
  return this.#initPromise;
@@ -267,11 +281,6 @@ export default class Blog {
267
281
  /** start a http server with default port 8080 */
268
282
  async startServer(port = 8080) {
269
283
  await this.init();
270
- // is database ready
271
- while (this.#databaseModel && !this.#databaseModel.isReady()) {
272
- await new Promise((resolve) => setTimeout(resolve, 100));
273
- //console.log("is not ready;");
274
- }
275
284
 
276
285
  this.#server = await createServer(
277
286
  this.postArticle.bind(this),
@@ -285,14 +294,16 @@ export default class Blog {
285
294
  this.database,
286
295
  );
287
296
 
288
- await new Promise((resolve, reject) => {
289
- const errorHandler = (err) => reject(err);
290
- this.#server.once("error", errorHandler);
291
- this.#server.listen(port, "0.0.0.0", () => {
292
- this.#server.removeListener("error", errorHandler);
293
- resolve();
294
- });
295
- });
297
+ await new Promise(
298
+ function f1(resolve, reject) {
299
+ const errorHandler = (err) => reject(err);
300
+ this.#server.once("error", errorHandler);
301
+ this.#server.listen(port, "0.0.0.0", () => {
302
+ this.#server.removeListener("error", errorHandler);
303
+ resolve();
304
+ });
305
+ }.bind(this),
306
+ );
296
307
  console.log(`server running at http://localhost:${port}/`);
297
308
  }
298
309
 
@@ -363,7 +374,10 @@ export default class Blog {
363
374
 
364
375
  /** render this blog content to valid html */
365
376
  async toHTML(loggedin) {
366
- const articles_after = await this.#articles.findAll(null, null, 50);
377
+ let articles_after = this.#articles.findAll(null, null, 50);
378
+ if (articles_after instanceof Promise) {
379
+ articles_after = await articles_after;
380
+ }
367
381
  // prettier-ignore
368
382
  debug("fetched %d articles", articles_after.length);
369
383
  const data = {
package/debug-loader.js CHANGED
@@ -27,14 +27,29 @@ export function createDebug(namespace) {
27
27
 
28
28
  const debug_perf_ns = createDebug("plainblog:performance");
29
29
  export function debug_perf(message, duration) {
30
+ let duration_rounded = duration.toFixed(2);
31
+ debug_perf_ns(message.padEnd(35) + " took " + duration_rounded + "ms");
30
32
  const threshold = 25;
31
33
  if (duration > threshold) {
32
34
  debug_perf_ns(message + ` WARNING! threshold > ${threshold}ms reached!`);
33
35
  }
34
36
  }
35
37
 
38
+ export async function measure_perf(message, fn) {
39
+ const start = performance.now();
40
+ try {
41
+ return await fn();
42
+ } finally {
43
+ const duration = performance.now() - start;
44
+ debug_perf(message, duration);
45
+ }
46
+ }
47
+
36
48
  export function debug_perf1(message, duration, threshold) {
37
49
  if (duration > threshold) {
38
- debug_perf_ns(message + ` WARNING! threshold > ${threshold}ms reached!`);
50
+ debug_perf_ns(
51
+ message +
52
+ ` took ${duration}ms. WARNING! threshold > ${threshold}ms reached!`,
53
+ );
39
54
  }
40
55
  }
@@ -56,7 +56,7 @@ export default class DataModel {
56
56
  return this.storage.getAllArticles(order);
57
57
  }
58
58
 
59
- async findAll(start, end, limit) {
59
+ findAll(start, end, limit) {
60
60
  debug("find all %d %d %d", start, end, limit);
61
61
  if (start == null && end == null && limit == null)
62
62
  return this.getAllArticles();
@@ -64,15 +64,17 @@ export default class DataModel {
64
64
  let articles = this.storage.getRange(start, end, limit);
65
65
 
66
66
  if (articles.length < limit && this.db) {
67
- const dbArticles = await this.db.findAll(limit, 0, start, end);
68
- for (const data of dbArticles) {
69
- if (!this.storage.contains(data.id)) {
70
- this.storage.insert(
71
- new Article(data.id, data.title, data.content, data.createdAt),
72
- );
67
+ return (async () => {
68
+ const dbArticles = await this.db.findAll(limit, 0, start, end);
69
+ for (const data of dbArticles) {
70
+ if (!this.storage.contains(data.id)) {
71
+ this.storage.insert(
72
+ new Article(data.id, data.title, data.content, data.createdAt),
73
+ );
74
+ }
73
75
  }
74
- }
75
- articles = this.storage.getRange(start, end, limit);
76
+ return this.storage.getRange(start, end, limit);
77
+ })();
76
78
  }
77
79
  return articles;
78
80
  }
@@ -13,6 +13,7 @@ export default class SqliteAdapter {
13
13
  this.db = null;
14
14
  this.initPromise = null;
15
15
  this.ready = false;
16
+ this.stmts = {};
16
17
  }
17
18
 
18
19
  async initialize() {
@@ -86,16 +87,23 @@ export default class SqliteAdapter {
86
87
  return this.dbname;
87
88
  }
88
89
 
90
+ getStmt(sql) {
91
+ if (!this.stmts[sql]) {
92
+ this.stmts[sql] = this.db.prepare(sql);
93
+ }
94
+ return this.stmts[sql];
95
+ }
96
+
89
97
  async getBlogTitle() {
90
98
  debug("getBlogTitle dbname: %s", this.dbname);
91
99
  if (!this.db) await this.initialize();
92
- const row = this.db.prepare("SELECT title FROM BlogInfos LIMIT 1").get();
100
+ const row = this.getStmt("SELECT title FROM BlogInfos LIMIT 1").get();
93
101
  return row ? row.title : "Blog";
94
102
  }
95
103
 
96
104
  async updateBlogTitle(newTitle) {
97
105
  if (!this.db) await this.initialize();
98
- this.db.prepare("UPDATE BlogInfos SET title = ?").run(newTitle);
106
+ this.getStmt("UPDATE BlogInfos SET title = ?").run(newTitle);
99
107
  }
100
108
 
101
109
  async save(newArticle) {
@@ -106,7 +114,7 @@ export default class SqliteAdapter {
106
114
  : new Date().toISOString();
107
115
  const updatedAt = new Date().toISOString();
108
116
 
109
- const stmt = this.db.prepare(
117
+ const stmt = this.getStmt(
110
118
  `INSERT INTO Articles (id, title, content, createdAt, updatedAt)
111
119
  VALUES (@id, @title, @content, @createdAt, @updatedAt)`,
112
120
  );
@@ -150,13 +158,13 @@ export default class SqliteAdapter {
150
158
 
151
159
  if (sets.length > 1) {
152
160
  const sql = `UPDATE Articles SET ${sets.join(", ")} WHERE id = @id`;
153
- this.db.prepare(sql).run(params);
161
+ this.getStmt(sql).run(params);
154
162
  }
155
163
  }
156
164
 
157
165
  async remove(id) {
158
166
  if (!this.db) await this.initialize();
159
- this.db.prepare("DELETE FROM Articles WHERE id = ?").run(id);
167
+ this.getStmt("DELETE FROM Articles WHERE id = ?").run(id);
160
168
  }
161
169
 
162
170
  async findAll(
@@ -195,7 +203,7 @@ export default class SqliteAdapter {
195
203
  params.push(offset);
196
204
  }
197
205
 
198
- const rows = this.db.prepare(query).all(...params);
206
+ const rows = this.getStmt(query).all(...params);
199
207
  return rows.map((row) => ({
200
208
  ...row,
201
209
  createdAt: new Date(row.createdAt),
@@ -69,7 +69,7 @@ export class BinarySearchTree {
69
69
  );
70
70
  const time_end = performance.now();
71
71
  const duration = time_end - time_start;
72
- debug_perf("BST took " + duration + "ms", duration);
72
+ debug_perf("BST", duration);
73
73
  return result;
74
74
  }
75
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/router.js CHANGED
@@ -9,112 +9,108 @@ const debug = createDebug("plainblog:api");
9
9
  const staticCache = new Map();
10
10
 
11
11
  // API routes
12
- export async function login(req, res, cb, cb2) {
12
+ export function login(req, res, cb, cb2) {
13
13
  // workaround for angular frontend
14
14
  if (
15
15
  (req.url === "/api/login" || req.url === "/login") &&
16
16
  req.method === "POST"
17
17
  ) {
18
18
  debug(`${req.method} ${req.url}`);
19
- await cb(req, res);
20
- return;
19
+ return cb(req, res);
21
20
  }
22
21
  if (req.method === "GET" && req.url === "/login") {
23
22
  debug(`${req.method} ${req.url}`);
24
23
  res.writeHead(200, { "Content-Type": "text/html" });
25
- await cb2(req, res);
26
- return;
24
+ return cb2(req, res);
27
25
  }
28
26
  }
29
27
 
30
- export async function logout(req, res, cb) {
28
+ export function logout(req, res, cb) {
31
29
  if (req.method === "POST") {
32
30
  if (req.url === "/logout" || req.url === "/api/logout") {
33
31
  debug(`${req.method} ${req.url}`);
34
- await cb(req, res);
35
- return;
32
+ return cb(req, res);
36
33
  }
37
34
  }
38
35
  }
39
36
 
40
- export async function api(req, res, cb) {
37
+ export function api(req, res, cb) {
41
38
  if (req.url.startsWith("/api")) {
42
39
  debug(`${req.method} ${req.url}`);
43
- await cb(req, res);
44
- return;
40
+ return cb(req, res);
45
41
  }
46
42
  }
47
43
 
48
- export async function new1(req, res, cb) {
44
+ export function new1(req, res, cb) {
49
45
  if (req.method === "POST" && req.url === "/new") {
50
46
  const time_start = performance.now();
51
47
  debug(`${req.method} ${req.url}`);
52
- await cb(req, res);
53
- const time_end = performance.now();
54
- const duration = time_end - time_start;
55
- debug_perf('"/new" took ' + duration + "ms", duration);
56
- return;
48
+ return cb(req, res).then(() => {
49
+ const time_end = performance.now();
50
+ const duration = time_end - time_start;
51
+ debug_perf('"/new"', duration);
52
+ });
57
53
  }
58
54
  }
59
55
 
60
- export async function handleLogin(req, res, cb) {
56
+ export function handleLogin(req, res, cb) {
61
57
  if (req.method === "POST") {
62
58
  debug(`${req.method} ${req.url}`);
63
- await cb(req, res);
64
- return;
59
+ return cb(req, res);
65
60
  }
66
61
  }
67
62
 
68
- export async function getPages(req, res, cb, publicDir) {
63
+ export function getPages(req, res, cb, publicDir) {
69
64
  if (req.method === "GET") {
70
- const getRoot_start = performance.now();
71
- if (req.url === "/") {
72
- debug(`${req.method} ${req.url}`);
73
- await cb(req, res);
74
- const getRoot_end = performance.now();
75
- const duration = getRoot_end - getRoot_start;
76
- debug_perf('GET "/" took ' + duration + "ms", duration);
77
- return;
78
- }
79
- // Try to serve static files from public folder
80
- // Normalize path to prevent directory traversal attacks
81
- const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
82
- const filePath = path.join(
83
- publicDir,
84
- safePath === "/" ? "index.html" : safePath,
85
- );
86
-
87
- //debug("%s", filePath);
88
- try {
89
- let data;
90
- if (staticCache.has(filePath)) {
91
- data = staticCache.get(filePath);
92
- } else {
93
- data = await readFile(filePath);
94
- staticCache.set(filePath, data);
65
+ return (async () => {
66
+ const getRoot_start = performance.now();
67
+ if (req.url === "/") {
68
+ debug(`${req.method} ${req.url}`);
69
+ await cb(req, res);
70
+ const getRoot_end = performance.now();
71
+ const duration = getRoot_end - getRoot_start;
72
+ debug_perf('GET "/"', duration);
73
+ return;
95
74
  }
75
+ // Try to serve static files from public folder
76
+ // Normalize path to prevent directory traversal attacks
77
+ const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
78
+ const filePath = path.join(
79
+ publicDir,
80
+ safePath === "/" ? "index.html" : safePath,
81
+ );
96
82
 
97
- // Manual MIME type detection (simplified)
98
- const ext = path.extname(filePath);
99
- const mimeTypes = {
100
- ".html": "text/html",
101
- ".css": "text/css",
102
- ".js": "text/javascript",
103
- };
83
+ //debug("%s", filePath);
84
+ try {
85
+ let data;
86
+ if (staticCache.has(filePath)) {
87
+ data = staticCache.get(filePath);
88
+ } else {
89
+ data = await readFile(filePath);
90
+ staticCache.set(filePath, data);
91
+ }
92
+
93
+ // Manual MIME type detection (simplified)
94
+ const ext = path.extname(filePath);
95
+ const mimeTypes = {
96
+ ".html": "text/html",
97
+ ".css": "text/css",
98
+ ".js": "text/javascript",
99
+ };
104
100
 
105
- res.writeHead(200, {
106
- "Content-Type": mimeTypes[ext] || "application/octet-stream",
107
- });
108
- res.end(data);
109
- } catch (err) {
110
- if (err) {
111
- if (!res.headersSent) {
112
- res.writeHead(404);
113
- return res.end("File Not Found");
101
+ res.writeHead(200, {
102
+ "Content-Type": mimeTypes[ext] || "application/octet-stream",
103
+ });
104
+ res.end(data);
105
+ } catch (err) {
106
+ if (err) {
107
+ if (!res.headersSent) {
108
+ res.writeHead(404);
109
+ return res.end("File Not Found");
110
+ }
114
111
  }
115
112
  }
116
- }
117
- return;
113
+ })();
118
114
  }
119
115
  }
120
116
  // ---------------------------------------------
package/server.js CHANGED
@@ -45,15 +45,21 @@ async function readBody(req, timeout = 15000, maxSize = 1024 * 1024) {
45
45
  }
46
46
  });
47
47
 
48
- req.on("end", () => {
48
+ req.on("end", function read_body_end() {
49
49
  if (!completed) {
50
50
  completed = true;
51
51
  clearTimeout(timer);
52
- resolve(Buffer.concat(chunks).toString());
52
+ if (chunks.length === 0) {
53
+ resolve("");
54
+ } else if (chunks.length === 1) {
55
+ resolve(chunks[0].toString());
56
+ } else {
57
+ resolve(Buffer.concat(chunks).toString());
58
+ }
53
59
  }
54
60
  const time_end = performance.now();
55
61
  const duration = time_end - time_start;
56
- debug_perf("readBody on end took " + duration + "ms", duration);
62
+ debug_perf("readBody on end", duration);
57
63
  });
58
64
 
59
65
  req.on("error", (err) => {
@@ -64,7 +70,7 @@ async function readBody(req, timeout = 15000, maxSize = 1024 * 1024) {
64
70
  }
65
71
  const time_end = performance.now();
66
72
  const duration = time_end - time_start;
67
- debug_perf("readBody on error took " + duration + "ms", duration);
73
+ debug_perf("readBody on error", duration);
68
74
  });
69
75
  });
70
76
  }
@@ -106,37 +112,55 @@ export async function createServer(
106
112
  res.on("finish", clearTimeouts);
107
113
  res.on("close", clearTimeouts);
108
114
  //debug("query %s", req.url);
109
- await api(req, res, async (req, res) => {
110
- const time_start = performance.now();
111
- await jsonAPI(
112
- req,
113
- res,
114
- title,
115
- articles,
116
- databaseModel,
117
- readBody,
118
- auth.isAuthenticated.bind(auth),
119
- );
120
- const time_end = performance.now();
121
- const duration = time_end - time_start;
122
- debug_perf1("jsonAPI took " + duration + "ms", duration, 400);
123
- });
124
- if (res.headersSent) return;
125
115
 
126
- await login(
127
- req,
128
- res,
129
- async (req, res) => {
130
- await auth.handleLogin(
116
+ // Helper: Führt eine Route aus und stoppt, wenn eine Antwort gesendet wurde
117
+ const tryRoute = async (routeCall) => {
118
+ if (res.headersSent) return true;
119
+ const p = routeCall();
120
+ if (p) {
121
+ await p;
122
+ return res.headersSent;
123
+ }
124
+ return false;
125
+ };
126
+
127
+ if (
128
+ await tryRoute(() =>
129
+ api(req, res, async function handleAPIRequest(req, res) {
130
+ const time_start = performance.now();
131
+ await jsonAPI(
132
+ req,
133
+ res,
134
+ title,
135
+ articles,
136
+ databaseModel,
137
+ readBody,
138
+ auth.isAuthenticated.bind(auth),
139
+ );
140
+ const time_end = performance.now();
141
+ const duration = time_end - time_start;
142
+ debug_perf1("jsonAPI", duration, 400);
143
+ }),
144
+ )
145
+ )
146
+ return;
147
+
148
+ if (
149
+ await tryRoute(() =>
150
+ login(
131
151
  req,
132
152
  res,
133
- readBody,
134
- REQUEST_TIMEOUT,
135
- MAX_REQUEST_SIZE,
136
- );
137
- },
138
- async (req, res) => {
139
- res.end(`${header("My Blog")}<body>
153
+ async (req, res) => {
154
+ await auth.handleLogin(
155
+ req,
156
+ res,
157
+ readBody,
158
+ REQUEST_TIMEOUT,
159
+ MAX_REQUEST_SIZE,
160
+ );
161
+ },
162
+ async (req, res) => {
163
+ res.end(`${header("My Blog")}<body>
140
164
  <form class="loginform" id="loginForm">
141
165
  <h1>Blog</h1>
142
166
  <!-- Message container -->
@@ -183,130 +207,144 @@ export async function createServer(
183
207
  });
184
208
  </script>
185
209
  </body></html>`);
186
- },
187
- );
188
- if (res.headersSent) return;
189
-
190
- await logout(req, res, (req, res) => {
191
- auth.handleLogout(req, res);
192
- });
193
- if (res.headersSent) return;
194
-
195
- await new1(req, res, async (req, res) => {
196
- if (!auth.isAuthenticated(req)) {
197
- debug("not authenticated");
198
- res.writeHead(403, { "Content-Type": "application/json" });
199
- res.end(JSON.stringify({ error: "Forbidden" }));
200
- return;
201
- }
202
-
203
- try {
204
- const body = await readBody(req);
205
- const params = new URLSearchParams(body);
206
- const articleData = Object.fromEntries(params.entries());
207
-
208
- debug("New Article Data:", articleData);
209
- // local
210
- // Write-before-exit strategy: Create in memory, save on closeServer()
211
- const savedArticle = Article.createNew(
212
- articleData.title,
213
- articleData.content,
214
- );
210
+ },
211
+ ),
212
+ )
213
+ )
214
+ return;
215
215
 
216
- databaseModel
217
- .save(savedArticle)
218
- .catch((err) => console.error("Async save failed:", err));
216
+ if (
217
+ await tryRoute(() =>
218
+ logout(req, res, (req, res) => {
219
+ auth.handleLogout(req, res);
220
+ }),
221
+ )
222
+ )
223
+ return;
219
224
 
220
- await postArticle(savedArticle);
221
- // external
225
+ if (
226
+ await tryRoute(() =>
227
+ new1(req, res, async (req, res) => {
228
+ if (!auth.isAuthenticated(req)) {
229
+ debug("not authenticated");
230
+ res.writeHead(403, { "Content-Type": "application/json" });
231
+ res.end(JSON.stringify({ error: "Forbidden" }));
232
+ return;
233
+ }
222
234
 
223
- // Success response
224
- res.writeHead(302, { Location: "/" });
225
- res.end();
226
- } catch (formErr) {
227
- debug("Error handling form submission: %s", formErr.message);
228
- if (!res.headersSent) {
229
- res.writeHead(400, { "Content-Type": "application/json" });
230
- res.end(JSON.stringify({ error: "Failed to parse form data" }));
231
- }
232
- }
233
- });
234
- if (res.headersSent) return;
235
-
236
- await getPages(
237
- req,
238
- res,
239
- async (req, res) => {
240
- // reload styles and scripts on (every) request
241
- if (assetManager.reloadStylesOnGET) {
242
- // This is a development-only feature and a major performance risk.
243
- if (process.env.NODE_ENV === "production") {
244
- console.warn(
245
- "Warning: 'reloadStylesOnGET' is enabled in a production-like environment. This is a major performance risk and should be disabled.",
235
+ try {
236
+ const body = await readBody(req);
237
+ const params = new URLSearchParams(body);
238
+ const articleData = Object.fromEntries(params.entries());
239
+
240
+ debug("New Article Data:", articleData);
241
+ // local
242
+ // Write-before-exit strategy: Create in memory, save on closeServer()
243
+ const savedArticle = Article.createNew(
244
+ articleData.title,
245
+ articleData.content,
246
246
  );
247
- assetManager.reloadStylesOnGET = false; // Disable it for subsequent requests
248
- }
249
- await assetManager.reload();
250
- }
251
247
 
252
- let loggedin = false;
253
- if (!auth.isAuthenticated(req)) {
254
- // login
255
- loggedin = false;
256
- } else {
257
- // logout
258
- loggedin = true;
259
- }
248
+ databaseModel
249
+ .save(savedArticle)
250
+ .catch((err) => console.error("Async save failed:", err));
260
251
 
261
- if (angular) {
262
- // use angular frontend
263
- const filePath = path.join(publicDir, "index.html");
252
+ await postArticle(savedArticle);
253
+ // external
264
254
 
265
- debug("%s", filePath);
266
- try {
267
- const data = await readFile(filePath);
268
- // Manual MIME type detection (simplified)
269
- const ext = path.extname(filePath);
270
- const mimeTypes = {
271
- ".html": "text/html",
272
- ".css": "text/css",
273
- ".js": "text/javascript",
274
- };
275
-
276
- res.writeHead(200, {
277
- "Content-Type": mimeTypes[ext] || "application/octet-stream",
278
- });
279
- res.end(data);
280
- } catch (fileErr) {
281
- debug("Error reading index file: %s", fileErr.message);
255
+ // Success response
256
+ res.writeHead(302, { Location: "/" });
257
+ res.end();
258
+ } catch (formErr) {
259
+ debug("Error handling form submission: %s", formErr.message);
282
260
  if (!res.headersSent) {
283
- debug404("unmatched route: %s %s", req.method, req.url);
284
- res.writeHead(404);
285
- return res.end("Index-File Not Found");
261
+ res.writeHead(400, { "Content-Type": "application/json" });
262
+ res.end(JSON.stringify({ error: "Failed to parse form data" }));
286
263
  }
287
264
  }
288
- } else {
289
- // use built in view engine
290
- try {
291
- const html = await html_content(loggedin); // render this blog to HTML
292
- res.writeHead(200, {
293
- "Content-Type": "text/html; charset=UTF-8",
294
- });
295
- res.end(html);
296
- return;
297
- } catch (renderErr) {
298
- console.error("Error rendering HTML:", renderErr);
299
- if (!res.headersSent) {
300
- res.writeHead(500, { "Content-Type": "text/plain" });
301
- res.end("Internal Server Error");
265
+ }),
266
+ )
267
+ )
268
+ return;
269
+
270
+ if (
271
+ await tryRoute(() =>
272
+ getPages(
273
+ req,
274
+ res,
275
+ async (req, res) => {
276
+ // reload styles and scripts on (every) request
277
+ if (assetManager.reloadStylesOnGET) {
278
+ // This is a development-only feature and a major performance risk.
279
+ if (process.env.NODE_ENV === "production") {
280
+ console.warn(
281
+ "Warning: 'reloadStylesOnGET' is enabled in a production-like environment. This is a major performance risk and should be disabled.",
282
+ );
283
+ assetManager.reloadStylesOnGET = false; // Disable it for subsequent requests
284
+ }
285
+ await assetManager.reload();
286
+ }
287
+
288
+ let loggedin = false;
289
+ if (!auth.isAuthenticated(req)) {
290
+ // login
291
+ loggedin = false;
292
+ } else {
293
+ // logout
294
+ loggedin = true;
302
295
  }
303
- return;
304
- }
305
- }
306
- },
307
- publicDir,
308
- );
309
- if (res.headersSent) return;
296
+
297
+ if (angular) {
298
+ // use angular frontend
299
+ const filePath = path.join(publicDir, "index.html");
300
+
301
+ debug("%s", filePath);
302
+ try {
303
+ const data = await readFile(filePath);
304
+ // Manual MIME type detection (simplified)
305
+ const ext = path.extname(filePath);
306
+ const mimeTypes = {
307
+ ".html": "text/html",
308
+ ".css": "text/css",
309
+ ".js": "text/javascript",
310
+ };
311
+
312
+ res.writeHead(200, {
313
+ "Content-Type": mimeTypes[ext] || "application/octet-stream",
314
+ });
315
+ res.end(data);
316
+ } catch (fileErr) {
317
+ debug("Error reading index file: %s", fileErr.message);
318
+ if (!res.headersSent) {
319
+ debug404("unmatched route: %s %s", req.method, req.url);
320
+ res.writeHead(404);
321
+ return res.end("Index-File Not Found");
322
+ }
323
+ }
324
+ } else {
325
+ // use built in view engine
326
+ try {
327
+ const html = await html_content(loggedin); // render this blog to HTML
328
+ res.writeHead(200, {
329
+ "Content-Type": "text/html; charset=UTF-8",
330
+ });
331
+ res.end(html);
332
+ return;
333
+ } catch (renderErr) {
334
+ console.error("Error rendering HTML:", renderErr);
335
+ if (!res.headersSent) {
336
+ res.writeHead(500, { "Content-Type": "text/plain" });
337
+ res.end("Internal Server Error");
338
+ }
339
+ return;
340
+ }
341
+ }
342
+ },
343
+ publicDir,
344
+ ),
345
+ )
346
+ )
347
+ return;
310
348
 
311
349
  // If no route was matched, send a 404
312
350
  if (!res.headersSent) {
@@ -472,7 +510,7 @@ async function jsonAPI(
472
510
  const dbArticles = await articles.findAll(start, end, limit);
473
511
  const time_end = performance.now();
474
512
  const duration = time_end - time_start;
475
- debug_perf('GET "/api/articles" took ' + duration + "ms", duration);
513
+ debug_perf('GET "/api/articles"', duration);
476
514
  if (!res.headersSent) {
477
515
  res.writeHead(200, { "Content-Type": "application/json" });
478
516
  const responseData = {
@@ -502,13 +540,10 @@ async function jsonAPI(
502
540
  body = await readBody(req, REQUEST_TIMEOUT, MAX_REQUEST_SIZE);
503
541
  const time_end1 = performance.now();
504
542
  const duration1 = time_end1 - time_start1;
505
- debug_perf(
506
- 'POST "/api/articles" readBody took ' + duration1 + "ms",
507
- duration1,
508
- );
543
+ debug_perf('POST "/api/articles" readBody', duration1);
509
544
  const time_end = performance.now();
510
545
  const duration = time_end - time_start;
511
- debug_perf('POST "/api/articles" took ' + duration + "ms", duration);
546
+ debug_perf('POST "/api/articles"', duration);
512
547
  } catch (err) {
513
548
  debug("Error reading request body: %s", err.message);
514
549
  if (!res.headersSent) {
@@ -541,7 +576,7 @@ async function jsonAPI(
541
576
  debug("new article: %s", newArticle.title);
542
577
  const time_end1 = performance.now();
543
578
  const duration1 = time_end1 - time_start;
544
- debug_perf("new article took " + duration1 + "ms", duration1);
579
+ debug_perf("new article took ", duration1);
545
580
 
546
581
  // Create article with a temporary ID for immediate use
547
582
  const articleWithTempId = Article.createNew(
@@ -563,7 +598,7 @@ async function jsonAPI(
563
598
 
564
599
  const time_end1 = performance.now();
565
600
  const duration = time_end1 - time_start;
566
- debug_perf("databaseModel took " + duration + "ms", duration);
601
+ debug_perf("databaseModel", duration);
567
602
  } catch (dbErr) {
568
603
  console.error("Database save error:", dbErr.message);
569
604
  console.error("Stack:", dbErr.stack);
@@ -636,7 +671,7 @@ async function jsonAPI(
636
671
  }
637
672
  const time_end = performance.now();
638
673
  const duration = time_end - time_start;
639
- debug_perf('DELETE "/api/articles" took ' + duration + "ms", duration);
674
+ debug_perf('DELETE "/api/articles"', duration);
640
675
  } else if (req.method === "PUT") {
641
676
  debug("PUT an article");
642
677
  if (!isAuthenticated(req)) {
@@ -698,7 +733,7 @@ async function jsonAPI(
698
733
  }
699
734
  const time_end = performance.now();
700
735
  const duration = time_end - time_start;
701
- debug_perf('POST "/api/articles" took ' + duration + "ms", duration);
736
+ debug_perf('POST "/api/articles"', duration);
702
737
  } else {
703
738
  if (!res.headersSent) {
704
739
  debug404("unmatched route: %s %s", req.method, req.url);
@@ -1,7 +1,7 @@
1
1
  import { parentPort, workerData } from "worker_threads";
2
2
  import { compileStyles, mergeStyles } from "../build-styles.js";
3
3
  import { compileScripts } from "../build-scripts.js";
4
- import { createDebug, debug_perf } from "../debug-loader.js";
4
+ import { createDebug, measure_perf } from "../debug-loader.js";
5
5
 
6
6
  async function run() {
7
7
  if (!parentPort) {
@@ -9,20 +9,17 @@ async function run() {
9
9
  }
10
10
  const { type, fileData, cssContents } = workerData;
11
11
  try {
12
- const time_start = performance.now();
13
- let result;
14
- if (type === "styles") {
15
- result = await compileStyles(fileData);
16
- } else if (type === "scripts") {
17
- result = await compileScripts(fileData);
18
- } else if (type === "mergeStyles") {
19
- result = await mergeStyles(...cssContents);
20
- } else {
21
- throw new Error(`Unknown compilation type: ${type}`);
22
- }
23
- const time_end = performance.now();
24
- const duration = time_end - time_start;
25
- debug_perf("compilerWorker took " + duration + "ms", duration);
12
+ const result = await measure_perf("compilerWorker", async () => {
13
+ if (type === "styles") {
14
+ return await compileStyles(fileData);
15
+ } else if (type === "scripts") {
16
+ return await compileScripts(fileData);
17
+ } else if (type === "mergeStyles") {
18
+ return await mergeStyles(...cssContents);
19
+ } else {
20
+ throw new Error(`Unknown compilation type: ${type}`);
21
+ }
22
+ });
26
23
  parentPort.postMessage({ status: "success", result });
27
24
  } catch (error) {
28
25
  parentPort.postMessage({ status: "error", error: error.message });