runtime

package
v1.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 13 Imported by: 0

README

zip/runtime — embedded multi-language code runner

runtime embeds language backends into a zip service so a request can run arbitrary-language source in-process — no separate runtime service, no inter-service RPC, no container-per-language.

Runner

Runner is a registry of language → Engine plus a goroutine-scoped, context.Context-bound dispatch loop:

type Runner interface {
    Run(ctx context.Context, lang string, src []byte, args ...any) (any, error)
    Register(lang string, engine Engine) error
    Languages() []string
}

func NewRunner() Runner

Each Run call:

  1. Looks up the engine for lang. No engine → ErrUnknownLanguage.
  2. Spawns a worker goroutine that calls engine.Eval(ctx, src, args...).
  3. Spawns a watcher goroutine on ctx.Done() that, for an Interruptible engine, calls Interrupt(ctx.Err()) so a runaway eval unwinds instead of leaking.
  4. Joins both goroutines before returning, so a borrowed VM is never handed back to its pool while still interrupted. Returns the engine result, or ctx.Err() if cancellation won the race.

This generalizes the settled JSRuntime.EvalContext contract (one worker, one watcher, joined before return, engine-specific interrupt on cancel) across every backend a host registers.

Engine SPI

// Implemented by every backend.
type Engine interface {
    Eval(ctx context.Context, src []byte, args ...any) (any, error)
}

// Optional: backend whose running eval can be aborted out-of-band
// (goja vm.Interrupt, pyvm PyErr_SetInterrupt, wasm trap).
type Interruptible interface {
    Interrupt(cause error)
}

An engine that fully manages cancellation inside Eval (zip's goja engine does — EvalContext owns the per-call interrupt watcher and targets only the borrowed VM) marks itself so the Runner's watcher is a benign no-op and the not-interruptible warning is suppressed. An engine that is neither Interruptible nor self-managed gets a one-time registration warning: its Run still returns ctx.Err() promptly, but the worker runs to completion.

Dependency direction — the registry is the seam, not an import edge

zip does not import hanzoai/base. Engine here is a zip-side projection of base's extruntime backends; any value implementing Eval satisfies it. base imports zip and registers its backends at app startup — zip exposes the registry and never names base:

// In the host binary (which already imports base) — NOT in zip:
runner := zipruntime.NewRunner()

// zip ships exactly one in-tree engine: pure-Go goja, no cgo.
js, _ := zipruntime.NewJSRuntime(zipruntime.JSOptions{PoolSize: 8})
runner.Register("js", js.Engine())

// base's backends plug in through the same seam via thin extruntime
// adapters — zip stays base-free:
runner.Register("py",    pyvmEngine())    // base/plugins/pyvm   (cgo CPython)
runner.Register("js-v8", v8vmEngine())    // base/plugins/v8vm   (cgo V8)
runner.Register("wasm",  wasmvmEngine())  // base/plugins/wasmvm (pure-Go wazero)
runner.Register("stark", starkvmEngine()) // base/plugins/starkvm

gojavm (base/plugins/gojavm) is the canonical first backend: it wraps this package's *JSRuntime, so there is exactly one goja engine in the stack and base, cloud, and every other zip consumer share it.

See also

  • jsvm.go — pooled goja VMs + EvalContext cancellation contract.
  • esbuild.go — TS/modern-JS → ES5 for the goja engine.
  • examples/express-in-zip/main.goPOST /runtime/:lang mounts the Runner over HTTP.

Documentation

Overview

esbuild.go wraps esbuild's pure-Go API (no CGO) so zip can compile TS / modern-JS handler source down to ES5 that the embedded goja VM executes. This is the build step of zip's TS migration path:

TS source --esbuild target=es5--> ES5 JS --drop into--> embedded goja

Run TranspileToES5 at service startup (or at build time) to compile bundled handlers before serving; the resulting bytes are handed to JSRuntime.LoadModule / Eval.

handler.go bridges a goja-loaded JS function into Fiber. It returns fiber.Handler (not zip.Handler) on purpose: the zip root package imports this package, so depending back on zip would be an import cycle. fiber.Handler mounts cleanly anywhere — app.Fiber().Get(...) or any *fiber.App — and is the natural seam for legacy Express-shaped handlers running inside the embedded VM.

The req/res objects handed to JS mirror the Express shape so existing `function (req, res) { res.json(...) }` handlers run unmodified:

req.method   req.path   req.query   req.headers   req.body
res.status(n)   res.set(k,v)   res.json(v)   res.send(v)

jsvm.go embeds a pure-Go JavaScript runtime (goja) into zip so a service can run TS/JS handlers in-process — no separate runtime service, no inter-service RPC, no container-per-service. Combined with esbuild.go (TS/modern-JS → ES5) this is zip's migration path: legacy TS source compiles to ES5 and drops straight into the embedded VM, then gets rewritten to a native Go handler in place over time.

The pool pattern is lifted from hanzoai/base/plugins/gojavm and slimmed to what zip needs: each *goja.Runtime carries the host functions and modules registered at construction time, and requests borrow a free VM (or a one-off when the pool is saturated) so they never pay per-request VM creation cost.

runner.go is zip's unified multi-language code runner. A Runner holds a registry of language → Engine and executes a source string against the engine registered for a language, each call in its own goroutine, bound to a context.Context. It generalizes the goroutine-scoped determinism contract of JSRuntime.EvalContext (one worker, one watcher, joined before return, engine-specific interrupt on cancel) across every language backend the host registers.

DEPENDENCY DIRECTION. zip does NOT import hanzoai/base. The Engine interface here is a zip-side projection — duck-typed, exactly like the Loader/Module projection in package internal/runtime. base's backends (gojavm, pyvm, v8vm, wasmvm, starkvm) implement an extruntime.Runtime SPI; a thin adapter at the host's app-startup makes each satisfy this Engine and registers it. base imports zip and registers; zip exposes the registry and never names base. That is the whole point of the SPI: the registry is the seam, not an import edge.

The one in-tree Engine zip ships is the goja engine — zip's own *JSRuntime, surfaced via (*JSRuntime).Engine(). It is pure Go, no cgo, and is the canonical "js" backend. Everything else plugs in from base.

Package runtime is the zip-side projection of HIP-0105's extension runtime contract. Consumers of zip pass a Loader implementation here (typically *extruntime.Loader from hanzoai/base/plugins/extruntime) and zip mounts modules as routes via app.Module().

Index

Constants

This section is empty.

Variables

View Source
var ErrUnknownLanguage = errors.New("zip/runtime: unknown language")

ErrUnknownLanguage is returned by Run when no engine is registered for the requested language.

Functions

func BundleToES2015

func BundleToES2015(entry string, files map[string][]byte, opts BundleOptions) ([]byte, error)

BundleToES2015 transpiles and bundles a multi-file TS/JS tree rooted at entry into a single ES2015 string ready for goja. files maps virtual paths (e.g. "entry.js", "lib/util.ts") to their source bytes; entry is the key of the root module. Relative imports between files are resolved against the importer's directory within the files map — no real filesystem access occurs. Packages listed in opts.External are left as require()/import calls. Returns a non-nil error if esbuild reports any error-level diagnostic (including an unresolved import).

func JSHandler

func JSHandler(rt *JSRuntime, fnName string) fiber.Handler

JSHandler returns a fiber.Handler that invokes the global JS function named fnName (defined in the runtime via Eval / LoadModule) with an Express-shaped (req, res) pair. State written through res is propagated back onto the Fiber Ctx after the call.

func JSModule

func JSModule(rt *JSRuntime, modulePath string) (fiber.Handler, error)

JSModule loads a CommonJS module whose `module.exports` is the handler function (Express shape) and returns a fiber.Handler for it. modulePath is the name the module was registered under via LoadModule.

func TranspileToES5

func TranspileToES5(src []byte, opts ESOptions) ([]byte, error)

TranspileToES5 compiles src (TS or modern JS) to ES5 JavaScript ready for goja. The output is CommonJS-format so a module that does `module.exports = handler` can be loaded via JSRuntime.LoadModule and resolved with require(). Returns a non-nil error if esbuild reports any error-level diagnostic.

Types

type BundleOptions

type BundleOptions struct {
	// Format is the output module format: "iife", "cjs", or "esm". Empty
	// defaults to "cjs", the format goja loads via require().
	Format string

	// Sourcemap inlines a base64 source map into the output for debugging.
	Sourcemap bool

	// Minify enables identifier/whitespace/syntax minification.
	Minify bool

	// External lists import paths (typically bare package specifiers like
	// "external" or "node:fs") that are left as require()/import calls in
	// the output instead of being bundled. Resolution of these is the
	// host's responsibility (e.g. a goja module registered under the same
	// name).
	External []string
}

BundleOptions configures a multi-file bundle. Zero value bundles to CommonJS (the format goja's require() understands) with no source map and no minification.

type ESOptions

type ESOptions struct {
	// Loader selects how the source is parsed. "ts", "tsx", "jsx", or
	// "js". Empty defaults to "ts" (TS is a superset of JS, so plain JS
	// also parses).
	Loader string

	// Minify enables identifier/whitespace minification.
	Minify bool

	// Sourcefile is the logical filename used in error messages.
	Sourcefile string
}

ESOptions configures a transpile. Zero value is a sane default: TypeScript loader, ES5 target, no minification, CommonJS format so `module.exports = ...` survives into the goja-loadable output.

type Engine

type Engine interface {
	// Eval evaluates src and returns the engine-native result value
	// (already converted to a Go value, e.g. goja's Export()). args are
	// passed through to engines that accept invocation arguments;
	// engines that only evaluate a top-level expression ignore them.
	//
	// Eval MUST respect ctx: at minimum it returns promptly once ctx is
	// done. Interruptible engines additionally honor Interrupt.
	Eval(ctx context.Context, src []byte, args ...any) (any, error)
}

Engine executes source in one language. It is the zip-side projection of base's extruntime backends; any value implementing Eval satisfies it, so base registers its backends without zip importing base.

An Engine MAY additionally implement Interruptible. When it does, the Runner's watcher goroutine calls Interrupt(ctx.Err()) on cancellation so a runaway evaluation unwinds instead of leaking. When it does not, the watcher cannot abort a running call — the Run still returns ctx.Err() promptly, but the underlying worker runs to completion. The Runner logs a one-time warning per such engine at registration.

type Interruptible

type Interruptible interface {
	Interrupt(cause error)
}

Interruptible is implemented by engines whose running evaluation can be aborted out-of-band (goja vm.Interrupt, pyvm PyErr_SetInterrupt, wasm trap). The Runner's watcher calls Interrupt with ctx.Err() on cancel.

type JSOptions

type JSOptions struct {
	// PoolSize is the number of pre-warmed *goja.Runtime kept hot. When
	// every pooled VM is busy, calls fall back to a freshly-built VM that
	// is discarded after the call. 0 selects a default of 8.
	PoolSize int

	// HostFns are Go functions exposed to JS as globals. Applied to every
	// VM in the pool (and to one-off VMs). Equivalent to calling
	// RegisterHostFn for each entry after construction, but applied to
	// the whole pool atomically at build time.
	HostFns map[string]any

	// Modules are CommonJS-style module sources registered into every VM,
	// reachable from JS via require(name). Applied at build time.
	Modules map[string]string
}

JSOptions configures a JSRuntime.

type JSRuntime

type JSRuntime struct {
	// contains filtered or unexported fields
}

JSRuntime is an embedded JavaScript runtime backed by a pool of goja VMs. It is safe for concurrent use: each call borrows an isolated VM.

func NewJSRuntime

func NewJSRuntime(opts JSOptions) (*JSRuntime, error)

NewJSRuntime builds a JSRuntime with the given options. Host functions and modules from opts are applied to every VM in the pool.

func (*JSRuntime) Engine

func (rt *JSRuntime) Engine() Engine

Engine adapts this JSRuntime to the Runner's Engine interface so it can be registered as a language backend:

runner.Register("js", rt.Engine())

The adapter is deliberately NOT Interruptible at the Runner seam: EvalContext already owns the per-call interrupt watcher and targets the exact pooled VM the call borrowed (interrupting only that VM, never a sibling concurrent call's VM). A Runner-level Interrupt would have to fan out to every pooled VM and could abort unrelated concurrent calls. So the goja engine self-manages cancellation inside Eval; the Runner's watcher is a no-op for it (and the registration warning is suppressed because Eval honors ctx fully). Engines without internal ctx handling implement Interruptible and let the Runner's watcher abort them.

func (*JSRuntime) Eval

func (rt *JSRuntime) Eval(src string) (any, error)

Eval evaluates src in a pooled VM and returns the exported Go value of the result (via goja's Export()). It is EvalContext with a background context — the evaluation cannot be cancelled.

func (*JSRuntime) EvalContext

func (rt *JSRuntime) EvalContext(ctx context.Context, src string) (any, error)

EvalContext evaluates src in a pooled VM under ctx and returns the exported Go value of the result. If ctx is cancelled or its deadline is exceeded before the evaluation completes, the running goja VM is interrupted and EvalContext returns ctx.Err() (context.Canceled or context.DeadlineExceeded). goja is single-threaded per borrowed VM, so a tight loop like `while(true){}` is preempted at the next interrupt check point rather than blocking the caller indefinitely.

func (*JSRuntime) InvokeFunc

func (rt *JSRuntime) InvokeFunc(ctx context.Context, fnName string, args ...any) (any, error)

InvokeFunc calls the global JS function named fnName with args (each converted to a goja value) and returns the exported Go value of its result. Like EvalContext, the call is interrupted and ctx.Err() is returned if ctx finishes before the function does. The named value must already be defined in the runtime (via Eval/EvalContext or LoadModule).

func (*JSRuntime) LoadModule

func (rt *JSRuntime) LoadModule(name, src string) error

LoadModule registers a CommonJS-style module under name. The module source is evaluated lazily the first time require(name) is called in a given VM. Applied to every pooled VM.

func (*JSRuntime) RegisterHostFn

func (rt *JSRuntime) RegisterHostFn(name string, fn any) error

RegisterHostFn exposes a Go function to JS as a global named name. The function is applied to every VM in the pool and to one-off VMs built afterwards. fn may be any value goja can bind (typically a Go func).

type Loader

type Loader = internal.Loader

Loader is re-exported from internal/runtime for ergonomic use:

loader := myextruntime.NewLoader(...) // implements zipruntime.Loader
app := zip.New(zip.Config{Loader: loader})

type Module

type Module = internal.Module

Module is re-exported from internal/runtime.

type Runner

type Runner interface {
	// Run looks up the engine for lang, spawns a worker goroutine that
	// calls engine.Eval, and a watcher goroutine that interrupts the
	// engine when ctx is done. It returns the engine result, or ctx.Err()
	// if cancellation won the race. Both goroutines are joined before Run
	// returns. If no engine is registered for lang it returns
	// ErrUnknownLanguage.
	Run(ctx context.Context, lang string, src []byte, args ...any) (any, error)

	// Register installs engine under lang. Re-registering a language
	// replaces the prior engine. Registration is concurrency-safe.
	Register(lang string, engine Engine) error

	// Languages returns the registered language names, sorted.
	Languages() []string
}

Runner executes arbitrary-language code in goroutine-scoped, ctx-bound tasks. Safe for concurrent use: Run, Register and Languages may all be called from many goroutines.

func NewRunner

func NewRunner() Runner

NewRunner returns an empty Runner. Register backends before Run.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL