Repository navigation
v1.1.9
@morojs/engine 1.1.9
A routing release with two new native functions. Nothing on the wire
changes, nothing in the API is removed or renamed, and the one new
capability is additive and feature-detected through probe().capabilities.
A 1.8.x MoroJS keeps working on this binary; a MoroJS that calls the new
functions checks the flag first.
A TLS release with one new native function. Nothing on the wire changes,
nothing in the API is removed or renamed, and the one new capability is
additive and feature-detected through probe().capabilities. A 1.8.x
MoroJS keeps working on this binary; a MoroJS that calls the new function
checks the flag first and reports a clear error on 1.1.7.
New — certificate rotation without a restart
updateSsl(serverId, ssl) replaces the certificate and key of a running TLS
server for every handshake that starts afterwards
(capabilities.tlsReload). Until now the material was read once in
serve(), so an ACME renewal or a rotated secret meant a listener restart;
in-process TLS was the only place in the engine where that was still true.
sslis the fullserve()ssl shape, not a diff: file paths or inline
PEM,passphrase,minVersion,requestCert/rejectUnauthorized,
ciphers/ciphersuites/ecdhCurve,ticketKeys. Inline wins over a
file path for the same slot, as inserve().- Validated before the swap. The new context is built and checked
(readable files, parsable PEM, key matching certificate, protocol floor)
while the current one keeps serving; any failure throws with the same
messagesserve()uses and leaves the live context untouched. - Established connections are unaffected. A
TlsSessionholds its own
reference to theSSL_CTXit handshaked with, so a kept-alive socket or
an in-flight request finishes on the old context while the next
handshake gets the new one. The old context is freed when its last
session closes. - Session-ticket keys carry over when the update omits them, so tickets
minted before a rotation still resume afterwards; an update that names
new keys ends resumption of the old tickets. The ALPN policy carries
over the same way. - Throws for a server that was not started with
ssl(TLS cannot be
turned on after the fact), for an unknownserverId, and for a missing
options object. - The swap runs on the server's loop thread, the only thread that
handshakes, so there is no lock and no window in which a handshake sees
a half-built context. The per-connection path is unchanged: sessions
still derive from the live context pointer; the rotation itself costs one
context build.
Internally serve() and updateSsl() share one option parser and one
context builder (parseSslOptions / buildTlsContext in src/binding.cpp),
so the two cannot drift; Server::tls() and the TlsContext accessors for
ticket keys and ALPN exist for the carry-over.
Test: test/tls-reload.test.mjs — a fresh handshake is shown the new
certificate after an update in either shape; a connection opened before the
update keeps answering on its old session while new ones see the new
certificate; five kinds of bad material each throw and the old certificate
keeps serving; a plaintext server and an unknown id are refused; tickets
resume across a rotation that keeps the keys and stop resuming after one
that changes them.
New — parameter routes answered inside the engine
setParamRoute(serverId, method, prefix, suffix, status?, headers?)
registers a route with one variable path segment, and the engine answers a
matching request with that segment as the body, without calling into JS
(capabilities.paramRoutes). setParamRoute(sid, GET, '/user/', '')
answers GET /user/42 with 42; ('/files/', '.json') answers
GET /files/report.json with report.
Static routes (1.1.5) already kept a fixed reply out of JS. A route whose
reply is the URL - an id echo, a name lookup key - could not use them, and
the per-request JS call was the remaining cost on the benchmark shape every
harness runs (GET /user/:id). It is gone: with a static route for /,
one for POST /user and a parameter route for /user/, a server built on
this engine never enters JS for that route table.
- A request matches when its path is
prefix+ one non-empty segment with
no/in it +suffix, on the given method. Everything else - an empty
segment, a second slash, another method, a different suffix - still
reachesonRequestexactly as before. - The segment goes out as it is on the wire, undecoded (
a%20bstays
a%20b), which is what uWS'swriteParameterValuedoes too. A parameter
that needs decoding stays a JS route. - The reply is byte-identical to
respond(reqId, status, headers, segment):
same status line,Content-Lengthcomputed from the segment,Date,
keep-alive handling, and the optional headers given at registration. - Static routes are checked first, then parameter routes, in registration
order. Registering the same(method, prefix, suffix)again replaces the
earlier reply;clearParamRoutes(serverId)hands every parameter route
back to JS. Both are per server, like the static-route calls. methodis the same indexonRequestreports (GET= 0,POST= 1,
...);statusdefaults to 200;headersis the flat
[name, value, ...]arraysetStaticRoutetakes, ornull.- Cost when unused: none. The dispatch sites test one
hasEngineRoutes()
(static or parameter routes present) before looking at either table, so
a server that registers neither runs the same path as 1.1.8.
Measured on the arm64 Linux VM used for the harness work (2 server cores,
zrk --closed, 6 generator threads, same engine build both sides, the raw
javascript/morojs-engine entry), GET /user/0 went from a JS callback
per request to a parameter route:
| connections | JS handler (req/s) | parameter route (req/s) |
|---|---|---|
| 64 | 309–320k | 348–384k |
| 512 | 395–414k | 435–441k |
GET / (a static route both sides) was unchanged, as expected.
Internally Server::tryStaticRoute() falls through to
tryParamRoute(), which matches against paramRoutes_ and calls the same
respond() the static path uses, with the segment pointing into the
request's own path buffer; nothing is copied before the write.
SetParamRoute in src/binding.cpp shares buildHeaders with
SetStaticRoute, so the two header shapes cannot drift.
Test: test/param-routes.test.mjs — the capability is advertised; a
parameter route echoes the segment with zero onRequest calls, with and
without a suffix and with custom headers; the segment is verbatim
(a%20b); five non-matching shapes each reach JS with the expected method
and path; the reply is byte-identical to respond() with the same segment;
re-registration replaces and clearParamRoutes hands the route back to JS.
Compatibility
- No wire changes. No removed or renamed exports (
tools/check-exports.mjs:
no drift — 31 native functions, 15 capability flags). - New:
updateSsl(serverId, ssl),capabilities.tlsReload. - New:
setParamRoute(serverId, method, prefix, suffix, status?, headers?),
clearParamRoutes(serverId),capabilities.paramRoutes. - Declared in all four places (
Initialize,NATIVE_API,index.mjs,
index.d.ts) and documented indocs/API.md(parameter routes section,
after static routes). - Verified on macOS (libuv) and Linux (io_uring defer-taskrun and libuv):
218 suite tests (214 from 1.1.8 plus the four above), the C++ unit tests
and the smoke test. - MoroJS uses it from the release after 1.8.12:
param('id')in place of a
handler on a bare single-parameter route registers a parameter route when
capabilities.paramRoutesis true and runs the equivalent
res.end(req.params.id)handler otherwise.