txiki.js 26.3.0 released, a new dawn!

txiki.js is a small and powerful JavaScript runtime. It targets the latest ECMAScript spec and implements many web platform features.

After a long while (I’m counting since release 24) txiki.js 26.3.0 has been released, and it’s the biggest release since its inception, so let’s unpack the release.

Replace wasm3 with WAMR

wasm3 was a great match for txiki.js: it was “just a bunch of C files”, easy to embed, and it just worked. Alas its creator suffered acute personal losses due to the war in Ukraine and the project development stopped.

Development never picked up, and since issues were not being solved, a contributor suggested migrating to WAMR and created a draft PR. It took a while, but it ultimately took it over and pushed it over the finish line! txiki.js now has a well supported and more featureful WASM support.

I also took the chance to modernize the WASI interface and add a couple of extra features there.

Native Windows build with MSVC

Something I wanted to do for a while was to make a better Windows build. Using MSVC, and fully static. Thanks to the move to WAMR and away from curl, making all of that happen became attainable. vcpkg helped, and the release binary is now a statically linked 5-6MB exe!

HTTP client improvements

txiki.js started out with a simple fetch polyfill, layered on top of XHR. This polyfill had some shortcomings, and I ended up vendoring it so I could make some changes to better adapt it to our internals, and modernize it, since it didn’t support streaming interfaces.

Roughly at that point I realized that rather than using XHR under the hood, it would probably be better to have a somewhat low level HTTP client which both XHR and fetch use internally. This also unlocked a big milestone for fetch in txiki.js: streaming support.

In addition, both fetch and XHR now support automatic decompression!

Web streams all the things!

Alright, let’s get with the big ones. Sockets, stdio and child process streams APIs have historically been somewhat bespoke across different runtimes (Node, Deno and Bun) but ever since the introduction of Web Streams, there seems to be convergence towards using them as the main API surface for reading and writing.

I went ahead and changed all custom APIs in txiki.js to be “Web Streams First”. There is no custom read / write API anymore, it’s Web Streams all the way down!

This suddenly makes everything more composable, and likely future proof.

Direct Sockets

Another big one. When I was browsing through WinterTC specs I noticed an interesting one, the sockets API. As I was browsing through the open issues I noticed there was a mention of Direct Sockets, a proposal Google is championing, that started out before the WinterTC one, but remained dormant for a while, but it picked up recently.

After looking at both I decided to go with Direct Sockets for now, since it’s the more complete one. I extrapolated pipe support and we are off to the races.

I’ll keep watching the space and adapt as it evolves, but the core is very similar.

Replaced curl with libwebsockets

I’ll write an in-depth article only for this one, but essentially I made the call to replace curl with libwebsockets because it gives us access to HTTP and WebSocket clients and servers with a single library.

Since we need some TLS support, I ended up using MbedTLS, since OpenSSL is way too big. This also allows me to start implementing the Subtle Crypto API on top of it going forward.

HTTP and WebSocket server

Oh boy was this a long time in the making. I had wanted this for such a long time, but needed to have all the ducks in a row. With the introduction of libwebsockets we were almost there already, so I went for it.

txiki.js now as a simple API for HTTP / WS servers, inspired by Deno, Bun and Cloudflare Workers. It’s compatible with Hono with a simple adapter too! Here is a sample:


// Run with: tjs serve examples/http-server.js
//

export default {
    fetch(request) {
        const url = new URL(request.url);

        return new Response(`Hello World!\nYou requested: ${url.pathname}\n`);
    },
};

Here is a en echo WebSocket server:

// Run with: tjs serve examples/ws-echo-server.js
//
// Connect with: websocat ws://localhost:8000
//

export default {
    fetch(request, { server }) {
        if (request.headers.get('upgrade') === 'websocket') {
            server.upgrade(request);

            return;
        }

        return new Response('This is a WebSocket server. Connect using a WebSocket client.\n');
    },
    websocket: {
        open(ws) {
            console.log('Client connected');
        },
        message(ws, data) {
            console.log(`Received: ${data}`);
            ws.sendText(`echo: ${data}`);
        },
        close(ws, code, reason) {
            console.log(`Client disconnected: ${code} ${reason}`);
        },
    },
};

Code bundling

Even though creating a standalone has been a possibility for a while with txiki.js, it was annoying that bundling had to be manually done.

With all the streams and fetch work done, it was easy to chain streams to download and decompress esbuild, so that’s what tjs bundle does first, before running it with the right command line arguments to create a bundle txiki.js will handle well. Problem solved!

New website!

To put the cherry on top (and because I was really pumped!) I decided to tackle something I had been putting back for a long time: a website for the project with the documentation and guides.

I’ve done similar things for other projects before, and the biggest problem is usually putting together the first version. Incremental improvements are easier, but getting started can feel impossible.

Well, that’s done now, so head over to txikijs.org to see it in action.

Shout out to my oldest daughter for doing the logo ❤️

Other cool stuff

Happy tinkering! As for me, I hope to make progress on TLS support and some of the subtle crypto API for the next release.

Leave a Reply

Your email address will not be published. Required fields are marked *