@velox0/cerver 0.3.1 → 0.4.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.
@@ -2,13 +2,12 @@ name: Publish package
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [main]
6
- release:
7
- types: [published]
5
+ tags:
6
+ - "v*"
8
7
  workflow_dispatch:
9
8
 
10
9
  permissions:
11
- contents: read
10
+ contents: write
12
11
  id-token: write
13
12
 
14
13
  jobs:
@@ -18,18 +17,18 @@ jobs:
18
17
 
19
18
  steps:
20
19
  - name: Checkout
21
- uses: actions/checkout@v4
20
+ uses: actions/checkout@v6
22
21
 
23
22
  - name: Setup pnpm
24
- uses: pnpm/action-setup@v4
23
+ uses: pnpm/action-setup@v6
25
24
  with:
26
25
  version: 10
27
26
  run_install: false
28
27
 
29
28
  - name: Setup Node.js
30
- uses: actions/setup-node@v4
29
+ uses: actions/setup-node@v6
31
30
  with:
32
- node-version: 20
31
+ node-version: 24
33
32
  registry-url: https://registry.npmjs.org
34
33
  cache: pnpm
35
34
 
@@ -179,7 +179,8 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
179
179
  ` { "${entry.servePath}", "${entry.mime}", ` +
180
180
  `${entry.name}, ${entry.name}_len, ` +
181
181
  `${entry.gzName}, ${entry.gzLen}, ` +
182
- `${entry.brName}, ${entry.brLen} },`
182
+ `${entry.brName}, ${entry.brLen}, ` +
183
+ `NULL, 0 },`
183
184
  );
184
185
  }
185
186
  lines.push("};");
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+
3
+ const { cString, handlerName } = require("./emit");
4
+
5
+ /**
6
+ * Generate a compile-time dispatch function that avoids the generic router.
7
+ *
8
+ * For static routes: method check → length check → memcmp (known lengths).
9
+ * For dynamic routes: segment-count check → per-segment inline comparisons.
10
+ *
11
+ * Falls through to generic router for unmatched paths.
12
+ *
13
+ * @param {IRRoute[]} routes - All IR routes
14
+ * @returns {string} - C source for cerver_generated_dispatch()
15
+ */
16
+ function generateDispatch(routes) {
17
+ const lines = [];
18
+
19
+ /* Separate static and dynamic routes */
20
+ const staticRoutes = routes.filter((r) => !r.urlPath.includes(":"));
21
+ const dynamicRoutes = routes.filter((r) => r.urlPath.includes(":"));
22
+
23
+ /* Forward declarations for handler functions */
24
+ lines.push("/* ---- Generated Fast Dispatch ---- */");
25
+ lines.push("");
26
+
27
+ /* Group static routes by method */
28
+ const byMethod = {};
29
+ for (const route of staticRoutes) {
30
+ if (!byMethod[route.method]) byMethod[route.method] = [];
31
+ byMethod[route.method].push(route);
32
+ }
33
+
34
+ /* Group dynamic routes by method */
35
+ const dynByMethod = {};
36
+ for (const route of dynamicRoutes) {
37
+ if (!dynByMethod[route.method]) dynByMethod[route.method] = [];
38
+ dynByMethod[route.method].push(route);
39
+ }
40
+
41
+ /* Generate the dispatch function */
42
+ lines.push(
43
+ "static cerver_handler_fn cerver_generated_dispatch(cerver_request_t *req) {"
44
+ );
45
+ lines.push(" const char *path = req->path;");
46
+ lines.push(" size_t path_len = 0;");
47
+ lines.push(" { const char *p = path; while (*p) { path_len++; p++; } }");
48
+ lines.push("");
49
+
50
+ const allMethods = new Set([
51
+ ...Object.keys(byMethod),
52
+ ...Object.keys(dynByMethod),
53
+ ]);
54
+
55
+ let firstMethod = true;
56
+ for (const method of allMethods) {
57
+ const methodStatic = byMethod[method] || [];
58
+ const methodDynamic = dynByMethod[method] || [];
59
+
60
+ /* Method check */
61
+ const methodLen = method.length;
62
+ lines.push(
63
+ ` ${firstMethod ? "" : "} else "}if (req->method[0] == '${method[0]}' && memcmp(req->method, ${cString(method)}, ${methodLen + 1}) == 0) {`
64
+ );
65
+ firstMethod = false;
66
+
67
+ /* ---- Static routes: group by path length for fast rejection ---- */
68
+ if (methodStatic.length > 0) {
69
+ /* Group by length */
70
+ const byLength = {};
71
+ for (const route of methodStatic) {
72
+ const len = route.urlPath.length;
73
+ if (!byLength[len]) byLength[len] = [];
74
+ byLength[len].push(route);
75
+ }
76
+
77
+ lines.push(" /* Static routes */");
78
+
79
+ const lengths = Object.keys(byLength)
80
+ .map(Number)
81
+ .sort((a, b) => a - b);
82
+
83
+ for (const len of lengths) {
84
+ const routesAtLen = byLength[len];
85
+
86
+ lines.push(` if (path_len == ${len}) {`);
87
+
88
+ for (const route of routesAtLen) {
89
+ const name = handlerName(route.method, route.urlPath);
90
+ if (route.urlPath === "/") {
91
+ lines.push(
92
+ ` if (path[0] == '/' && path[1] == '\\0') return ${name};`
93
+ );
94
+ } else {
95
+ lines.push(
96
+ ` if (memcmp(path, ${cString(route.urlPath)}, ${len}) == 0) return ${name};`
97
+ );
98
+ }
99
+ }
100
+
101
+ lines.push(" }");
102
+ }
103
+ }
104
+
105
+ /* ---- Dynamic routes: inline segment matching ---- */
106
+ if (methodDynamic.length > 0) {
107
+ lines.push("");
108
+ lines.push(" /* Dynamic routes */");
109
+
110
+ for (const route of methodDynamic) {
111
+ const segments = route.urlPath.split("/").filter(Boolean);
112
+ const name = handlerName(route.method, route.urlPath);
113
+
114
+ lines.push(" {");
115
+ lines.push(` /* ${route.urlPath} */`);
116
+ lines.push(` const char *p = path;`);
117
+ lines.push(` if (*p == '/') p++;`);
118
+ lines.push(` int match = 1;`);
119
+
120
+ for (let i = 0; i < segments.length; i++) {
121
+ const seg = segments[i];
122
+ const isLast = i === segments.length - 1;
123
+
124
+ if (seg.startsWith(":")) {
125
+ const paramName = seg.slice(1);
126
+ /* Dynamic segment: find the segment boundaries */
127
+ lines.push(` const char *seg${i}_start = p;`);
128
+ lines.push(` while (*p && *p != '/') p++;`);
129
+ lines.push(` size_t seg${i}_len = (size_t)(p - seg${i}_start);`);
130
+ lines.push(` if (seg${i}_len == 0) match = 0;`);
131
+
132
+ if (!isLast) {
133
+ lines.push(` if (match && *p == '/') p++; else if (!${isLast}) match = 0;`);
134
+ }
135
+ } else {
136
+ /* Static segment: compare directly */
137
+ const segLen = seg.length;
138
+ lines.push(
139
+ ` if (match && memcmp(p, ${cString(seg)}, ${segLen}) == 0 && (p[${segLen}] == '/' || p[${segLen}] == '\\0')) {`
140
+ );
141
+ lines.push(` p += ${segLen};`);
142
+ lines.push(` if (*p == '/') p++;`);
143
+ lines.push(` } else { match = 0; }`);
144
+ }
145
+ }
146
+
147
+ /* Ensure path is fully consumed */
148
+ lines.push(` if (match && *p != '\\0') match = 0;`);
149
+
150
+ /* Extract params on match */
151
+ lines.push(` if (match) {`);
152
+
153
+ /* Re-iterate to set params */
154
+ let paramIdx = 0;
155
+ for (let i = 0; i < segments.length; i++) {
156
+ const seg = segments[i];
157
+ if (seg.startsWith(":")) {
158
+ const paramName = seg.slice(1);
159
+ lines.push(
160
+ ` req->params[req->params_count].key = ${cString(paramName)};`
161
+ );
162
+ lines.push(
163
+ ` req->params[req->params_count].value = seg${i}_start;`
164
+ );
165
+ lines.push(` req->params_count++;`);
166
+ paramIdx++;
167
+ }
168
+ }
169
+
170
+ lines.push(` return ${name};`);
171
+ lines.push(` }`);
172
+ lines.push(` }`);
173
+ }
174
+ }
175
+ }
176
+
177
+ if (!firstMethod) {
178
+ lines.push(" }");
179
+ }
180
+
181
+ lines.push("");
182
+ lines.push(" return NULL; /* No match — fall through to generic router */");
183
+ lines.push("}");
184
+ lines.push("");
185
+
186
+ return lines.join("\n");
187
+ }
188
+
189
+ module.exports = { generateDispatch };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { handlerName, emitStatement } = require("./emit");
4
4
  const { generateRouteTable } = require("./route_table");
5
+ const { generateDispatch } = require("./dispatch_gen");
5
6
 
6
7
  /**
7
8
  * Generate the complete C server source file from IR routes.
@@ -70,6 +71,11 @@ function generateServer(routes, config, assetsCode) {
70
71
  lines.push("");
71
72
  }
72
73
 
74
+ /* ---- Generated fast dispatch ---- */
75
+ lines.push("/* ---- Generated Fast Dispatch (compile-time optimized) ---- */");
76
+ lines.push("");
77
+ lines.push(generateDispatch(routes));
78
+
73
79
  /* ---- Main function ---- */
74
80
  lines.push("/* ---- Entry Point ---- */");
75
81
  lines.push("");
@@ -81,7 +87,7 @@ function generateServer(routes, config, assetsCode) {
81
87
  lines.push("");
82
88
  lines.push(" /* Allow PORT env override */");
83
89
  lines.push(' const char *env_port = getenv("CERVER_PORT");');
84
- lines.push(" if (!env_port) env_port = getenv(\"PORT\");");
90
+ lines.push(' if (!env_port) env_port = getenv("PORT");');
85
91
  lines.push(" if (env_port) port = atoi(env_port);");
86
92
  lines.push("");
87
93
  lines.push(" cerver_server_t srv;");
@@ -92,6 +98,11 @@ function generateServer(routes, config, assetsCode) {
92
98
  );
93
99
  lines.push("");
94
100
 
101
+ /* Wire up generated dispatch */
102
+ lines.push(" /* Use generated compile-time dispatch for fast routing */");
103
+ lines.push(" cerver_set_dispatch(&srv, cerver_generated_dispatch);");
104
+ lines.push("");
105
+
95
106
  if (assetsCode) {
96
107
  lines.push(
97
108
  " cerver_set_assets(&srv, cerver_embedded_assets, cerver_embedded_asset_count);"
@@ -22,6 +22,20 @@ function detectCompiler() {
22
22
  );
23
23
  }
24
24
 
25
+ /**
26
+ * Check if a compiler supports a given flag.
27
+ */
28
+ function supportsFlag(cc, flag) {
29
+ try {
30
+ execSync(`echo 'int main(){}' | ${cc} ${flag} -x c -o /dev/null - 2>/dev/null`, {
31
+ stdio: "ignore",
32
+ });
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
25
39
  /**
26
40
  * Compile the generated C source into a binary.
27
41
  *
@@ -46,9 +60,9 @@ function compile(distDir, runtimeDir, opts) {
46
60
  .filter((f) => f.endsWith(".c"))
47
61
  .map((f) => path.join(runtimeDir, f));
48
62
 
49
- /* Build the compiler command */
63
+ /* Build the compiler command with aggressive optimization */
50
64
  const args = [
51
- "-O2",
65
+ "-O3",
52
66
  "-Wall",
53
67
  "-Wextra",
54
68
  "-Wno-unused-parameter",
@@ -60,10 +74,23 @@ function compile(distDir, runtimeDir, opts) {
60
74
  "-lpthread",
61
75
  ];
62
76
 
77
+ /* Add LTO if supported */
78
+ if (supportsFlag(cc, "-flto")) {
79
+ args.splice(1, 0, "-flto");
80
+ }
81
+
82
+ /* Add march=native if supported (for SIMD/hardware-specific opts) */
83
+ if (supportsFlag(cc, "-march=native")) {
84
+ args.push("-march=native");
85
+ }
86
+
63
87
  if (opts && opts.static) {
64
88
  args.push("-static");
65
89
  }
66
90
 
91
+ /* On macOS, we need to define _GNU_SOURCE for some functions */
92
+ args.push("-D_GNU_SOURCE");
93
+
67
94
  console.log(` compiling with ${cc}...`);
68
95
  console.log(` ${cc} ${args.join(" ")}`);
69
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries",
5
5
  "main": "lib/index.js",
6
6
  "bin": {