01 — Runtime Layer

The runtime layer is responsible for executing JavaScript/TypeScript applications. It handles JS evaluation, source transforms, caching, compression, and request routing.

Current State

Today bext has two runtime modes:

  • Plugin mode: NAPI addon inside Bun. Bun owns the event loop; bext handles cache lookups, compression, ETags, tenant resolution, and routing in native Rust via processRequest().
  • Server mode: Standalone actix-web binary with an embedded JSC render pool. No external JS runtime needed.

Both share bext-core for all business logic.

Target State

A unified runtime that can:

  1. Run any JS/TS app with bext run ./my-app
  2. Auto-detect the framework (Next.js, Hono, Express, plain fetch handler)
  3. Provide per-app isolation with memory and CPU limits
  4. Apply source transforms transparently
  5. Manage caches per-app

Implementation Tasks

RT-1: Framework Auto-Detection

Detect the framework from project files and wire up the appropriate handler chain.

Detection heuristic (priority order):

1. next.config.{js,ts,mjs}  →  Next.js adapter
2. bext.config.toml          →  bext native app
3. package.json "scripts.start" contains "hono"  →  Hono adapter
4. src/index.{ts,js} exports default { fetch }   →  Generic fetch handler
5. src/index.{ts,js} calls .listen()              →  Express/Fastify adapter
6. static/ or public/ directory only              →  Static site

File: bext-core/src/runtime/detect.rs

pub enum FrameworkKind {
    NextJs { app_dir: bool, pages_dir: bool },
    Hono,
    FetchHandler,
    Express,
    StaticSite,
    BextNative,
}

pub struct DetectionResult {
    pub kind: FrameworkKind,
    pub entry_point: PathBuf,
    pub static_dir: Option,
    pub config_path: Option,
}

pub fn detect(project_dir: &Path) -> DetectionResult { ... }

Tasks:

  • Create bext-core/src/runtime/mod.rs module
  • Implement detect() with the heuristic above
  • Write adapter trait that each framework implements
  • Next.js adapter (wraps existing route scanner + JSC SSR)
  • Hono adapter (load as fetch handler, wrap in JSC context)
  • Generic fetch adapter (export default { fetch })
  • Static site adapter (just serve files with ETag/304)
  • Tests for each detection case
  • bext-server scan --detect to print detected framework

RT-2: JS Isolate Pool

Replace the shared JSC pool with per-app isolates that enforce memory and CPU limits.

Current architecture:

JscRenderPool (shared)
  └── 4 worker threads, each with a JSC context
  └── All apps share the same context + global scope
  └── Component memoization cache is global

Target architecture:

IsolateManager
  └── App "marketing" → Isolate { context, memory_limit: 128MB, bundle }
  └── App "dashboard" → Isolate { context, memory_limit: 256MB, bundle }
  └── App "api"       → Isolate { context, memory_limit: 64MB, bundle }
  └── Shared worker thread pool (work-stealing)

Key design decisions:

  • JSC JSGlobalContextRef per app (already works — JSC supports multiple contexts per VM)
  • Memory limit via JSC's JSContextGroupSetExecutionTimeLimit + periodic GC pressure
  • CPU limit via fuel counting (similar to wasmtime fuel in WASM plugins)
  • Isolates are lazily created on first request, evicted on LRU basis
  • Each isolate has its own component memoization cache
  • Bundle hot-reload creates a new isolate, swaps atomically (old isolate drains)

File: bext-core/src/runtime/isolate.rs

pub struct IsolateConfig {
    pub app_id: String,
    pub bundle_js: String,
    pub memory_limit_mb: usize,     // default: 128
    pub execution_timeout_ms: u64,  // default: 5000
    pub max_concurrent: usize,      // default: 4
}

pub struct Isolate {
    context: JscContext,
    component_cache: ComponentCache,
    created_at: Instant,
    request_count: AtomicU64,
    memory_usage: AtomicUsize,
}

pub struct IsolateManager {
    isolates: DashMap<String, Arc>,
    worker_pool: WorkerPool,
    max_isolates: usize,           // LRU eviction when exceeded
}

impl IsolateManager {
    pub fn get_or_create(&self, config: &IsolateConfig) -> Result<Arc>;
    pub fn render(&self, app_id: &str, page: &str, props: &str) -> Result;
    pub fn hot_reload(&self, app_id: &str, new_bundle: String) -> Result<()>;
    pub fn evict(&self, app_id: &str);
    pub fn stats(&self) -> IsolateStats;
}

Tasks:

  • Design isolate lifecycle (create, render, hot-reload, evict)
  • Implement IsolateManager with DashMap-based pool
  • Per-isolate memory tracking via JSC APIs
  • Execution timeout enforcement
  • LRU eviction when max_isolates exceeded
  • Component cache scoped per isolate
  • Atomic hot-reload (new isolate + drain old)
  • Metrics: per-isolate request count, memory usage, render latency
  • Integration tests with multiple concurrent isolates
  • Migration path from JscRenderPoolIsolateManager

RT-3: Transform Pipeline Generalization

The current 14 transforms are Next.js-specific. Generalize to support other frameworks.

Current transforms:

import_strip, barrel_optimize, font_optimize, cache_directive,
server_boundary, server_actions, flow_extract, env_inline,
post_process, css_module, alias_rewrite, process_polyfill,
use_router_patch, shim_inject

Target: Transform profiles per framework:

pub enum TransformProfile {
    NextJs,      // All 14 transforms
    Hono,        // env_inline, import_strip, alias_rewrite
    Generic,     // env_inline, alias_rewrite, process_polyfill
    None,        // Static sites, pre-built bundles
    Custom(Vec),  // User-selected subset
}

Config:

[transforms]
profile = "nextjs"              # or "hono", "generic", "none"
# OR explicit list:
enabled = ["env_inline", "alias_rewrite", "barrel_optimize"]

[transforms.env_inline]
prefix = "NEXT_PUBLIC_"         # override per-transform config

[transforms.barrel_optimize]
packages = ["lucide-react", "@heroicons/react"]

Tasks:

  • Create TransformProfile enum with per-framework defaults
  • Make each transform independently configurable in TOML
  • Allow custom transform ordering via priority field
  • Add transforms section to bext.config.toml
  • Framework adapters select their default profile
  • User can override profile in config
  • Document which transforms apply to which framework

RT-4: TypeScript Support

Built-in TypeScript execution without requiring external tooling.

Current state: bext transforms operate on JS/TS source but don't do type stripping. The server relies on Bun (plugin mode) or a pre-built bundle (server mode).

Target: bext can load .ts files directly for simple apps (Hono, fetch handlers). Full app frameworks (Next.js) still use a bundler for the SSR bundle.

Approach:

  • Use tsc_rs_parser (already a dependency) for type stripping
  • Add a TypeStripTransform that removes type annotations, interfaces, enums
  • For simple apps: load entry point, strip types, evaluate in JSC
  • For complex apps: delegate to bundler (Bun build, esbuild, etc.)

Tasks:

  • Implement TypeStripTransform using tsc_rs_parser
  • Auto-apply when loading .ts files in JSC
  • Handle import type vs import (already done by import_strip)
  • Handle enum → object literal conversion
  • Handle decorators (strip or error with helpful message)
  • Benchmark: strip + eval vs pre-bundled
  • Config: typescript.strip = true (default for simple apps)

RT-5: Module Resolution

For apps that don't use a bundler, bext needs to resolve imports at runtime.

Scope: Only for simple apps (Hono, fetch handlers). Complex apps use a bundler.

Resolution order:

  1. Explicit path (./foo.ts, ../bar.js)
  2. Index files (./foo./foo/index.ts)
  3. node_modules lookup (walk up directories)
  4. Package.json exports field
  5. Workspace packages (workspace:* protocol)

Implementation:

  • In-memory module graph built on first load
  • Each module cached after type-strip + transform
  • Hot-reload invalidates affected modules (dependency tracking)
  • bext-core/src/runtime/resolver.rs

Tasks:

  • Implement Node-compatible module resolution
  • Support package.json exports/main/module fields
  • Handle workspace packages
  • Module cache with dependency tracking
  • Hot-reload: invalidate module + dependents
  • Source maps for error stack traces
  • Tests with real npm packages (hono, zod, etc.)

Performance Targets

Metric Current Target
Cached page response 52K req/s 60K+ req/s
Cold SSR render 2-5ms 2-5ms (isolate overhead < 100us)
Isolate creation N/A < 50ms
Hot reload swap ~200ms < 100ms
Type strip (10KB) N/A < 1ms
Memory per isolate N/A < 10MB base

Dependencies

New crates needed:

  • None for RT-1, RT-3 (uses existing deps)
  • RT-2: JSC API extensions (may need raw FFI for memory limits)
  • RT-4: tsc_rs_parser (already present)
  • RT-5: May need package_json crate for spec-compliant resolution

Risk Assessment

Risk Likelihood Impact Mitigation
JSC per-context memory limits not granular enough Medium High Use RSS monitoring + kill/restart instead
Module resolution edge cases (CJS/ESM interop) High Medium Start with ESM-only, add CJS shim layer later
Transform profile misses framework-specific needs Medium Low Allow custom transform lists from day 1
Isolate creation too slow for cold starts Low Medium Pre-warm pool, lazy creation with SWR