Parity with txiki.js and Elsa (2026-09-28)

txiki.js and Elsa are small JavaScript runtimes built on QuickJS. This report lists every API they offer and marks, for each one:

  • whether it belongs to Ecma TC55’s Minimum Common Web API (WinterTC, 2025 snapshot);
  • its status in Zinc and its Zinc equivalent.

Zinc compiles TypeScript to C++ and has no JS engine, so a few rows cannot apply.

The inventory was taken from the sources:

  • txiki.js: master @ 8f14257 (2026-09-14). types/src/*.d.ts, src/js/** and website/docs/**.
  • Elsa: master @ 1982c3d (2022-11-29). js/*.js, core/ops/*.go and cmd/. Elsa is dormant.

Status

  • exists: in Zinc before this work.
  • added: added by this work.
  • partial: available with documented gaps, whether it existed or was added.
  • missing: not in Zinc.
  • n/a: needs a JS engine.

The WinterTC column marks the rows of the minimum common API. Those rows came first. In the columns for txiki and Elsa, ✓ means supported, ~ partly, and – absent.

Summary

Rowsexistsaddedpartialmissingn/atotal
WinterTC minimum common API321129045
Other Web APIs (txiki / Elsa globals)0247013
tjs namespace and Elsa ops729913260
Standard library modules (tjs:*, Elsa std/)0347014
CLI5123314
All155631395146

A row is one line of the tables below. Some lines group several interfaces: the 13 stream classes, the 4 timer functions and the 3 global error hooks are one row each.

  • Before this work, 15 rows existed in Zinc.
  • Now, 102 do: 15 existed, 56 were added and 31 are partial. Some partial rows existed before (console, performance, zinc:storage, zinc:events, zinc:net serve).
  • Not covered: 39 rows are missing and 5 cannot apply.
  • Of the 45 WinterTC rows, 36 are covered (3 existed, 21 added, 12 partial). The 9 missing ones are:
    • streams;
    • compression streams;
    • the text streams;
    • URLPattern;
    • the WebAssembly Table, Tag / Exception and streaming functions;
    • globalThis / self;
    • the global error hooks.

The Zinc side is covered by conformance programs in tests/conformance/: web, fetch_web, fetch, socket, sys_process, fs_ext, os_info, path, sqlite, ffi and wasm. Each one prints the same bytes on the sim and natively (macos; ps1 and esp32 profiles where the capabilities allow). The Web API rows are also checked against Node 24 and against WPT data; see the Web APIs chapter.

1. WinterTC minimum common API

APItxikiElsaWinterTCZincZinc equivalent / notes
AbortController, AbortSignal (abort, timeout, any, reason, throwIfAborted)✓–✓addedglobal (lib/std/web.ts)
Event✓–✓addedglobal; options, phases, preventDefault / passive, composedPath
EventTarget✓–✓addedglobal; once / passive / signal / capture, exceptions to reportError
CustomEvent✓–✓addedglobal, CustomEvent<T> with { detail }
ErrorEvent✓–✓addedglobal
MessageChannel, MessagePort✓–✓partialglobal; same thread, no transfer list, no structured clone of the data
MessageEvent✓–✓addedglobal (data: unknown)
PromiseRejectionEvent✓–✓partialthe class exists, but the runtime does not fire unhandledrejection (it reports and exits)
DOMException✓–✓addedglobal, names, legacy code and constants
Headers✓–✓addedglobal (lib/std/fetch.ts): validation, sort-and-combine, getSetCookie
Request✓–✓addedglobal; method normalization, body rules, clone, body transfer
Response✓–✓partialglobal; error / redirect / json; no body stream
FormData✓–✓partialglobal; multipart serialize, urlencoded parse (no multipart parse)
Blob✓–✓partialglobal; text / bytes / arrayBuffer / slice; no stream()
File✓–✓addedglobal
CompressionStream, DecompressionStream✓–✓missingneeds streams + zlib (≈2 d: miniz as a plugin)
ReadableStream, WritableStream, TransformStream + 10 controller / reader / strategy classes✓ (polyfill)–✓missing≈1–2 weeks: a Zinc port of the reference implementation, then Response.body, Blob.stream, text streams
TextEncoder✓✓✓addedglobal; encode, encodeInto
TextDecoder✓✓✓partialglobal; UTF-8 only (all its labels), fatal, ignoreBOM, stream; other encodings missing (≈1 d for UTF-16 + windows-1252)
TextEncoderStream, TextDecoderStream✓–✓missingafter streams (≈0.5 d)
URL✓–✓addedglobal; WHATWG parser, WPT urltestdata 896/896, setters 278/278
URLSearchParams✓–✓addedglobal; keys() / values() / entries() return arrays
URLPattern✓–✓missing≈3 d (needs a small regex engine: Zinc has no regex)
Crypto / crypto✓–✓addedglobal; getRandomValues (OS entropy), randomUUID
SubtleCrypto✓ (all)–✓partialdigest SHA-1 / 256 / 384 / 512 in Zinc; no keys, sign, encrypt (≈1 week with a vetted C library, e.g. Monocypher + BearSSL)
CryptoKey✓–✓partialthe class only
Performance / performance✓–✓partialnow() existed; timeOrigin, mark, measure missing (≈0.5 d)
WebAssembly.Module, Instance✓ (WAMR)–✓addedzinc:wasm on wasm3 (plugin), WebAssembly global
WebAssembly.Memory✓–✓partialexported memory read / write / byteLength; no grow, no imported memory
WebAssembly.Global✓–✓partialread exported globals only
WebAssembly.Table✓–✓missing≈1 d over wasm3 internals
WebAssembly.Tag, Exception, JSTag~–✓missingwasm3 has no exception handling
WebAssembly.CompileError, LinkError, RuntimeError✓–✓addedfrom zinc:wasm
WebAssembly.compile, instantiate, validate✓–✓addedWebAssembly global
WebAssembly.compileStreaming, instantiateStreaming✓–✓missingafter streams; instantiate(await res.bytes()) works
atob, btoa✓~✓addedglobal, forgiving-base64
setTimeout / clearTimeout / setInterval / clearInterval✓–✓existsdeterministic virtual clock in tests
queueMicrotask✓–✓exists
console✓~✓existslog / info / debug / warn / error / trace / time / count / assert / table; group, dir, countReset missing
structuredClone✓–✓partialglobal; plain data through JSON
reportError––✓addedglobal; prints Uncaught … and goes on
navigator.userAgent✓–✓addedglobal navigator
fetch()✓~ (GET text)✓addedglobal; http(s) through libcurl / esp_http_client, data:, abort, timeout
globalThis, self✓✓ (window)✓missingZinc rejects globalThis (Z1015): no dynamic global object
onerror, onunhandledrejection, onrejectionhandled✓–✓missingthe runtime reports uncaught errors and exits (crash policy); a hook is ≈1 d

2. Other Web APIs (globals in txiki or Elsa, outside the WinterTC list)

APItxikiElsaZincZinc equivalent / notes
WebSocket client✓–addedzinc:socket WebSocket (not a global; ws:// only, no TLS)
WebSocket server (tjs.serve websocket)✓–addedzinc:socket serveWebSocket
WebSocketStream✓–missingafter streams
EventSource✓–missing≈0.5 d over fetch once bodies stream
XMLHttpRequest✓–missinglegacy; fetch covers it
Direct Sockets TCPSocket, TCPServerSocket, UDPSocket, PipeSocket✓–partialzinc:socket (callback API rather than streams)
TLSSocket, TLSServerSocket✓–missingneeds a TLS library (≈3 d with mbedTLS)
Worker, BroadcastChannel✓–missingthe runtime has threads natively; a message-passing Worker ≈1 week
localStorage✓–partialzinc:storage (get / set / remove / keys, persisted; NVS on esp32)
sessionStorage✓–missinga Map does it
FileReader✓–missinglegacy; Blob.text() / bytes()
encodeURIComponent / decodeURIComponent / encodeURI / decodeURI✓✓partialglobals from zinc:web (ECMAScript, missing from Zinc’s lib before)
EventEmitter (Elsa)–✓partialzinc:events Emitter<T> (typed, one channel) and EventTarget

3. tjs namespace and Elsa ops

APItxikiElsaZincZinc equivalent / notes
tjs.args✓✓ (Elsa.args)existssys.args()
tjs.exePath✓–missing≈1 h (_NSGetExecutablePath / /proc/self/exe)
tjs.version, engine.versions✓–missing≈1 h
tjs.pid✓–addedsys.pid()
tjs.ppid✓–missing≈15 min
tjs.exit✓–existssys.exit(code)
tjs.cwd, tjs.chdir✓✓ (Elsa.cwd)addedsys.cwd(), sys.chdir()
tjs.env (get / set / delete / keys)✓✓ (Elsa.env)addedsys.env, setEnv, unsetEnv, envKeys
tjs.hostName✓–addedos.hostname()
tjs.homeDir, tjs.tmpDir✓–addedos.homedir(), os.tmpdir() / fs.tmpdir()
tjs.kill(pid, sig)✓–addedsys.kill(pid, 'SIGTERM')
tjs.addSignalListener✓–addedsys.onSignal('SIGTERM', cb) on the event loop
tjs.removeSignalListener✓–missing≈15 min
tjs.spawn with stdin / stdout / stderr pipes✓–existszinc:process spawn / run (plugin)
spawn stdio inherit / ignore, uid / gid✓–partialalways piped
Process.wait() / exit status / term signal, kill✓–existsp.exited, p.code (128 + signal), p.kill()
tjs.exec (execvp)✓–missing≈30 min
tjs.system.cpus✓–addedos.cpus(), os.availableParallelism()
tjs.system.loadAvg✓–addedos.loadavg()
tjs.system.uptime✓–addedos.uptime()
tjs.system.networkInterfaces✓–addedos.networkInterfaces()
tjs.system.userInfo✓–addedos.userInfo()
memory totals (not in txiki)––addedos.totalmem(), os.freemem(), plus arch, type, release
tjs.stdin✓–partialsys.onStdin(cb) (chunks); no stream, no raw mode
tjs.stdout, tjs.stderr✓–addedsys.write, sys.writeErr (and console)
isTerminal✓–addedsys.isatty(fd)
setRawMode, terminal width / height✓–missing≈0.5 d (termios, TIOCGWINSZ)
tjs.open → FileHandle (read / write at offsets, truncate, sync, stat)✓–missingwhole-file functions only; ≈1 d
tjs.readFile✓✓existsfs.readText / fs.readBytes (added)
tjs.writeFile✓✓existsfs.writeText / fs.writeBytes (added), appendText
tjs.stat, tjs.lstat✓✓ (Elsa.stat)addedfs.stat, fs.lstat
tjs.statFs✓–missing≈30 min
tjs.readDir✓–addedfs.readDir (with types), fs.list
Elsa.walk–✓partialrecurse over fs.readDir
tjs.makeDir (recursive)✓✓addedfs.mkdir(path, true)
tjs.makeTempDir✓–addedfs.mkdtemp(prefix)
tjs.makeTempFile✓–missing≈30 min
tjs.remove (recursive)✓✓addedfs.remove(path, true)
tjs.rename, tjs.copyFile✓–addedfs.rename, fs.copyFile
tjs.realPath, tjs.readLink, tjs.symlink✓–addedfs.realpath, fs.readlink, fs.symlink
tjs.link✓–missing≈15 min
tjs.chmod✓–addedfs.chmod
tjs.chown, lchown, utime, lutime✓–missing≈1 h
tjs.watch✓–addedfs.watch / unwatch (polling every 100 ms; FSEvents / inotify ≈1 d)
Elsa.exists–✓existsfs.exists
tjs.lookup (DNS)✓–addedzinc:socket lookup(host)
tjs.connect('tcp' | 'pipe')✓–addedzinc:socket connect, connectUnix
tjs.connect('udp') / listen('udp')✓–addedzinc:socket udp()
tjs.listen('tcp' | 'pipe')✓–addedzinc:socket listen, listenUnix
tjs.connect('tls') / listen('tls')✓–missingneeds a TLS library (see §2)
tjs.serve (fetch-handler HTTP server)✓✓ (Elsa.serve)partialzinc:net serve(port, handler): Request / Reply records with headers (added); not Request → Response objects, no HTTP/2 or TLS
Elsa.runPlugin (Go .so)–✓partialnative plugins are compiled in (plugins/); runtime loading through zinc:ffi
tjs.engine.compile / serialize / evalBytecode / gc✓–n/ano JS engine (AOT C++)
tjs.setImportMap✓–n/amodules are resolved at compile time
tjs.createConsole✓–missing≈0.5 d
tjs.Error (errno errors)✓–partialmessages start with the errno code (ENOENT: stat path)
permissions (--fs, --net, --env)–✓partialcompile-time capabilities (requires, Z5003 / Z5005) instead of run-time flags
Elsa.mode / tests registry–✓addedzinc test <dir> (below)
HTTP/2 / HTTP/3 in fetch✓–partiallibcurl negotiates HTTP/2 over TLS where built with nghttp2
cookie jar, proxy variables✓–partiallibcurl honours *_proxy; no cookie jar

4. Standard library modules

ModuletxikiElsaZincZinc equivalent / notes
tjs:path✓–addedzinc:path (POSIX; the same results as Node’s path.posix); no win32 flavour
tjs:sqlite✓–addedzinc:sqlite plugin, bundled amalgamation 3.53.4
tjs:ffi✓–partialzinc:ffi plugin: scalar and string signatures, memory helpers; no structs, callbacks or variadics (≈3 d with libffi)
tjs:assert✓–addedzinc:assert
tjs:hashing (md5 … sha3)✓✓ (std/hash)partialSHA-1 / 256 / 384 / 512 in zinc:web; md5, sha224, sha512/t, sha3 and HMAC missing (≈1 d)
tjs:uuid (v1–v7)✓✓ (std/uuid)partialv4 via crypto.randomUUID(); v7 ≈1 h
tjs:getopts✓–missing≈0.5 d
tjs:ipaddr✓–missingthe URL host parser covers IPv4 / IPv6 parsing; CIDR helpers ≈1 d
tjs:posix-socket✓–missingraw sockets, ≈1 d over the socket plugin
tjs:readline✓–missingline editing, history, colours: ≈2 d (needs raw mode)
tjs:utils (inspect, format)✓–partialconsole formats like Node’s inspect, but it is not exposed as a function (≈1 h)
tjs:wasi✓–missingwasm3’s WASI layer as an option of the wasm plugin (≈1 d)
Elsa std/encoding/hex, std/bytes–✓missingtoHex exists in zinc:web; the rest ≈0.5 d
Elsa std/fmt/colors–✓missing≈1 h

5. CLI

CommandtxikiElsaZincZinc equivalent / notes
run a program✓ tjs run✓ elsa runexistszinc run (native or --target sim)
type-check–✓ elsa devexistszinc check (TypeScript + Zinc rules)
test runner✓ tjs test✓ elsa testaddedzinc test <dir>: every test-*.ts / *.test.ts, pass = exit 0, with zinc:assert
bundle✓ tjs bundle✓ elsa bundlen/awhole-program compilation replaces bundling
standalone executable✓ tjs compile✓ elsa pkgexistsevery zinc build is a native executable; zinc export packages it with assets
app packages✓ tjs app–existszinc export / zinc deploy
eval of an expression✓–n/ano engine; zinc run of a file
REPL✓✓missinga compile-per-line REPL on the sim target is possible (≈3 d); a native REPL needs an interpreter (see docs/reports VM design)
tjs serve FILE✓–missing≈0.5 d (a server template calling zinc:net serve)
elsa install (shims in ~/.elsa)–✓missing≈1 h
dev mode–✓ elsa devexistszinc dev (hot reload, red box, inspector)
TypeScript~ (stripped by esbuild)~ (esbuild)partialZinc is typed TypeScript, a strict subset: no eval, regex or reflection (see chapter 2)
import maps / URL imports✓✓n/acompile-time module resolution only
--memory-limit, --stack-size✓–partialthe heap is a fixed budget per profile (zinc.json targets.<id>.heap)

What remains, with estimates

Ordered by value for server-style programs:

  1. Streams: ReadableStream / WritableStream / TransformStream, then streaming Response.body / Request.body, Blob.stream(), TextEncoderStream / TextDecoderStream and WebSocketStream. About 1–2 weeks. This is the biggest WinterTC gap.
  2. TLS for sockets and wss://, through mbedTLS or the OS (SecureTransport is deprecated). About 3 days, plus certificates on embedded targets.
  3. CompressionStream / DecompressionStream with miniz (MIT). About 2 days after streams.
  4. Web Crypto keys: HMAC, AES-GCM, ECDSA / Ed25519, PBKDF2 / HKDF with a vetted library. About 1 week. HMAC and PBKDF2 alone take about 1 day in Zinc.
  5. Global error hooks: onerror / unhandledrejection events before the crash policy. About 1 day.
  6. FileHandle (positional I/O), statFs, makeTempFile, link, chown, utime. About 1.5 days.
  7. Terminal: raw mode, window size, tjs:readline. About 2.5 days.
  8. Workers with message passing on the existing native threads. About 1 week.
  9. FFI through libffi: structs, callbacks, variadics. About 3 days.
  10. WebAssembly: Table, memory.grow, WASI. About 2 days.
  11. URLPattern, with a small regex engine. About 3 days.
  12. Small items: exePath, version, ppid, exec, removeSignalListener, uuid v7, getopts, colours, serve CLI, console.group / dir. About 2 days in all.

Known compiler limits met on the way

Worked around in the new modules, not fixed:

  • A generic method called with a lambda does not get its template argument deduced in C++ (db.transaction<T>).
  • A throwing call inside an array spread or an object literal, in an async function, emits a return; inside a value-returning lambda.
  • await inside call arguments is evaluated before the earlier arguments.
  • Zinc classes cannot be iterable ([Symbol.iterator]), so for…of needs entries() arrays.
  • Rest parameters are refused (Z9009), so path.join takes up to six segments.
  • Printing an absent string (null / undefined) natively prints an empty string.

Fixed in the compiler and runtime by this work:

  • ?? on nullable strings;
  • a computed '' being mistaken for null;
  • integer literals in bitwise operators on the f32 / fx12 profiles;
  • JSON.stringify calling toJSON;
  • f64 literals and parameter defaults in the sim under fixed point;
  • constructor unknown[] contexts;
  • library any calls in the strict profile;
  • the event loop exiting while a callback had just started work.

Edit this page on GitHub ↗