@velox0/cerver 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -1,8 +1,12 @@
1
+ <div align="center">
2
+ <img src="templates/cerver.png" alt="Cerver Logo" width="120" />
3
+ </div>
4
+
1
5
  # Cerver
2
6
 
3
7
  A lightweight, compile-time web framework that transpiles restricted JavaScript server logic into highly optimized native C HTTP server binaries.
4
8
 
5
- Cerver takes a Next.js-style file-based routing structure (written in a strict subset of JavaScript), parses it, generates equivalent C code, embeds your static assets, and compiles it all into a single, standalone executable (~50KB) that runs with zero Node.js dependency.
9
+ Cerver takes a Next.js-style file-based routing structure (written in a strict subset of JavaScript), parses it, generates equivalent C code, embeds your static assets, and compiles it all into a single, standalone executable that runs with zero Node.js dependency.
6
10
 
7
11
  ## Features
8
12
 
@@ -154,6 +154,22 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
154
154
  brName,
155
155
  brLen,
156
156
  });
157
+
158
+ /* Auto-alias: /any/path/index.html → /any/path */
159
+ if (asset.servePath.endsWith("/index.html")) {
160
+ const dirPath = asset.servePath === "/index.html"
161
+ ? "/"
162
+ : asset.servePath.slice(0, -"/index.html".length);
163
+ assetEntries.push({
164
+ name,
165
+ servePath: dirPath,
166
+ mime,
167
+ gzName,
168
+ gzLen,
169
+ brName,
170
+ brLen,
171
+ });
172
+ }
157
173
  }
158
174
 
159
175
  /* Generate the asset table */
@@ -85,7 +85,7 @@ function generateServer(routes, config, assetsCode) {
85
85
  lines.push(" if (env_port) port = atoi(env_port);");
86
86
  lines.push("");
87
87
  lines.push(" cerver_server_t srv;");
88
- lines.push(" cerver_init(&srv, port);");
88
+ lines.push(` cerver_init(&srv, port, ${config.threads});`);
89
89
  lines.push("");
90
90
  lines.push(
91
91
  " cerver_add_routes(&srv, cerver_routes, cerver_route_count);"
@@ -53,15 +53,103 @@ function newProject(name) {
53
53
  <meta charset="utf-8">
54
54
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
55
55
  <title>${name}</title>
56
+ <link rel="icon" href="/favicon.ico" type="image/x-icon">
57
+ <style>
58
+ :root {
59
+ --bg-color: #0f172a;
60
+ --text-color: #f8fafc;
61
+ --accent-color: #38bdf8;
62
+ --card-bg: rgba(30, 41, 59, 0.7);
63
+ }
64
+ body {
65
+ margin: 0;
66
+ padding: 0;
67
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
68
+ background-color: var(--bg-color);
69
+ color: var(--text-color);
70
+ display: flex;
71
+ flex-direction: column;
72
+ align-items: center;
73
+ justify-content: center;
74
+ min-height: 100vh;
75
+ background: radial-gradient(circle at top right, #1e293b, #0f172a);
76
+ }
77
+ .container {
78
+ background: var(--card-bg);
79
+ backdrop-filter: blur(12px);
80
+ -webkit-backdrop-filter: blur(12px);
81
+ border: 1px solid rgba(255, 255, 255, 0.1);
82
+ padding: 3rem;
83
+ border-radius: 16px;
84
+ text-align: center;
85
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
86
+ max-width: 500px;
87
+ width: 90%;
88
+ animation: fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
89
+ opacity: 0;
90
+ transform: translateY(20px);
91
+ }
92
+ @keyframes fadeUp {
93
+ to { opacity: 1; transform: translateY(0); }
94
+ }
95
+ .logo {
96
+ width: 120px;
97
+ height: 120px;
98
+ margin-bottom: 1.5rem;
99
+ border-radius: 24%;
100
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
101
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
102
+ }
103
+ .logo:hover {
104
+ transform: scale(1.08) rotate(-3deg);
105
+ }
106
+ h1 {
107
+ margin: 0 0 1rem 0;
108
+ font-size: 2.5rem;
109
+ font-weight: 700;
110
+ letter-spacing: -0.025em;
111
+ }
112
+ p {
113
+ margin: 0;
114
+ color: #94a3b8;
115
+ font-size: 1.125rem;
116
+ line-height: 1.6;
117
+ }
118
+ .badge {
119
+ display: inline-block;
120
+ margin-top: 2rem;
121
+ padding: 0.5rem 1rem;
122
+ background: rgba(56, 189, 248, 0.1);
123
+ color: var(--accent-color);
124
+ border-radius: 9999px;
125
+ font-size: 0.875rem;
126
+ font-weight: 600;
127
+ border: 1px solid rgba(56, 189, 248, 0.2);
128
+ }
129
+ </style>
56
130
  </head>
57
131
  <body>
58
- <h1>${name}</h1>
59
- <p>Served by cerver.</p>
132
+ <div class="container">
133
+ <img src="/cerver.png" alt="cerver logo" class="logo">
134
+ <h1>${name}</h1>
135
+ <p>Your ultra-fast, native web application is running.</p>
136
+ <div class="badge">Powered by cerver</div>
137
+ </div>
60
138
  </body>
61
139
  </html>
62
140
  `
63
141
  );
64
142
 
143
+ // Copy standard static assets
144
+ fs.copyFileSync(
145
+ path.join(templatesDir, "cerver.png"),
146
+ path.join(projectDir, "public", "cerver.png")
147
+ );
148
+ fs.copyFileSync(
149
+ path.join(templatesDir, "favicon.ico"),
150
+ path.join(projectDir, "public", "favicon.ico")
151
+ );
152
+
65
153
  // package.json
66
154
  fs.writeFileSync(
67
155
  path.join(projectDir, "package.json"),
@@ -83,6 +171,8 @@ function newProject(name) {
83
171
  console.log(" Created:");
84
172
  console.log(" app/routes/index.js");
85
173
  console.log(" public/index.html");
174
+ console.log(" public/cerver.png");
175
+ console.log(" public/favicon.ico");
86
176
  console.log(" cerver.config.js");
87
177
  console.log(" package.json");
88
178
  console.log("");
@@ -57,6 +57,7 @@ function compile(distDir, runtimeDir, opts) {
57
57
  serverC,
58
58
  ...runtimeSources,
59
59
  `-I${runtimeDir}`,
60
+ "-lpthread",
60
61
  ];
61
62
 
62
63
  if (opts && opts.static) {
package/lib/config.js CHANGED
@@ -8,6 +8,7 @@ const DEFAULTS = {
8
8
  embed: true,
9
9
  minify: true,
10
10
  compression: "none",
11
+ threads: 4,
11
12
  };
12
13
 
13
14
  /**
@@ -45,6 +46,9 @@ function loadConfig(projectDir) {
45
46
  if (!["none", "gzip", "brotli", "both"].includes(config.compression)) {
46
47
  throw new Error(`cerver: unsupported compression "${config.compression}"`);
47
48
  }
49
+ if (!Number.isInteger(config.threads) || config.threads < 1 || config.threads > 64) {
50
+ throw new Error(`cerver: invalid thread count ${config.threads} (must be 1-64)`);
51
+ }
48
52
 
49
53
  return config;
50
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/runtime/cerver.h CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  #include <stddef.h>
13
13
  #include <stdint.h>
14
+ #include <pthread.h>
14
15
 
15
16
  /* ------------------------------------------------------------------ */
16
17
  /* Limits */
@@ -22,8 +23,12 @@
22
23
  #define CERVER_MAX_PATH 2048
23
24
  #define CERVER_MAX_HEADER_VAL 4096
24
25
  #define CERVER_READ_BUF 8192
26
+ #define CERVER_READ_BUF_MAX (1 << 20) /* 1 MB hard limit */
25
27
  #define CERVER_MAX_ROUTES 256
26
28
 
29
+ #define CERVER_THREAD_POOL_DEFAULT 4
30
+ #define CERVER_TASK_QUEUE_SIZE 256
31
+
27
32
  /* ------------------------------------------------------------------ */
28
33
  /* Key-value pair (used for headers, query params, route params) */
29
34
  /* ------------------------------------------------------------------ */
@@ -146,10 +151,20 @@ typedef struct {
146
151
  int asset_count;
147
152
  const char *public_dir; /* NULL if embedded mode */
148
153
  volatile int running;
154
+
155
+ /* Thread pool */
156
+ int thread_count;
157
+ pthread_t *threads;
158
+ int task_queue[CERVER_TASK_QUEUE_SIZE];
159
+ int tq_head;
160
+ int tq_tail;
161
+ int tq_count;
162
+ pthread_mutex_t tq_mutex;
163
+ pthread_cond_t tq_cond;
149
164
  } cerver_server_t;
150
165
 
151
166
  /* Server lifecycle */
152
- int cerver_init(cerver_server_t *srv, int port);
167
+ int cerver_init(cerver_server_t *srv, int port, int threads);
153
168
  int cerver_add_routes(cerver_server_t *srv, cerver_route_t *routes, int count);
154
169
  int cerver_set_assets(cerver_server_t *srv, cerver_asset_t *assets, int count);
155
170
  void cerver_set_public_dir(cerver_server_t *srv, const char *dir);
package/runtime/server.c CHANGED
@@ -1,7 +1,9 @@
1
1
  /*
2
- * server.c — Socket setup and event loop for the cerver runtime.
2
+ * server.c — Socket setup, thread pool, and event loop for the cerver runtime.
3
3
  *
4
4
  * Uses kqueue on macOS, epoll on Linux, with a select() fallback.
5
+ * Requests are dispatched to a fixed-size thread pool via a ring-buffer
6
+ * task queue protected by a mutex + condition variable.
5
7
  */
6
8
 
7
9
  #include "cerver.h"
@@ -17,6 +19,7 @@
17
19
  #include <sys/types.h>
18
20
  #include <netinet/in.h>
19
21
  #include <arpa/inet.h>
22
+ #include <pthread.h>
20
23
 
21
24
  /* Platform-specific event API */
22
25
  #if defined(__APPLE__) || defined(__FreeBSD__)
@@ -49,53 +52,122 @@ static int set_nonblocking(int fd) {
49
52
  }
50
53
 
51
54
  /* ------------------------------------------------------------------ */
52
- /* Handle a single connection: read, parse, dispatch, respond */
55
+ /* memmem fallback for systems that lack it */
53
56
  /* ------------------------------------------------------------------ */
54
57
 
55
- static int set_blocking(int fd) {
56
- int flags = fcntl(fd, F_GETFL, 0);
57
- if (flags < 0) return -1;
58
- return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
58
+ #if !defined(__APPLE__) && !defined(__linux__) && !defined(_GNU_SOURCE)
59
+ static void *cerver_memmem(const void *hay, size_t haylen,
60
+ const void *needle, size_t nlen) {
61
+ if (nlen == 0) return (void *)hay;
62
+ if (nlen > haylen) return NULL;
63
+ const char *p = (const char *)hay;
64
+ const char *end = p + haylen - nlen;
65
+ for (; p <= end; p++) {
66
+ if (memcmp(p, needle, nlen) == 0) return (void *)p;
67
+ }
68
+ return NULL;
59
69
  }
70
+ #define memmem cerver_memmem
71
+ #endif
72
+
73
+ /* ------------------------------------------------------------------ */
74
+ /* Buffered read — accumulates until \r\n\r\n or limit reached */
75
+ /* ------------------------------------------------------------------ */
76
+
77
+ static char *read_full_request(int fd, size_t *out_len) {
78
+ size_t cap = CERVER_READ_BUF;
79
+ size_t len = 0;
80
+ char *buf = malloc(cap + 1); /* +1 for null terminator */
81
+ if (!buf) return NULL;
82
+
83
+ while (len < (size_t)CERVER_READ_BUF_MAX) {
84
+ ssize_t n = read(fd, buf + len, cap - len);
85
+ if (n <= 0) break;
86
+ len += (size_t)n;
87
+
88
+ /* Check for end of headers */
89
+ if (len >= 4 && memmem(buf, len, "\r\n\r\n", 4)) break;
90
+
91
+ /* Grow buffer if full */
92
+ if (len == cap) {
93
+ size_t newcap = cap * 2;
94
+ if (newcap > (size_t)CERVER_READ_BUF_MAX)
95
+ newcap = (size_t)CERVER_READ_BUF_MAX;
96
+ char *tmp = realloc(buf, newcap + 1);
97
+ if (!tmp) { free(buf); return NULL; }
98
+ buf = tmp;
99
+ cap = newcap;
100
+ }
101
+ }
102
+
103
+ if (len == 0) {
104
+ free(buf);
105
+ return NULL;
106
+ }
107
+
108
+ buf[len] = '\0';
109
+ *out_len = len;
110
+ return buf;
111
+ }
112
+
113
+ /* ------------------------------------------------------------------ */
114
+ /* Handle a single connection: read, parse, dispatch, respond */
115
+ /* ------------------------------------------------------------------ */
60
116
 
61
117
  static void handle_connection(cerver_server_t *srv, int client_fd) {
62
- /* Client sockets may inherit non-blocking mode from listener.
63
- Set to blocking so read() waits for data. */
64
- set_blocking(client_fd);
118
+ /* Ensure the client socket is in blocking mode for reads.
119
+ Some platforms inherit O_NONBLOCK from the listening socket. */
120
+ int flags = fcntl(client_fd, F_GETFL, 0);
121
+ if (flags >= 0 && (flags & O_NONBLOCK)) {
122
+ fcntl(client_fd, F_SETFL, flags & ~O_NONBLOCK);
123
+ }
124
+
125
+ /* Set a read timeout so workers don't block on stale connections */
126
+ struct timeval tv = { 5, 0 }; /* 5 seconds */
127
+ setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
65
128
 
66
- char buf[CERVER_READ_BUF];
67
- ssize_t n = read(client_fd, buf, sizeof(buf) - 1);
129
+ /* Read the full request with buffering */
130
+ size_t req_len = 0;
131
+ char *buf = read_full_request(client_fd, &req_len);
68
132
 
69
- if (n <= 0) {
133
+ if (!buf || req_len == 0) {
134
+ if (buf) free(buf);
70
135
  close(client_fd);
71
136
  return;
72
137
  }
73
- buf[n] = '\0';
74
138
 
75
139
  /* Parse HTTP request */
76
140
  cerver_request_t req;
77
141
  memset(&req, 0, sizeof(req));
78
142
 
79
- if (cerver_parse_request(buf, (size_t)n, &req) < 0) {
143
+ if (cerver_parse_request(buf, req_len, &req) < 0) {
80
144
  /* Bad request — send 400 */
81
145
  const char *resp = "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request";
82
146
  write(client_fd, resp, strlen(resp));
147
+ free(buf);
83
148
  close(client_fd);
84
149
  return;
85
150
  }
86
151
 
152
+ /*
153
+ * The parser malloc'd its own mutable copy (req._raw_buf) and all
154
+ * internal pointers (path, headers, etc.) reference that copy.
155
+ * We can now free our original read buffer.
156
+ */
157
+ free(buf);
158
+
87
159
  /* Prepare response */
88
160
  cerver_response_t res;
89
161
  memset(&res, 0, sizeof(res));
90
162
 
91
- /* Try to dispatch to a route handler */
92
- cerver_handler_fn handler = cerver_dispatch(srv, &req);
163
+ /* Try static assets first files take priority over route handlers */
164
+ if (cerver_serve_static(srv, &req, &res) < 0) {
165
+ /* No static file found — try route handlers */
166
+ cerver_handler_fn handler = cerver_dispatch(srv, &req);
93
167
 
94
- if (handler) {
95
- handler(&req, &res);
96
- } else {
97
- /* Try static assets */
98
- if (cerver_serve_static(srv, &req, &res) < 0) {
168
+ if (handler) {
169
+ handler(&req, &res);
170
+ } else {
99
171
  /* 404 */
100
172
  cerver_res_text(&res, 404, "Not Found");
101
173
  }
@@ -115,16 +187,91 @@ static void handle_connection(cerver_server_t *srv, int client_fd) {
115
187
  close(client_fd);
116
188
  }
117
189
 
190
+ /* ------------------------------------------------------------------ */
191
+ /* Thread pool — task queue (ring buffer) */
192
+ /* ------------------------------------------------------------------ */
193
+
194
+ static int enqueue_task(cerver_server_t *srv, int client_fd) {
195
+ pthread_mutex_lock(&srv->tq_mutex);
196
+
197
+ if (srv->tq_count >= CERVER_TASK_QUEUE_SIZE) {
198
+ /* Queue full — drop connection */
199
+ pthread_mutex_unlock(&srv->tq_mutex);
200
+ const char *resp = "HTTP/1.1 503 Service Unavailable\r\n"
201
+ "Content-Length: 19\r\nConnection: close\r\n\r\n"
202
+ "Service Unavailable";
203
+ write(client_fd, resp, strlen(resp));
204
+ close(client_fd);
205
+ return -1;
206
+ }
207
+
208
+ srv->task_queue[srv->tq_tail] = client_fd;
209
+ srv->tq_tail = (srv->tq_tail + 1) % CERVER_TASK_QUEUE_SIZE;
210
+ srv->tq_count++;
211
+
212
+ pthread_cond_signal(&srv->tq_cond);
213
+ pthread_mutex_unlock(&srv->tq_mutex);
214
+ return 0;
215
+ }
216
+
217
+ static int dequeue_task(cerver_server_t *srv) {
218
+ pthread_mutex_lock(&srv->tq_mutex);
219
+
220
+ while (srv->tq_count == 0 && srv->running) {
221
+ pthread_cond_wait(&srv->tq_cond, &srv->tq_mutex);
222
+ }
223
+
224
+ if (!srv->running && srv->tq_count == 0) {
225
+ pthread_mutex_unlock(&srv->tq_mutex);
226
+ return -1; /* shutdown signal */
227
+ }
228
+
229
+ int fd = srv->task_queue[srv->tq_head];
230
+ srv->tq_head = (srv->tq_head + 1) % CERVER_TASK_QUEUE_SIZE;
231
+ srv->tq_count--;
232
+
233
+ pthread_mutex_unlock(&srv->tq_mutex);
234
+ return fd;
235
+ }
236
+
237
+ /* ------------------------------------------------------------------ */
238
+ /* Worker thread entry point */
239
+ /* ------------------------------------------------------------------ */
240
+
241
+ static void *worker_thread(void *arg) {
242
+ cerver_server_t *srv = (cerver_server_t *)arg;
243
+
244
+ while (srv->running) {
245
+ int client_fd = dequeue_task(srv);
246
+ if (client_fd < 0) break; /* shutdown */
247
+
248
+ handle_connection(srv, client_fd);
249
+ }
250
+
251
+ return NULL;
252
+ }
253
+
118
254
  /* ------------------------------------------------------------------ */
119
255
  /* Server init */
120
256
  /* ------------------------------------------------------------------ */
121
257
 
122
- int cerver_init(cerver_server_t *srv, int port) {
258
+ int cerver_init(cerver_server_t *srv, int port, int threads) {
123
259
  memset(srv, 0, sizeof(*srv));
124
260
  srv->port = port;
125
261
  srv->sock_fd = -1;
126
262
  srv->running = 0;
127
263
  srv->public_dir = NULL;
264
+
265
+ /* Thread pool config */
266
+ srv->thread_count = (threads > 0) ? threads : CERVER_THREAD_POOL_DEFAULT;
267
+ srv->threads = NULL;
268
+ srv->tq_head = 0;
269
+ srv->tq_tail = 0;
270
+ srv->tq_count = 0;
271
+
272
+ pthread_mutex_init(&srv->tq_mutex, NULL);
273
+ pthread_cond_init(&srv->tq_cond, NULL);
274
+
128
275
  return 0;
129
276
  }
130
277
 
@@ -144,6 +291,43 @@ void cerver_set_public_dir(cerver_server_t *srv, const char *dir) {
144
291
  srv->public_dir = dir;
145
292
  }
146
293
 
294
+ /* ------------------------------------------------------------------ */
295
+ /* Start thread pool */
296
+ /* ------------------------------------------------------------------ */
297
+
298
+ static int start_thread_pool(cerver_server_t *srv) {
299
+ srv->threads = malloc(sizeof(pthread_t) * (size_t)srv->thread_count);
300
+ if (!srv->threads) {
301
+ perror("cerver: malloc threads");
302
+ return -1;
303
+ }
304
+
305
+ /* Use 2 MB stack per worker (handles deep call chains with large buffers) */
306
+ pthread_attr_t attr;
307
+ pthread_attr_init(&attr);
308
+ pthread_attr_setstacksize(&attr, 2 * 1024 * 1024);
309
+
310
+ for (int i = 0; i < srv->thread_count; i++) {
311
+ if (pthread_create(&srv->threads[i], &attr, worker_thread, srv) != 0) {
312
+ perror("cerver: pthread_create");
313
+ /* Clean up already-created threads */
314
+ srv->running = 0;
315
+ pthread_cond_broadcast(&srv->tq_cond);
316
+ for (int j = 0; j < i; j++) {
317
+ pthread_join(srv->threads[j], NULL);
318
+ }
319
+ free(srv->threads);
320
+ srv->threads = NULL;
321
+ return -1;
322
+ }
323
+ }
324
+
325
+ pthread_attr_destroy(&attr);
326
+
327
+ printf("cerver: started %d worker thread(s)\n", srv->thread_count);
328
+ return 0;
329
+ }
330
+
147
331
  /* ------------------------------------------------------------------ */
148
332
  /* Server listen — event loop */
149
333
  /* ------------------------------------------------------------------ */
@@ -190,7 +374,13 @@ int cerver_listen(cerver_server_t *srv) {
190
374
 
191
375
  srv->running = 1;
192
376
 
193
- printf("cerver: listening on http://0.0.0.0:%d\n", srv->port);
377
+ printf("cerver: listening on http://localhost:%d\n", srv->port);
378
+
379
+ /* Start the thread pool */
380
+ if (start_thread_pool(srv) < 0) {
381
+ close(srv->sock_fd);
382
+ return -1;
383
+ }
194
384
 
195
385
  /* ================================================================== */
196
386
  /* kqueue event loop (macOS / FreeBSD) */
@@ -236,7 +426,7 @@ int cerver_listen(cerver_server_t *srv) {
236
426
  perror("cerver: accept");
237
427
  break;
238
428
  }
239
- handle_connection(srv, client_fd);
429
+ enqueue_task(srv, client_fd);
240
430
  }
241
431
  }
242
432
  }
@@ -285,7 +475,7 @@ int cerver_listen(cerver_server_t *srv) {
285
475
  perror("cerver: accept");
286
476
  break;
287
477
  }
288
- handle_connection(srv, client_fd);
478
+ enqueue_task(srv, client_fd);
289
479
  }
290
480
  }
291
481
  }
@@ -319,7 +509,7 @@ int cerver_listen(cerver_server_t *srv) {
319
509
  (struct sockaddr *)&client_addr,
320
510
  &client_len);
321
511
  if (client_fd >= 0) {
322
- handle_connection(srv, client_fd);
512
+ enqueue_task(srv, client_fd);
323
513
  }
324
514
  }
325
515
  }
@@ -335,10 +525,31 @@ int cerver_listen(cerver_server_t *srv) {
335
525
  /* ------------------------------------------------------------------ */
336
526
 
337
527
  void cerver_shutdown(cerver_server_t *srv) {
528
+ srv->running = 0;
529
+
530
+ /* Wake all worker threads so they can exit */
531
+ pthread_mutex_lock(&srv->tq_mutex);
532
+ pthread_cond_broadcast(&srv->tq_cond);
533
+ pthread_mutex_unlock(&srv->tq_mutex);
534
+
535
+ /* Join all worker threads */
536
+ if (srv->threads) {
537
+ for (int i = 0; i < srv->thread_count; i++) {
538
+ pthread_join(srv->threads[i], NULL);
539
+ }
540
+ free(srv->threads);
541
+ srv->threads = NULL;
542
+ }
543
+
544
+ /* Close listener socket */
338
545
  if (srv->sock_fd >= 0) {
339
546
  close(srv->sock_fd);
340
547
  srv->sock_fd = -1;
341
548
  }
342
- srv->running = 0;
549
+
550
+ /* Destroy synchronization primitives */
551
+ pthread_mutex_destroy(&srv->tq_mutex);
552
+ pthread_cond_destroy(&srv->tq_cond);
553
+
343
554
  printf("\ncerver: server stopped\n");
344
555
  }
Binary file
Binary file
package/test/run.js CHANGED
@@ -198,6 +198,7 @@ test("loadConfig merges defaults and supports export default configs", () => {
198
198
  embed: true,
199
199
  minify: true,
200
200
  compression: "none",
201
+ threads: 4,
201
202
  });
202
203
 
203
204
  writeFile(
@@ -210,6 +211,7 @@ test("loadConfig merges defaults and supports export default configs", () => {
210
211
  embed: false,
211
212
  minify: false,
212
213
  compression: "gzip",
214
+ threads: 4,
213
215
  });
214
216
  } finally {
215
217
  cleanup(dir);