@velox0/cerver 0.4.2 → 0.4.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/README.md CHANGED
@@ -1,9 +1,11 @@
1
- <div align="center">
2
- <img src="templates/cerver.png" alt="Cerver Logo" width="120" />
3
- </div>
4
-
5
1
  # Cerver
6
2
 
3
+ [![Publish](https://github.com/velox0/cerver/actions/workflows/publish.yml/badge.svg)](https://github.com/velox0/cerver/actions/workflows/publish.yml)
4
+ [![CI](https://github.com/velox0/cerver/actions/workflows/ci.yml/badge.svg)](https://github.com/velox0/cerver/actions/workflows/ci.yml)
5
+ [![npm](https://img.shields.io/npm/v/@velox0/cerver)](https://www.npmjs.com/package/@velox0/cerver)
6
+
7
+ <img src="templates/cerver.png" alt="Cerver Logo" width="200px" align="right" />
8
+
7
9
  A lightweight, compile-time web framework that transpiles restricted JavaScript server logic into highly optimized native C HTTP server binaries.
8
10
 
9
11
  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.
@@ -11,11 +13,26 @@ Cerver takes a Next.js-style file-based routing structure (written in a strict s
11
13
  ## Features
12
14
 
13
15
  - **Compile-Time Framework**: Your JavaScript is parsed and compiled to native C. There is no JavaScript engine (like V8) or interpreter included in the final binary.
14
- - **Microscopic Footprint**: Generated executables are typically ~50KB and start in milliseconds.
16
+ - **Microscopic Footprint**: Generated executables are tiny and start in milliseconds.
15
17
  - **Single-Binary Deployment**: Static assets (HTML, CSS, JS, images) are automatically minified and embedded directly into the executable as C byte arrays.
16
18
  - **Native Performance**: Uses `kqueue` (macOS) or `epoll` (Linux) event loops for high-performance non-blocking I/O.
17
19
  - **File-Based Routing**: Intuitive `app/routes/` directory structure, supporting dynamic segments (e.g., `/item/[id].js`).
18
20
 
21
+ ## Benchmarks (Autocannon)
22
+
23
+ Local loopback runs against `localhost`, 20s per run.
24
+
25
+ Note: timeouts only appear at the highest concurrency (240 connections). On a single machine, autocannon and the server compete for CPU and kernel resources; the timeouts are likely client/loopback saturation rather than server errors.
26
+
27
+ | Connections | Pipelining | Avg req/s | Avg latency | p99 latency | Total read | Errors (timeouts) |
28
+ | ----------- | ---------- | --------- | ----------- | ----------- | ---------- | ----------------- |
29
+ | 60 | 1 | 123,005 | 0.01 ms | 0 ms | 21.0 GB | 0 |
30
+ | 60 | 10 | 125,973 | 4.26 ms | 10 ms | 21.5 GB | 0 |
31
+ | 120 | 1 | 124,214 | 0.15 ms | 1 ms | 21.2 GB | 0 |
32
+ | 120 | 10 | 131,245 | 8.64 ms | 15 ms | 22.4 GB | 0 |
33
+ | 240 | 1 | 124,890 | 0.54 ms | 1 ms | 21.3 GB | 118 (timeout) |
34
+ | 240 | 10 | 123,677 | 9.87 ms | 14 ms | 21.1 GB | 1540 (timeout) |
35
+
19
36
  ## Getting Started
20
37
 
21
38
  1. Install globally (requires `gcc` or `clang` on your system):
@@ -162,6 +162,9 @@ function generateDispatch(routes) {
162
162
  lines.push(
163
163
  ` req->params[req->params_count].value = seg${i}_start;`
164
164
  );
165
+ lines.push(
166
+ ` ((char*)seg${i}_start)[seg${i}_len] = '\\0';`
167
+ );
165
168
  lines.push(` req->params_count++;`);
166
169
  paramIdx++;
167
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries",
5
5
  "main": "bin/cerver.js",
6
6
  "bin": {
@@ -30,4 +30,4 @@
30
30
  "chokidar": "^3.6.0",
31
31
  "commander": "^13.1.0"
32
32
  }
33
- }
33
+ }
package/runtime/cerver.h CHANGED
@@ -119,6 +119,9 @@ typedef struct {
119
119
 
120
120
  /* Keep-alive control: set to 1 to force close after response */
121
121
  int _force_close;
122
+
123
+ /* Internal file descriptor for sendfile serving */
124
+ int _file_fd;
122
125
  } cerver_response_t;
123
126
 
124
127
  /* Response helpers — called by generated handler code */
@@ -233,6 +236,9 @@ struct cerver_server {
233
236
  /* Worker pool */
234
237
  int worker_count;
235
238
  cerver_worker_t* workers;
239
+
240
+ /* Route trie for radix/trie-based routing */
241
+ void* route_trie;
236
242
  };
237
243
 
238
244
  /* Server lifecycle */
@@ -262,6 +268,9 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive);
262
268
 
263
269
  int cerver_route_match(const cerver_route_t* route, cerver_request_t* req);
264
270
  cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req);
271
+ void* cerver_trie_create(void);
272
+ void cerver_trie_insert(void* trie, const char* pattern, const char* method, cerver_handler_fn handler);
273
+ void cerver_trie_free(void* trie);
265
274
 
266
275
  /* ------------------------------------------------------------------ */
267
276
  /* MIME (internal) */
@@ -12,6 +12,64 @@
12
12
  #include <string.h>
13
13
  #include <unistd.h>
14
14
  #include <sys/uio.h>
15
+ #include <errno.h>
16
+
17
+ #ifdef __linux__
18
+ #include <sys/sendfile.h>
19
+ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
20
+ off_t off = offset;
21
+ return sendfile(out_fd, in_fd, &off, count);
22
+ }
23
+ #elif defined(__APPLE__)
24
+ #include <sys/types.h>
25
+ #include <sys/socket.h>
26
+ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
27
+ off_t len = (off_t)count;
28
+ int res = sendfile(in_fd, out_fd, offset, &len, NULL, 0);
29
+ if (res == 0) {
30
+ return (ssize_t)len;
31
+ }
32
+ if (len > 0) {
33
+ return (ssize_t)len;
34
+ }
35
+ /* Fallback to read-write copy if not a socket or unsupported on this descriptor type */
36
+ char buf[8192];
37
+ if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
38
+ size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
39
+ ssize_t n_read = read(in_fd, buf, to_read);
40
+ if (n_read <= 0) return n_read;
41
+
42
+ size_t written = 0;
43
+ while (written < (size_t)n_read) {
44
+ ssize_t n_write = write(out_fd, buf + written, (size_t)n_read - written);
45
+ if (n_write < 0) {
46
+ if (errno == EINTR) continue;
47
+ return -1;
48
+ }
49
+ written += (size_t)n_write;
50
+ }
51
+ return (ssize_t)written;
52
+ }
53
+ #else
54
+ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
55
+ char buf[8192];
56
+ if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
57
+ size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
58
+ ssize_t n_read = read(in_fd, buf, to_read);
59
+ if (n_read <= 0) return n_read;
60
+
61
+ size_t written = 0;
62
+ while (written < (size_t)n_read) {
63
+ ssize_t n_write = write(out_fd, buf + written, (size_t)n_read - written);
64
+ if (n_write < 0) {
65
+ if (errno == EINTR) continue;
66
+ return -1;
67
+ }
68
+ written += (size_t)n_write;
69
+ }
70
+ return (ssize_t)written;
71
+ }
72
+ #endif
15
73
 
16
74
  /* ------------------------------------------------------------------ */
17
75
  /* Status text lookup */
@@ -93,40 +151,89 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
93
151
  hlen += snprintf(header + hlen, sizeof(header) - (size_t)hlen, "\r\n");
94
152
 
95
153
  /*
96
- * Use writev() to send header + body in a single syscall.
97
- * This avoids Nagle interaction and reduces context switches.
154
+ * Use writev() or sendfile() to send response, or copy to contiguous
155
+ * buffer if body is small to avoid writev round-trips.
98
156
  */
99
- if (res->body && res->body_len > 0) {
100
- struct iovec iov[2];
101
- iov[0].iov_base = header;
102
- iov[0].iov_len = (size_t)hlen;
103
- iov[1].iov_base = (void*)res->body;
104
- iov[1].iov_len = res->body_len;
105
-
106
- size_t total = iov[0].iov_len + iov[1].iov_len;
107
- size_t written = 0;
157
+ if (res->_body_owned == 3) {
158
+ /* Send header first */
159
+ size_t header_total = (size_t)hlen;
160
+ size_t header_written = 0;
161
+ while (header_written < header_total) {
162
+ ssize_t n = write(fd, header + header_written, header_total - header_written);
163
+ if (n < 0) {
164
+ if (errno == EINTR) continue;
165
+ return -1;
166
+ }
167
+ header_written += (size_t)n;
168
+ }
108
169
 
109
- while (written < total) {
110
- ssize_t n = writev(fd, iov, 2);
111
- if (n < 0) return -1;
112
- written += (size_t)n;
170
+ /* Zero-copy body sending via sendfile(2) */
171
+ size_t body_total = res->body_len;
172
+ size_t body_sent = 0;
173
+ while (body_sent < body_total) {
174
+ ssize_t n = cerver_sendfile(fd, res->_file_fd, (off_t)body_sent, body_total - body_sent);
175
+ if (n < 0) {
176
+ if (errno == EINTR) continue;
177
+ return -1;
178
+ }
179
+ if (n == 0) break; /* EOF */
180
+ body_sent += (size_t)n;
181
+ }
182
+ } else if (res->body && res->body_len > 0) {
183
+ if ((size_t)hlen + res->body_len <= sizeof(header)) {
184
+ /* Small response optimization: copy body into header buffer and send in one syscall */
185
+ memcpy(header + hlen, res->body, res->body_len);
186
+ size_t total = (size_t)hlen + res->body_len;
187
+ size_t written = 0;
188
+ while (written < total) {
189
+ ssize_t n = write(fd, header + written, total - written);
190
+ if (n < 0) {
191
+ if (errno == EINTR) continue;
192
+ return -1;
193
+ }
194
+ written += (size_t)n;
195
+ }
196
+ } else {
197
+ /* Large response: use writev to send header + body */
198
+ struct iovec iov[2];
199
+ iov[0].iov_base = header;
200
+ iov[0].iov_len = (size_t)hlen;
201
+ iov[1].iov_base = (void*)res->body;
202
+ iov[1].iov_len = res->body_len;
203
+
204
+ size_t total = iov[0].iov_len + iov[1].iov_len;
205
+ size_t written = 0;
113
206
 
114
- /* Adjust iov for partial writes */
115
- if (written < iov[0].iov_len) {
116
- iov[0].iov_base = header + written;
117
- iov[0].iov_len -= (size_t)n;
118
- } else {
119
- /* Header fully sent, adjust body iov */
120
- size_t body_sent = written - (size_t)hlen;
121
- iov[0].iov_len = 0;
122
- iov[1].iov_base = (void*)(res->body + body_sent);
123
- iov[1].iov_len = res->body_len - body_sent;
207
+ while (written < total) {
208
+ ssize_t n = writev(fd, iov, 2);
209
+ if (n < 0) return -1;
210
+ written += (size_t)n;
211
+
212
+ /* Adjust iov for partial writes */
213
+ size_t to_consume = (size_t)n;
214
+ if (to_consume < iov[0].iov_len) {
215
+ iov[0].iov_base = (char*)iov[0].iov_base + to_consume;
216
+ iov[0].iov_len -= to_consume;
217
+ } else {
218
+ to_consume -= iov[0].iov_len;
219
+ iov[0].iov_len = 0;
220
+ iov[1].iov_base = (char*)iov[1].iov_base + to_consume;
221
+ iov[1].iov_len -= to_consume;
222
+ }
124
223
  }
125
224
  }
126
225
  } else {
127
226
  /* No body — just send header */
128
- ssize_t written = write(fd, header, (size_t)hlen);
129
- if (written < 0) return -1;
227
+ size_t total = (size_t)hlen;
228
+ size_t written = 0;
229
+ while (written < total) {
230
+ ssize_t n = write(fd, header + written, total - written);
231
+ if (n < 0) {
232
+ if (errno == EINTR) continue;
233
+ return -1;
234
+ }
235
+ written += (size_t)n;
236
+ }
130
237
  }
131
238
 
132
239
  return 0;
package/runtime/router.c CHANGED
@@ -19,8 +19,15 @@
19
19
 
20
20
  const char* cerver_req_param(const cerver_request_t* req, const char* key) {
21
21
  for (int i = 0; i < req->params_count; i++) {
22
- if (strcmp(req->params[i].key, key) == 0) {
23
- return req->params[i].value;
22
+ const char* pkey = req->params[i].key;
23
+ if (pkey && pkey[0] == key[0]) {
24
+ int j = 0;
25
+ while (key[j] && pkey[j] == key[j]) {
26
+ j++;
27
+ }
28
+ if (key[j] == '\0' && (pkey[j] == '\0' || pkey[j] == '/')) {
29
+ return req->params[i].value;
30
+ }
24
31
  }
25
32
  }
26
33
  return "";
@@ -102,6 +109,8 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
102
109
  const char* rp = path; /* request path pointer */
103
110
 
104
111
  int saved_params = req->params_count;
112
+ char* param_slashes[CERVER_MAX_PARAMS];
113
+ int param_slash_count = 0;
105
114
 
106
115
  /* Skip leading '/' */
107
116
  if (*pp == '/') pp++;
@@ -122,10 +131,12 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
122
131
  /* Dynamic segment — extract parameter */
123
132
  if (req->params_count < CERVER_MAX_PARAMS) {
124
133
  req->params[req->params_count].key = pp_seg + 1;
125
- /* Temporarily NUL-terminate the key at the slash */
126
- /* The key points into the route pattern (static/const) */
134
+ /* The key points into the route pattern (static/const, not NUL-terminated) */
127
135
  req->params[req->params_count].value = rp_seg;
128
136
  req->params_count++;
137
+ if (*rp == '/') {
138
+ param_slashes[param_slash_count++] = (char*)rp;
139
+ }
129
140
  }
130
141
  } else {
131
142
  /* Static segment — must match exactly */
@@ -146,6 +157,11 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
146
157
  return 0;
147
158
  }
148
159
 
160
+ /* Match succeeded, NUL-terminate extracted values in-place inside req->path */
161
+ for (int i = 0; i < param_slash_count; i++) {
162
+ *param_slashes[i] = '\0';
163
+ }
164
+
149
165
  return 1;
150
166
  }
151
167
 
@@ -153,6 +169,166 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
153
169
  /* Dispatch: find and return the handler for a request */
154
170
  /* ------------------------------------------------------------------ */
155
171
 
172
+ /* ------------------------------------------------------------------ */
173
+ /* Trie/Radix Route Router */
174
+ /* ------------------------------------------------------------------ */
175
+
176
+ static char* trie_strndup(const char* s, size_t n) {
177
+ char* p = malloc(n + 1);
178
+ if (p) {
179
+ memcpy(p, s, n);
180
+ p[n] = '\0';
181
+ }
182
+ return p;
183
+ }
184
+
185
+ typedef struct trie_node trie_node_t;
186
+
187
+ struct trie_node {
188
+ char* segment;
189
+ int is_param;
190
+ char* param_name;
191
+
192
+ struct {
193
+ const char* method;
194
+ cerver_handler_fn handler;
195
+ } handlers[16];
196
+ int handler_count;
197
+
198
+ trie_node_t** children;
199
+ int children_count;
200
+ int children_cap;
201
+ };
202
+
203
+ void* cerver_trie_create(void) {
204
+ trie_node_t* node = calloc(1, sizeof(trie_node_t));
205
+ return node;
206
+ }
207
+
208
+ static trie_node_t* trie_create_node(const char* segment, size_t len) {
209
+ trie_node_t* node = calloc(1, sizeof(trie_node_t));
210
+ if (node && segment) {
211
+ node->segment = trie_strndup(segment, len);
212
+ if (node->segment[0] == ':') {
213
+ node->is_param = 1;
214
+ node->param_name = trie_strndup(node->segment + 1, len - 1);
215
+ }
216
+ }
217
+ return node;
218
+ }
219
+
220
+ void cerver_trie_insert(void* trie, const char* pattern, const char* method, cerver_handler_fn handler) {
221
+ if (!trie) return;
222
+ trie_node_t* curr = (trie_node_t*)trie;
223
+ const char* p = pattern;
224
+ while (*p == '/') p++;
225
+
226
+ while (*p) {
227
+ const char* seg_start = p;
228
+ while (*p && *p != '/') p++;
229
+ size_t len = (size_t)(p - seg_start);
230
+ if (len == 0) {
231
+ while (*p == '/') p++;
232
+ continue;
233
+ }
234
+
235
+ // Find if child exists
236
+ trie_node_t* child = NULL;
237
+ for (int i = 0; i < curr->children_count; i++) {
238
+ trie_node_t* c = curr->children[i];
239
+ if (strlen(c->segment) == len && memcmp(c->segment, seg_start, len) == 0) {
240
+ child = c;
241
+ break;
242
+ }
243
+ }
244
+
245
+ if (!child) {
246
+ child = trie_create_node(seg_start, len);
247
+ if (curr->children_count >= curr->children_cap) {
248
+ curr->children_cap = curr->children_cap == 0 ? 4 : curr->children_cap * 2;
249
+ curr->children = realloc(curr->children, curr->children_cap * sizeof(trie_node_t*));
250
+ }
251
+ curr->children[curr->children_count++] = child;
252
+ }
253
+
254
+ curr = child;
255
+ while (*p == '/') p++;
256
+ }
257
+
258
+ // Add handler to leaf
259
+ if (curr->handler_count < 16) {
260
+ curr->handlers[curr->handler_count].method = method;
261
+ curr->handlers[curr->handler_count].handler = handler;
262
+ curr->handler_count++;
263
+ }
264
+ }
265
+
266
+ void cerver_trie_free(void* trie) {
267
+ if (!trie) return;
268
+ trie_node_t* node = (trie_node_t*)trie;
269
+ for (int i = 0; i < node->children_count; i++) {
270
+ cerver_trie_free(node->children[i]);
271
+ }
272
+ free(node->children);
273
+ free(node->segment);
274
+ free(node->param_name);
275
+ free(node);
276
+ }
277
+
278
+ static int trie_match_recursive(trie_node_t* node, const char* path, cerver_request_t* req, cerver_handler_fn* out_handler, int param_start_idx) {
279
+ // Skip leading slashes
280
+ while (*path == '/') path++;
281
+
282
+ if (*path == '\0') {
283
+ // Check if node has a handler for req->method
284
+ for (int i = 0; i < node->handler_count; i++) {
285
+ if (strcmp(node->handlers[i].method, req->method) == 0) {
286
+ *out_handler = node->handlers[i].handler;
287
+ req->params_count = param_start_idx;
288
+ return 1;
289
+ }
290
+ }
291
+ return 0;
292
+ }
293
+
294
+ // Extract next segment from path
295
+ const char* seg_start = path;
296
+ while (*path && *path != '/') path++;
297
+ size_t seg_len = (size_t)(path - seg_start);
298
+
299
+ // Try static children first
300
+ for (int i = 0; i < node->children_count; i++) {
301
+ trie_node_t* child = node->children[i];
302
+ if (!child->is_param) {
303
+ if (strlen(child->segment) == seg_len && memcmp(child->segment, seg_start, seg_len) == 0) {
304
+ if (trie_match_recursive(child, path, req, out_handler, param_start_idx)) {
305
+ return 1;
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ // Try parameter/dynamic children next
312
+ for (int i = 0; i < node->children_count; i++) {
313
+ trie_node_t* child = node->children[i];
314
+ if (child->is_param) {
315
+ if (param_start_idx < CERVER_MAX_PARAMS) {
316
+ req->params[param_start_idx].key = child->param_name;
317
+ req->params[param_start_idx].value = seg_start;
318
+ }
319
+ if (trie_match_recursive(child, path, req, out_handler, param_start_idx + 1)) {
320
+ return 1;
321
+ }
322
+ }
323
+ }
324
+
325
+ return 0;
326
+ }
327
+
328
+ /* ------------------------------------------------------------------ */
329
+ /* Dispatch: find and return the handler for a request */
330
+ /* ------------------------------------------------------------------ */
331
+
156
332
  cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
157
333
  /* Try the generated compile-time dispatch first */
158
334
  if (srv->dispatch_override) {
@@ -160,13 +336,19 @@ cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
160
336
  if (h) return h;
161
337
  }
162
338
 
163
- /* Fall back to generic route table scan */
164
- if (!srv->routes) return NULL;
339
+ /* Fall back to generic route table scan via Trie */
340
+ if (!srv->route_trie) return NULL;
165
341
 
166
- for (int i = 0; i < srv->route_count; i++) {
167
- if (cerver_route_match(&srv->routes[i], req)) {
168
- return srv->routes[i].handler;
342
+ cerver_handler_fn handler = NULL;
343
+ req->params_count = 0;
344
+ if (trie_match_recursive((trie_node_t*)srv->route_trie, req->path, req, &handler, 0)) {
345
+ // NUL-terminate extracted values in-place inside req->path
346
+ for (int i = 0; i < req->params_count; i++) {
347
+ char* val = (char*)req->params[i].value;
348
+ while (*val && *val != '/') val++;
349
+ if (*val == '/') *val = '\0';
169
350
  }
351
+ return handler;
170
352
  }
171
353
 
172
354
  return NULL;
package/runtime/server.c CHANGED
@@ -278,6 +278,8 @@ static void handle_connection(cerver_server_t* srv, int client_fd) {
278
278
  free((void*)res.body);
279
279
  else if (res._body_owned == 2 && res.body)
280
280
  munmap((void*)res.body, res.body_len);
281
+ else if (res._body_owned == 3 && res._file_fd >= 0)
282
+ close(res._file_fd);
281
283
 
282
284
  free(buf);
283
285
  if (write_err < 0) break;
@@ -348,7 +350,7 @@ static int create_listener(int port, int reuseport) {
348
350
  int opt = 1;
349
351
  setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
350
352
 
351
- #ifdef __linux__
353
+ #if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
352
354
  if (reuseport) setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
353
355
  #else
354
356
  (void)reuseport;
@@ -621,6 +623,13 @@ int cerver_init(cerver_server_t* srv, int port, int threads) {
621
623
  int cerver_add_routes(cerver_server_t* srv, cerver_route_t* routes, int count) {
622
624
  srv->routes = routes;
623
625
  srv->route_count = count;
626
+
627
+ srv->route_trie = cerver_trie_create();
628
+ if (srv->route_trie) {
629
+ for (int i = 0; i < count; i++) {
630
+ cerver_trie_insert(srv->route_trie, routes[i].pattern, routes[i].method, routes[i].handler);
631
+ }
632
+ }
624
633
  return 0;
625
634
  }
626
635
 
@@ -702,7 +711,7 @@ int cerver_listen(cerver_server_t* srv) {
702
711
  return -1;
703
712
  }
704
713
 
705
- srv->sock_fd = create_listener(srv->port, 0);
714
+ srv->sock_fd = create_listener(srv->port, 1);
706
715
  if (srv->sock_fd < 0) {
707
716
  srv->running = 0;
708
717
  for (int i = 0; i < pool_size; i++) pthread_join(pool_threads[i], NULL);
@@ -730,9 +739,13 @@ int cerver_listen(cerver_server_t* srv) {
730
739
  w->id = i;
731
740
  w->srv = srv;
732
741
  w->event_fd = -1;
733
- #ifdef __linux__
734
- w->listen_fd = create_listener(srv->port, 1);
735
- if (w->listen_fd < 0) w->listen_fd = srv->sock_fd;
742
+ #if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
743
+ if (i == 0) {
744
+ w->listen_fd = srv->sock_fd;
745
+ } else {
746
+ w->listen_fd = create_listener(srv->port, 1);
747
+ if (w->listen_fd < 0) w->listen_fd = srv->sock_fd;
748
+ }
736
749
  #else
737
750
  w->listen_fd = srv->sock_fd;
738
751
  #endif
@@ -791,7 +804,7 @@ void cerver_shutdown(cerver_server_t* srv) {
791
804
 
792
805
  if (srv->workers) {
793
806
  for (int i = 0; i < srv->worker_count; i++) {
794
- #ifdef __linux__
807
+ #if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
795
808
  if (srv->workers[i].listen_fd != srv->sock_fd && srv->workers[i].listen_fd >= 0)
796
809
  close(srv->workers[i].listen_fd);
797
810
  #endif
@@ -801,6 +814,11 @@ void cerver_shutdown(cerver_server_t* srv) {
801
814
  srv->workers = NULL;
802
815
  }
803
816
 
817
+ if (srv->route_trie) {
818
+ cerver_trie_free(srv->route_trie);
819
+ srv->route_trie = NULL;
820
+ }
821
+
804
822
  if (srv->sock_fd >= 0) {
805
823
  close(srv->sock_fd);
806
824
  srv->sock_fd = -1;
package/runtime/static.c CHANGED
@@ -189,59 +189,19 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
189
189
  const char* mime = cerver_mime_from_path(full_path);
190
190
 
191
191
  /*
192
- * Use mmap for zero-copy serving instead of fopen+malloc+fread.
193
- * The mmap'd region is used directly as the response body.
194
- * We mark it as _body_owned=0 since munmap needs special handling,
195
- * but for simplicity we'll use read() for small files and mmap for large.
192
+ * Use sendfile for zero-copy filesystem static serving.
193
+ * The file is opened and its descriptor is stored in the response structure
194
+ * for streaming directly to the client socket in the writer.
196
195
  */
197
- if (file_size > 65536) {
198
- /* Large files: mmap for zero-copy */
199
- int fd = open(full_path, O_RDONLY);
200
- if (fd < 0) return -1;
201
-
202
- void* mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
203
- close(fd);
204
-
205
- if (mapped == MAP_FAILED) return -1;
206
-
207
- /* Advise the kernel we'll read sequentially */
208
- madvise(mapped, file_size, MADV_SEQUENTIAL);
209
-
210
- res->status = 200;
211
- res->content_type = mime;
212
- res->body = (const char*)mapped;
213
- res->body_len = file_size;
214
- res->_body_owned = 2; /* Special flag: needs munmap, not free */
215
- } else {
216
- /* Small files: read into buffer (avoids mmap overhead) */
217
- int fd = open(full_path, O_RDONLY);
218
- if (fd < 0) return -1;
219
-
220
- char* file_data = malloc(file_size);
221
- if (!file_data) {
222
- close(fd);
223
- return -1;
224
- }
225
-
226
- size_t total = 0;
227
- while (total < file_size) {
228
- ssize_t n = read(fd, file_data + total, file_size - total);
229
- if (n <= 0) break;
230
- total += (size_t)n;
231
- }
232
- close(fd);
233
-
234
- if (total != file_size) {
235
- free(file_data);
236
- return -1;
237
- }
238
-
239
- res->status = 200;
240
- res->content_type = mime;
241
- res->body = file_data;
242
- res->body_len = file_size;
243
- res->_body_owned = 1; /* malloc'd */
244
- }
196
+ int fd = open(full_path, O_RDONLY);
197
+ if (fd < 0) return -1;
198
+
199
+ res->status = 200;
200
+ res->content_type = mime;
201
+ res->body = NULL;
202
+ res->body_len = file_size;
203
+ res->_body_owned = 3; /* Special flag: sendfile, close fd */
204
+ res->_file_fd = fd;
245
205
 
246
206
  add_cache_headers(res, path);
247
207
 
@@ -224,6 +224,22 @@ static void test_route_match_mismatch_resets_params(void) {
224
224
  MU_ASSERT_EQ_INT(0, req.params_count);
225
225
  }
226
226
 
227
+ static void test_route_match_multi_segment(void) {
228
+ cerver_route_t route;
229
+ route.method = "GET";
230
+ route.pattern = "/users/:id/profile";
231
+ route.handler = handler_a;
232
+
233
+ cerver_request_t req;
234
+ memset(&req, 0, sizeof(req));
235
+ strcpy(req.method, "GET");
236
+ strcpy(req.path, "/users/123/profile");
237
+
238
+ MU_ASSERT(cerver_route_match(&route, &req) == 1);
239
+ MU_ASSERT_EQ_INT(1, req.params_count);
240
+ MU_ASSERT_STREQ("123", cerver_req_param(&req, "id"));
241
+ }
242
+
227
243
  static void test_request_header_helpers(void) {
228
244
  cerver_request_t req;
229
245
  memset(&req, 0, sizeof(req));
@@ -318,10 +334,27 @@ static void test_static_filesystem_small(void) {
318
334
 
319
335
  MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
320
336
  MU_ASSERT_EQ_SIZE(5, res.body_len);
321
- MU_ASSERT(memcmp(res.body, "small", 5) == 0);
322
- MU_ASSERT_EQ_INT(1, res._body_owned);
337
+ MU_ASSERT(res.body == NULL);
338
+ MU_ASSERT_EQ_INT(3, res._body_owned);
339
+ MU_ASSERT(res._file_fd >= 0);
340
+
341
+ int fds[2];
342
+ MU_ASSERT(pipe(fds) == 0);
343
+ MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
344
+ close(fds[1]);
323
345
 
324
- free((void*)res.body);
346
+ char out[1024];
347
+ ssize_t n = read_all(fds[0], out, sizeof(out));
348
+ MU_ASSERT(n > 0);
349
+ close(fds[0]);
350
+
351
+ MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
352
+ MU_ASSERT(strstr(out, "Content-Length: 5\r\n") != NULL);
353
+ MU_ASSERT(strstr(out, "\r\nsmall") != NULL);
354
+
355
+ if (res._body_owned == 3 && res._file_fd >= 0) {
356
+ close(res._file_fd);
357
+ }
325
358
  unlink(file_path);
326
359
  rmdir(dir);
327
360
  }
@@ -334,10 +367,10 @@ static void test_static_filesystem_large(void) {
334
367
  char file_path[PATH_MAX];
335
368
  snprintf(file_path, sizeof(file_path), "%s/large.bin", dir);
336
369
 
337
- char* payload = (char*)malloc(70000);
370
+ char* payload = (char*)malloc(32000);
338
371
  MU_ASSERT(payload != NULL);
339
- memset(payload, 'a', 70000);
340
- MU_ASSERT_EQ_INT(0, write_file(file_path, payload, 70000));
372
+ memset(payload, 'a', 32000);
373
+ MU_ASSERT_EQ_INT(0, write_file(file_path, payload, 32000));
341
374
  free(payload);
342
375
 
343
376
  cerver_server_t srv;
@@ -352,11 +385,27 @@ static void test_static_filesystem_large(void) {
352
385
  strcpy(req.path, "/large.bin");
353
386
 
354
387
  MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
355
- MU_ASSERT_EQ_SIZE(70000, res.body_len);
356
- MU_ASSERT_EQ_INT(2, res._body_owned);
357
- MU_ASSERT(((const char*)res.body)[0] == 'a');
388
+ MU_ASSERT_EQ_SIZE(32000, res.body_len);
389
+ MU_ASSERT(res.body == NULL);
390
+ MU_ASSERT_EQ_INT(3, res._body_owned);
391
+ MU_ASSERT(res._file_fd >= 0);
358
392
 
359
- munmap((void*)res.body, res.body_len);
393
+ int fds[2];
394
+ MU_ASSERT(pipe(fds) == 0);
395
+ MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
396
+ close(fds[1]);
397
+
398
+ char out[35000];
399
+ ssize_t n = read_all(fds[0], out, sizeof(out));
400
+ MU_ASSERT(n > 0);
401
+ close(fds[0]);
402
+
403
+ MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
404
+ MU_ASSERT(strstr(out, "Content-Length: 32000\r\n") != NULL);
405
+
406
+ if (res._body_owned == 3 && res._file_fd >= 0) {
407
+ close(res._file_fd);
408
+ }
360
409
  unlink(file_path);
361
410
  rmdir(dir);
362
411
  }
@@ -402,6 +451,7 @@ int main(void) {
402
451
  mu_run("write_response_force_close", test_write_response_force_close);
403
452
  mu_run("route_match_and_dispatch", test_route_match_and_dispatch);
404
453
  mu_run("route_match_mismatch_resets_params", test_route_match_mismatch_resets_params);
454
+ mu_run("route_match_multi_segment", test_route_match_multi_segment);
405
455
  mu_run("request_header_helpers", test_request_header_helpers);
406
456
  mu_run("static_embedded_prefers_br", test_static_embedded_prefers_br);
407
457
  mu_run("static_embedded_index_fallback", test_static_embedded_index_fallback);
package/test/run.js CHANGED
@@ -190,6 +190,17 @@ test("generateRouteTable emits forward declarations, entries, and count", () =>
190
190
  assert.match(code, /static const int cerver_route_count = 2;/);
191
191
  });
192
192
 
193
+ test("generateDispatch generates correct parameter extraction and termination", () => {
194
+ const { generateDispatch } = require("../lib/codegen/dispatch_gen");
195
+ const routes = [
196
+ IR.IRRoute("GET", "/users/:id/profile", ["id"], IR.IRHandler([], [])),
197
+ ];
198
+ const code = generateDispatch(routes);
199
+ assert.match(code, /req->params\[req->params_count\]\.key = "id";/);
200
+ assert.match(code, /req->params\[req->params_count\]\.value = seg1_start;/);
201
+ assert.match(code, /\(\(char\*\)seg1_start\)\[seg1_len\] = '\\0';/);
202
+ });
203
+
193
204
  test("loadConfig merges defaults and supports export default configs", () => {
194
205
  const dir = tempDir();
195
206
  try {