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:
- Run any JS/TS app with
bext run ./my-app - Auto-detect the framework (Next.js, Hono, Express, plain fetch handler)
- Provide per-app isolation with memory and CPU limits
- Apply source transforms transparently
- 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.rsmodule - 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 --detectto 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
JSGlobalContextRefper 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
IsolateManagerwith 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
JscRenderPool→IsolateManager
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
TransformProfileenum with per-framework defaults - Make each transform independently configurable in TOML
- Allow custom transform ordering via
priorityfield - Add
transformssection tobext.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
TypeStripTransformthat 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
TypeStripTransformusingtsc_rs_parser - Auto-apply when loading
.tsfiles in JSC - Handle
import typevsimport(already done byimport_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:
- Explicit path (
./foo.ts,../bar.js) - Index files (
./foo→./foo/index.ts) node_moduleslookup (walk up directories)- Package.json
exportsfield - 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.jsonexports/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_jsoncrate 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 |