ananda rizki
All projects

JSpark

A desktop playground for JavaScript and TypeScript that lets you replay your code one step at a time, backwards as well as forwards. Visit website

No.
03 of 23
Type
Desktop App
Stack
TypeScript, Electron, Node.js, React.js, CodeMirror 6, Tailwind CSS, Babel, esbuild, SQLite, and Vitest
Credit
JSpark is my own product. I designed and built it myself, from the first prototype to the released app.
JSpark, cover

About

JSpark answers one question: "What does this code actually do?" You open it, type a few lines, and see the result straight away, with no project, terminal or config files to set up. It is a playground, a place to try an idea, check how something behaves or sketch a quick page, not a place to build applications.

Its main feature is the Timeline. After a Run, you can move through your code like a video: step forwards or backwards one statement at a time, drag across a strip to any moment, or let it play in slow motion. Beside the code, the Variables panel shows every value at that step, the Console shows what had been printed so far, and the Call Stack shows how the code got there. Pointing at any variable in the editor shows what it held at that moment. It also marks closures and explains why a loop variable keeps a different value on each turn, two of the ideas people find hardest in JavaScript.

Around it are the things a playground needs. Peeks draw a value right beside the line with a //? comment. Code runs in real Node.js, or as a live web page next to the editor, with HTML, CSS, Tailwind and frameworks such as React, Vue and Svelte working without setup. A Packages window searches npm and installs packages for every Snippet at once. A Library keeps the snippets worth keeping. Everything stays on the computer: there is no account, no cloud and no telemetry, and it works offline.

My Role

I built JSpark on my own: product design, interface, engineering and release. The app is Electron, which I chose over a smaller shell with Bun for one reason: correctness. A playground puts error messages and the order of timers and promises on screen all the time, and those had to match the V8 and Node behaviour people deploy to.

The Timeline works by recording, not by pausing. Before a Run, JSpark rewrites the Snippet's syntax tree so the code reports each step as it runs, then plays the finished recording back. That is what makes stepping backwards possible, because nothing has to be undone. I first tried V8's debugger for this, measured it at around 40 microseconds per step because it recompiles on every hit, and moved to instrumenting the code instead.

The editor is CodeMirror 6 with a TypeScript language service I wrote for it, giving autocomplete, types on hover and type errors. I first used Monaco, then swapped it after measuring the two side by side: the change cut the app's memory by up to 42% with a large snippet open.

Web pages run in a frame on their own origin, so Chromium gives each one its own process. A page stuck in an endless loop cannot freeze the editor, and three layers stop a runaway: process isolation, a loop guard that names the line, and a heartbeat that ends a page that stops answering.

Around the code I built the rest of a product: a free edition and a paid JSpark+ unlocked by a License Key, sold through Polar; automatic updates; and a release pipeline where a git tag builds the app in CI, and the Mac build is signed and notarized there. It shipped on Mac first and came to Windows 10 and 11 in 1.4.0, from one installer for Intel, AMD and ARM PCs.

Challenge

The hardest part was making the Timeline tell the truth. Recording every value at every step is expensive, and the cheap shortcuts are quietly wrong. Copying only the variables that changed made an object look frozen in the past, while keeping a reference to it showed values from the future. Neither threw an error. I designed a recording that tracks each object's identity and logs every write to it, so each step shows the value the code really saw, and I gave each recording limits on steps, time and memory, so a long Run is cut short and says so instead of crashing the app.

The other challenge was running unfinished code all day. A playground mostly runs code that is half written, so infinite loops and runaway pages are normal, not rare. JSpark had to stay responsive through them, while still letting code that is meant to keep running, such as a small HTTP server or a setInterval, do so. The answer was to stop a Run when it stays busy without a pause, not when it runs for a long time. A server waits between requests and keeps running; a while (true) loop never lets go and is stopped.

Fig. 1of 5
JSpark, plate 2
Fig. 2of 5
JSpark, plate 3
Fig. 3of 5
JSpark, plate 4
Fig. 4of 5
JSpark, plate 5
Fig. 5of 5