Hoi hoi!
I'm @nyaomaru, a frontend engineer who was recently knocked off my feet by how good the zeetong (sole) from a Dutch fish shop was. ππΈ
In...
For further actions, you may consider blocking this person and/or reporting abuse
Rewinding execution state in JavaScript is notoriously tricky because of how reference types mutate under the hood, so I am curious if you are using structural sharing or deep cloning at each step to keep memory usage in check. Building visual debuggers or step-through tools always makes me appreciate the underlying engine mechanics even more. I actually ran into a similar state-tracking challenge when building our Next.js and Supabase SaaS boilerplate, PubliFlow, where we needed to visualize complex data flow changes for the user without tanking performance. How are you handling the serialization of circular references if they ever get caught in your execution snapshot?
Thank you for great question. π
We use a hybrid approach that combines deep cloning for each step with lazy shallow reconstruction instead of full structural sharing.
Conceptually, it looks like this π
deepCloneusesstructuredClonewhen possible. This isolates the captured values for each step. Rewinding simply moves through the recorded timeline. It does not reverse mutations or run the code again.The tradeoff is that a large object visible across many steps may still be cloned repeatedly, so traces are currently capped at 3,000 steps.
Runtime snapshots are not serialized as JSON.
structuredClonepreserves cycles and shared references. AWeakMapbased fallback handles unsupported cases. The UI formatter is intentionally lossy and displays repeated references as "[Circular]".PubliFlow sounds like it faced a very similar balance between snapshot fidelity and keeping the visualization responsive. πΌ
That hybrid approach is a clever compromise. Relying on deep cloning for visible variables while lazily reconstructing the rest via spread operators likely keeps the memory overhead manageable compared to full structural sharing. I am curious how you handle circular references or non-serializable objects like DOM nodes when performing that initial deep clone.
Yeah, thatβs pretty much it! Full structural sharing would probably be even more memory efficient, but this hybrid keeps the implementation simpler while avoiding eager reconstruction of every complete snapshot.
Circular references are handled by
structuredClone. If that cannot clone a value, our fallback uses aWeakMapto preserve cycles and shared references.DOM nodes are a little different. Execution runs inside a
Web Worker, so they cannot enter the execution context in the first place. We mainly support algorithm focused data such as objects, arrays, maps, sets, dates, and regular expressions. If values such as functions or symbols need to cross the worker boundary, we replace them with readable labels. πΈUsing structuredClone with a WeakMap fallback is a pragmatic way to handle circular references without reinventing the wheel. I am curious about how you handle DOM nodes since they are inherently non-serializable and tied to the live document state. Do you serialize their attributes and tree structure, or just maintain lightweight references to the actual DOM elements during the rewind process?
Neither, actually.
We do not currently have a DOM specific snapshot path, so we neither serialize the tree nor retain live element references.
If we add DOM support later, I would use a small serializable projection of the relevant attributes and structure.
Live references would keep mutating and undermine reliable rewinding. πΈ
very cool!π±
Thx!! πΈ
the AST transform approach is the right call here. the alternative is running the original code and somehow snapshotting state externally, which falls apart the moment closures or mutation are in play. by injecting recordStep at transform time, you own the snapshot shape β that's the part where most replay systems break.
the Web Worker isolation is what i didn't see coming but immediately makes sense. without it, an infinite loop hangs the whole UI. we hit this running untrusted LLM generated snippets in a browser runner; Worker plus message timeout kill switch is the only safe pattern.
curious whether the AST transformation handles generators and async/await, or whether those are explicitly out of scope for DSA use cases?
Thanks for the thoughtful comment πΈ
Thatβs exactly why I went with the AST transform approach. The injected
recordStepcalls capture state at meaningful execution boundaries.The UI navigates that recorded trace, so stepping backward, forward, or jumping between steps does not require re-running the code or trying to undo JavaScript mutations π
Web Workers arenβt something I reach for in most UI work, but theyβre a great fit when execution needs to be separated from rendering. An infinite loop cannot freeze the main UI thread, and the host can terminate the Worker when the timeout expires.
async/await is supported now!
The runtime waits for Promise-returning entry functions and records await suspension, resumption, and rejection.
It also tracks concurrent in-process branches such as
Promise.allindependently, so their call frames do not get mixed together.Full event-loop visualization, external side-effect replay, and network request visualization are still outside the current scope.
Generators, however, are not yet supported as a first-class traced execution model.
Supporting them properly means tracking frame state across yield, next, throw, and return.
Itβs a great suggestion, and I plan to support them πΌ