lite – The Node Dev Runtime (execute() is really synchronous)
execute() is synchronous by design — that is the whole point, since one
codebase runs under both Classic ASP and Node. ASP is genuinely synchronous
because ADODB blocks the OS thread. Node had to be made to match.
What changed in 6.9.0
Before 6.9.0, the Node path used deasync.loopWhile(), which re-enters and
pumps the Node event loop until the query resolves. So while it "blocked",
other callbacks fired and exceptions propagated through a loop that shouldn't
have been running. The symptom was a hard crash:
FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal
— the dev API process dying outright, typically under a cold API plus concurrent traffic plus an erroring request. Every crash of that class was "a callback fired during the block."
From 6.9.0, deasync is gone. execute() now:
- posts the SQL to a worker thread that owns its own mssql pool;
- calls
Atomics.wait()on aSharedArrayBuffer— a real OS-level block, during which the event loop does not run and no callback can fire; - wakes on the worker's
Atomics.notify(); - pulls the result via
receiveMessageOnPort(), which drains a MessagePort without the event loop (you cannot usepostMessage/.on('message')for the return path — the main thread is blocked).
A real block admits no callbacks, so the crash class disappears. Route code
(validate / secure / preprocess) stays synchronous and untouched, and the
ASP/ADODB path is unchanged, so production is unaffected either way.
What this means for you day to day
- The dev API is strictly serial. Requests queue behind each other. Measured
cost is roughly ~115ms of excess latency per queued request: a single
/app≈ 100ms, ten in parallel ≈ 2150ms. - Cypress and other e2e suites need longer timeouts. Typical working values:
defaultCommandTimeout15s,requestTimeout15s,responseTimeout45s. Raise the timeouts — do not re-architect around the serialisation. LITE_EXECUTE_TIMEOUT_MS(default120000) is the finite wait, and the only recoverable exit from a wedged worker.- Connection count roughly doubled: worker pool max 10 + main pool max 10, so up to ~20 per process where it used to be ~10.
closePool()also terminates the worker.
Things not to retry
- Removing goldfish's request queue on the theory it's redundant now. It was tried; it made things worse (2868ms vs 2150ms). The cost is inherent to the blocking, not the scheduling.
Measurement trap
git-bash on Windows spawns processes in ~725ms, so a
for … do curl … done loop measures the shell, not the API — easily good
for a phantom 20× regression. Use a single curl --parallel process, or curl's
own time_total.
Deploying a lite change into a project
lite is Rollup-inlined by goldfish. Copying dist into a project's
node_modules/@kyd/lite does nothing on its own — the project must be
rebuilt afterwards for the change to reach the bundle.
Related: a package's version label can lie if node_modules was overwritten in
place during a spike (manifest says one version, code is another). Check that
before trusting a .node-build diff.