The Runtime API loop
The contract
Identical to the Jolt sibling's – see lambda-mvp-jlt's runtime-api-loop.md for the full contract table. The only thing that changes in this port is the transport.
http.jank: no HTTP client exists in jank's ecosystem
Jolt has jolt-lang/http-client. jank has nothing equivalent anywhere in its ecosystem, so src/net/b12n/lambda_mvp/http.jank is a hand-rolled HTTP/1.1-over-raw-socket client, built directly on BSD sockets via jank's cpp/ C++ interop.
This is tractable specifically because the Runtime API is plain HTTP on a loopback address with no TLS – a general-purpose HTTP/HTTPS client would be a much bigger undertaking.
Six interop gotchas shaped the implementation (see the code comments in http.jank for exactly where each applies):
htonsis a preprocessor macro on Darwin (likely glibc too), not a linkable symbol -- wrapped in a smallcpp/rawC helper.cpp/newdoes not zero-initialize a struct --sockaddr_in's platform-specific padding is explicitlymemsetbefore use.(cpp/new cpp/sockaddr_in)already returns a pointer – taking its address again produces a corrupting double pointer that compiles cleanly and fails at runtime.cpp/unsafe-castrefuses asockaddr_in*->sockaddr*cast outright -- done inside acpp/rawC helper instead, where ordinary C++ casting rules apply.send's second parameter isconst void*, and jank has no object->void* conversion path -- casting the jank string to(:* cpp/char)first routes through a conversion path that already works, and C++ implicitly widenschar*toconst void*from there.- A
defnthat shadows aclojure.coresymbol (here,get) compiles clean with only a warning, but crashes the COMPILED BINARY at load time with a hard runtime error --(:refer-clojure :exclude [get])in the ns form is required, not stylistic.
One TCP connection per call
http.jank connects, sends, receives, and closes for every single get/post call – it does not keep a persistent connection open across the Runtime API's poll/execute/respond cycle. This is simpler and loopback connects are cheap, but it's a real, deliberate simplification: the real AWS Runtime API endpoint's behavior under this pattern (versus a client that reuses one keep-alive connection) was verified only against the offline mock server and a generic local test server during this port's development, not against AWS itself until the real deploy/invoke smoke test.
Startup failures are silent: no try/catch, no init-error, exit 0
main.jank's -main calls runtime/run with no try/catch around it, unlike the Jolt sibling's main.clj. That's not an oversight: a trivial try/throw/catch program with zero interop content took 15+ minutes to jank compile and never finished, confirmed directly rather than assumed (spec Open Risk #9). main.jank can't afford that cost, so the try/catch stays out until a future jank release fixes the underlying compile-time issue.
The consequence is worth knowing before you deploy this. If runtime/run throws during startup -- a malformed AWS_LAMBDA_RUNTIME_API, or the initial connection to the Runtime API sidecar failing – the exception propagates all the way up, uncaught. jank prints its own error report to stderr (message, data, and a stack trace), but the process then exits 0 anyway, regardless of the throw. This was verified directly against a trivial uncaught-throw probe, not assumed.
Two things follow from that exit code. First, runtime/post-init-error never runs, because there's no catch point left to call it from (the function stays defined and unused in runtime.jank, ready to wire back in once the compile-time cost is fixed). So the Lambda Runtime API never sees an init-error report for this failure. Second, nothing in the process's own exit status signals that anything went wrong, so a container orchestrator or Lambda's own supervisor watching this process gets no signal from it either.
If a deployment seems to hang or never responds, don't trust a clean exit code or the absence of an init-error report to mean startup succeeded – neither one tells you that here. Check CloudWatch Logs for the function instead. jank's own printed error report, with the message, data, and stack trace, is the only place a startup throw actually shows up.
A handler throw crashes the whole process too, not just that invocation
The startup case above is about main.jank calling runtime/run for the first time. The same missing try/catch also wraps every SUBSEQUENT call to handler inside run's own loop -- there's no catch point around (handler body ctx) either, for the identical reason (spec Open Risk #9: a trivial try/catch took 15+ minutes to jank compile and never finished).
The Jolt sibling wraps each invocation's handler call individually, so a single bad event reports a per-invocation HandlerError to the Runtime API and the loop continues to the next event. This port can't do that yet: if handler throws for any reason on ANY invocation, the exception propagates uncaught, the process exits (with the same exit-0 behavior described above), and the whole Lambda execution environment goes down – not just the one invocation that triggered it. The next invocation gets a fresh cold start instead of a graceful per-event error response.
For this project's own demo handler (string-building and an atom swap!, no I/O, no parsing of untrusted structure) that path is unreachable in practice. It becomes a real concern the moment a handler you write can throw – parsing the event JSON, calling out to another service, anything I/O-bound. Until a future jank release resolves the try/catch compile-time cost, keep handlers defensive (validate input before it can throw, or return an error payload rather than throwing) rather than relying on the runtime loop to catch a mistake for you.
Testing without AWS: the mock Runtime API
tools/mock_runtime_api.py is reused verbatim from the Jolt sibling -- canned events, real header set, PASS/FAIL assertion, 410 Gone to end the loop. bb probe wires it to the compiled binary.