repljs is a durable, branchable JavaScript/TypeScript REPL built around:
- durable session history
- restore and fork from committed cells
- replay-aware host effects
- structured value transport with
jswire
Most embedders should import the top-level package:
import repl "github.com/mackross/repljs"It re-exports the embedder-facing contracts and enums, and provides:
repl.New()for the default engine implementationrepl.NewTypeScriptFactory()for the default incremental TS checker
Concrete store backends still live in subpackages such as:
github.com/mackross/repljs/store/sqlitegithub.com/mackross/repljs/store/mem
This section is the "non-obvious behavior" list: semantics that are easy to miss, choices that are intentional but unusual, and notation invented by this project.
OpenSession(...)reopens an existing durable session at its current active cursor.Restore(...)does not fork. It moves the active session cursor to an existing committed cell on the same branch and rebuilds runtime state there.Fork(...)is the explicit branch-creating operation. If you want a new branch rooted at an earlier cell, useFork, notRestore.- Value handles are branch/runtime-local. A handle that was valid before
RestoreorForkmay become stale afterwards.
Submit(...)is the JavaScript convenience path.SubmitCell(...)is the structured API and lets callers choosejsvstsper cell.Submit(...)/SubmitCell(...)block until the cell result and tracked async work created by that cell have settled.- Failed submit returns a typed
*engine.SubmitFailurewith linked effect summaries for the effects started by that cell. - Successful submit returns a
CompletionValuehandle, and callers can useInspect(...)to obtain richer renderings. - Each committed cell also has a branch-local monotonic index in addition to its durable UUID.
- UUID remains the durable identity.
- The index is the low-token, branch-visible position used for REPL conveniences.
- TypeScript is optional and lazy.
- If a TS frontend is configured, the first
tscell creates it on demand. - Replay/open/restore execute stored emitted JS and do not require a live TS checker.
- If a TS frontend is configured, the first
$lastis the previous successful cell's completion value on the current branch/runtime position.$val(N)returns the completion value for visible committed cell indexNon the current branch lineage.- Failed cells do not update
$last. - These bindings are rebuilt during replay/open/restore; they are runtime conveniences, not synthetic user source.
Date/Date.now()/Date()are replay-aware through the Goja time source hook.Math.random()is replay-aware through the Goja random source hook.- Timer globals are intentionally unavailable:
setTimeoutsetIntervalclearTimeoutclearInterval
- The event loop still exists for promises and async host callbacks. Only the timer globals are disabled.
- All cells run through the same internal async wrapper model. That is how top-level
awaitworks, and it also keeps completion behavior consistent across cells with and withoutawait. cmd/replstarts injsmode and supports:ts/:jsto switch the default language for later submits.- Because every cell uses the wrapper model, some behavior now follows wrapped-function semantics rather than raw script semantics. See tests around top-level await and wrapped-cell behavior for the current edge cases.
Previewis usually the raw Goja string coercion of the completion value.- Example: a plain object preview is usually
[object Object]. - Fulfilled promise completions are rendered from
jswireasPromise<...>so they do not collapse into their settled value.
- Example: a plain object preview is usually
Summaryis the low-token, shape-first rendering intended for embedders and LLMs.Fullis a richer but still bounded rendering.SummaryandFullare rendered from the durablejswirepayload, not from a live VM walk.
- Shared references and cycles use:
&Nfor the first definition of a shared value*Nfor a later reference back to that same value
- Example:
&1 {self: *1}This is project-specific notation inspired by YAML/Lisp-style shared-structure markers; it is not standard JavaScript syntax.
- Long strings are truncated in
Summaryand shown asstring(N) "prefix…"whereNis the original rune length. - Arrays, objects, maps, sets, typed arrays, and buffers show a bounded sample plus an omission count like
…+3. Fulluses larger budgets thanSummary, but it is still intentionally bounded.
- Structured inspection currently renders object properties in deterministic sorted order.
- This means inspection output may not preserve original insertion order for plain object properties.
- The main reason is stable, comparable output for tests and embedders.
- Host calls cross the effect layer and are journaled durably.
- Failed submit exposes linked effects directly on the returned error so embedders can decide what to show the LLM.
- Effect params/results are also encoded through
jswire, so built-in JS types can survive host crossings better than plain JSON. console.log(...args)is not stored durably, but the engine returns the rendered log lines on submit and can replay a committed cell later to re-derive them.inspect(...args)is a pure helper that returns the bounded summary string; it is not stored.
jswireis the project-specific wire format for moving JS values between runtimes and storing inspectable structured values.- It preserves more than JSON, including built-in JS structures such as:
DateRegExpMapSetArrayBuffer- typed arrays
- shared refs / cycles
- Functions, promises, symbols, and other unsupported runtime values do not round-trip through
jswire.
- Some exact inspector-output tests are intentionally "reviewed shape" tests. They exist to pin human-reviewed formatting, not just semantic equivalence.