@lexho111/plainblog 0.7.2 → 0.7.4

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/Blog.js CHANGED
@@ -123,13 +123,13 @@ export default class Blog {
123
123
  * @param {string} style - A string containing CSS rules.
124
124
  */
125
125
  set style(style) {
126
- debug("set style");
127
- this.assetManager.styles = style;
126
+ debug("set style %s", style);
127
+ this.assetManager.style = style;
128
128
  this.assetManager.compilestyle = true;
129
129
  }
130
130
 
131
131
  get style() {
132
- return this.assetManager.styles;
132
+ return this.assetManager._styles;
133
133
  }
134
134
 
135
135
  /**
@@ -137,8 +137,8 @@ export default class Blog {
137
137
  * @param {string|string[]} files - A single file path or an array of file paths.
138
138
  */
139
139
  set stylesheetPath(files) {
140
+ debug(`set stylesheet path: ${files}`);
140
141
  this.assetManager.stylesheetPath = files;
141
- console.log(`this.assetManager.stylesheetPath: ${files}`);
142
142
  this.assetManager.compilestyle = true;
143
143
  }
144
144
 
@@ -160,77 +160,98 @@ export default class Blog {
160
160
 
161
161
  /** initializes database */
162
162
  async init() {
163
- if (this.#initPromise) return this.#initPromise;
163
+ //if (this.#initPromise) return this.#initPromise;
164
164
 
165
165
  this.#initPromise = (async () => {
166
- await this.assetManager.init();
166
+ try {
167
+ try {
168
+ await this.assetManager.init();
169
+ } catch (e) {
170
+ console.warn(
171
+ "Warning: AssetManager initialization failed. Styles may not be compiled.",
172
+ e.message,
173
+ );
174
+ }
167
175
 
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),
176
+ if (this.#isExternalAPI) {
177
+ console.log("external API");
178
+ await this.#loadFromAPI();
179
+ } else {
180
+ debug(`database: ${this.database.type}`);
181
+ if (!this.#databaseModel) {
182
+ if (this.database.type === "file") {
183
+ this.#databaseModel = new DatabaseModel(
184
+ new WorkerAdapter(this.database),
185
+ );
186
+ } else if (this.database.type === "sqlite") {
187
+ this.#databaseModel = new DatabaseModel(
188
+ new SqliteAdapter(this.database),
189
+ );
190
+ }
191
+ }
192
+ // Timeout nach 60 Sekunden, um Hängenbleiben zu verhindern
193
+ let timeoutHandle;
194
+ const timeout = new Promise((_, reject) => {
195
+ timeoutHandle = setTimeout(
196
+ () => reject(new Error("Database initialization timed out")),
197
+ 60000,
177
198
  );
178
- } else if (this.database.type === "sqlite") {
179
- this.#databaseModel = new DatabaseModel(
180
- new SqliteAdapter(this.database),
199
+ });
200
+ try {
201
+ await Promise.race([this.#databaseModel.initialize(), timeout]);
202
+ } finally {
203
+ clearTimeout(timeoutHandle); // Timer aufräumen, sobald fertig
204
+ }
205
+
206
+ if (this.#databaseModel.isReady()) {
207
+ console.log(`connected to database`);
208
+ console.log(
209
+ `using ${this.#databaseModel.getType()} database '${this.#databaseModel.getDBName()}'`,
181
210
  );
182
211
  }
212
+ this.#articles.setDatabase(this.#databaseModel);
213
+ const [dbTitle, dbArticles] = await Promise.all([
214
+ this.#databaseModel.getBlogTitle(),
215
+ this.#databaseModel.findAll(-1),
216
+ ]);
217
+ debug("dbArticles.size(): %d", dbArticles.length);
218
+ //log(filename, "dbArticles.size(): " + dbArticles.length);
219
+ //log(filename, "dbArticles.size(): " + dbArticles.length, "Blog.js");
220
+ debug("all articles in Blog after loading from db");
221
+
222
+ // Displays a beautiful table in the console
223
+ //table(dbArticles)
224
+
225
+ if (dbArticles.length == 0) {
226
+ dbArticles.push(
227
+ new Article(
228
+ 1,
229
+ "Sample Entry #1",
230
+ "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.",
231
+ new Date(),
232
+ ),
233
+ );
234
+ dbArticles.push(
235
+ new Article(
236
+ 2,
237
+ "Sample Entry #2",
238
+ "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.",
239
+ new Date(),
240
+ ),
241
+ );
242
+ }
243
+ if (this.assetManager.reloadStylesOnGET)
244
+ console.log("reload scripts and styles on get-request");
245
+ let title = "";
246
+ if (this.#title != null && this.#title.length > 0)
247
+ title = this.#title; // use blog title if set
248
+ else title = dbTitle; // use title from the database
249
+ const responseData = { title: title, articles: dbArticles };
250
+ this.#applyBlogData(responseData);
183
251
  }
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);
252
+ } catch (err) {
253
+ this.#initPromise = null; // Reset promise on error to allow retry
254
+ throw err;
234
255
  }
235
256
  })();
236
257
  return this.#initPromise;
@@ -267,11 +288,6 @@ export default class Blog {
267
288
  /** start a http server with default port 8080 */
268
289
  async startServer(port = 8080) {
269
290
  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
291
 
276
292
  this.#server = await createServer(
277
293
  this.postArticle.bind(this),
@@ -285,14 +301,16 @@ export default class Blog {
285
301
  this.database,
286
302
  );
287
303
 
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
- });
304
+ await new Promise(
305
+ function f1(resolve, reject) {
306
+ const errorHandler = (err) => reject(err);
307
+ this.#server.once("error", errorHandler);
308
+ this.#server.listen(port, "0.0.0.0", () => {
309
+ this.#server.removeListener("error", errorHandler);
310
+ resolve();
311
+ });
312
+ }.bind(this),
313
+ );
296
314
  console.log(`server running at http://localhost:${port}/`);
297
315
  }
298
316
 
@@ -363,7 +381,10 @@ export default class Blog {
363
381
 
364
382
  /** render this blog content to valid html */
365
383
  async toHTML(loggedin) {
366
- const articles_after = await this.#articles.findAll(null, null, 50);
384
+ let articles_after = this.#articles.findAll(null, null, 50);
385
+ if (articles_after instanceof Promise) {
386
+ articles_after = await articles_after;
387
+ }
367
388
  // prettier-ignore
368
389
  debug("fetched %d articles", articles_after.length);
369
390
  const data = {
package/cluster-server.js CHANGED
@@ -1,44 +1,49 @@
1
1
  import cluster from "cluster";
2
2
  import os from "os";
3
3
  import process from "node:process";
4
+ import { fileURLToPath } from "url";
4
5
  import Blog from "./Blog.js";
5
6
 
6
- const numCPUs = os.cpus().length;
7
+ export function startCluster(port = process.env.PORT || 8080) {
8
+ const numCPUs = os.cpus().length;
7
9
 
8
- if (cluster.isPrimary) {
9
- console.log(`master ${process.pid} is running`);
10
+ if (cluster.isPrimary) {
11
+ console.log(`master ${process.pid} is running`);
10
12
 
11
- // Fork workers (one per CPU core)
12
- for (let i = 0; i < numCPUs; i++) {
13
- cluster.fork();
14
- }
13
+ // Fork workers (one per CPU core)
14
+ for (let i = 0; i < numCPUs; i++) {
15
+ cluster.fork();
16
+ }
15
17
 
16
- cluster.on("exit", (worker, code, signal) => {
17
- console.log(
18
- `worker ${worker.process.pid} died (${signal || code}). restarting...`,
19
- );
20
- cluster.fork(); // Restart dead worker
21
- });
22
- } else {
23
- // Worker process
24
- const blog = new Blog();
18
+ cluster.on("exit", (worker, code, signal) => {
19
+ console.log(
20
+ `worker ${worker.process.pid} died (${signal || code}). restarting...`,
21
+ );
22
+ cluster.fork(); // Restart dead worker
23
+ });
24
+ } else {
25
+ // Worker process
26
+ const blog = new Blog();
25
27
 
26
- const port = process.env.PORT || 8080;
28
+ blog
29
+ .startServer(port)
30
+ .then(() => {
31
+ console.log(`worker ${process.pid} started on port ${port}`);
32
+ })
33
+ .catch((err) => {
34
+ console.error(`Worker ${process.pid} failed to start:`, err);
35
+ process.exit(1);
36
+ });
27
37
 
28
- blog
29
- .startServer(port)
30
- .then(() => {
31
- console.log(`worker ${process.pid} started on port ${port}`);
32
- })
33
- .catch((err) => {
34
- console.error(`Worker ${process.pid} failed to start:`, err);
35
- process.exit(1);
38
+ // Graceful shutdown
39
+ process.on("SIGTERM", async () => {
40
+ console.log(`worker ${process.pid} shutting down...`);
41
+ await blog.closeServer();
42
+ process.exit(0);
36
43
  });
44
+ }
45
+ }
37
46
 
38
- // Graceful shutdown
39
- process.on("SIGTERM", async () => {
40
- console.log(`worker ${process.pid} shutting down...`);
41
- await blog.closeServer();
42
- process.exit(0);
43
- });
47
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
48
+ startCluster();
44
49
  }
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
  }
package/index.js CHANGED
@@ -4,6 +4,7 @@ import FileAdapter from "./model/FileAdapter.js";
4
4
  import PostgresAdapter from "./model/PostgresAdapter.js";
5
5
  import SequelizeAdapter from "./model/SequelizeAdapter.js";
6
6
  import SqliteAdapter from "./model/SqliteAdapter.js";
7
+ import { startCluster } from "./cluster-server.js";
7
8
 
8
9
  export {
9
10
  Blog,
@@ -12,5 +13,6 @@ export {
12
13
  SequelizeAdapter,
13
14
  FileAdapter,
14
15
  SqliteAdapter,
16
+ startCluster,
15
17
  };
16
18
  export default Blog;
@@ -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() {
@@ -31,7 +32,7 @@ export default class SqliteAdapter {
31
32
  console.error(
32
33
  "The 'better-sqlite3' package is not installed. Please install it by running: npm install better-sqlite3",
33
34
  );
34
- throw new Error("Missing optional dependency: 'better-sqlite3'");
35
+ process.exit(1);
35
36
  } else {
36
37
  throw error;
37
38
  }
@@ -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
 
@@ -0,0 +1,72 @@
1
+ import { parentPort, workerData } from "worker_threads";
2
+ import fs from "fs";
3
+
4
+ export async function run(
5
+ baseContent,
6
+ extraContent = "",
7
+ inlineContent = "",
8
+ options = {},
9
+ ) {
10
+ const { forceFallback = false, disableFallback = false } = options;
11
+ let finalStyles = baseContent + "\n" + extraContent + "\n" + inlineContent;
12
+
13
+ // postcss, autoprefixer, cssnano installed?
14
+ if (!forceFallback) {
15
+ try {
16
+ const { default: postcss } = await import("postcss");
17
+ const { default: autoprefixer } = await import("autoprefixer");
18
+ const { default: cssnano } = await import("cssnano");
19
+
20
+ const result = await postcss([autoprefixer, cssnano]).process(
21
+ finalStyles,
22
+ {
23
+ from: undefined,
24
+ },
25
+ );
26
+ return result.css;
27
+ } catch (e) {
28
+ // no --> print "please install postcss, autoprefixer, cssnano"
29
+ console.log(
30
+ "please run 'npm install postcss autoprefixer cssnano' for full feature support.",
31
+ );
32
+ }
33
+ }
34
+
35
+ // fallback-minify
36
+ if (!disableFallback) {
37
+ finalStyles = finalStyles
38
+ .replace(/\/\*[\s\S]*?\*\//g, "")
39
+ .replace(/\s*([:;{}])\s*/g, "$1")
40
+ .replace(/}\s+/g, "}")
41
+ .replace(/;}/g, "}")
42
+ .trim();
43
+ }
44
+ return finalStyles;
45
+ }
46
+
47
+ // compile
48
+ if (parentPort && workerData && workerData.outputPath) {
49
+ const {
50
+ baseContent,
51
+ extraContent,
52
+ inlineContent,
53
+ outputPath,
54
+ forceFallback,
55
+ disableFallback,
56
+ } = workerData;
57
+
58
+ (async () => {
59
+ try {
60
+ const finalStyles = await run(baseContent, extraContent, inlineContent, {
61
+ forceFallback,
62
+ disableFallback,
63
+ });
64
+ // generate 'public/styles.min.css'
65
+ fs.writeFileSync(outputPath, finalStyles);
66
+
67
+ parentPort.postMessage("success");
68
+ } catch (error) {
69
+ parentPort.postMessage({ error: error.message });
70
+ }
71
+ })();
72
+ }