@kyneta/cast
v1.2.0
Published
Compiled delta-driven web framework
Readme
@kyneta/cast
🧪 Prototype — This package is an experimental exploration of compiled delta-driven UI. Not ready for production use.
A compiled delta-driven UI framework powered by the CHANGEFEED protocol from @kyneta/schema.
Overview
Kyneta transforms natural TypeScript into code that directly consumes structured deltas for O(k) DOM updates, where k is the number of operations (not the size of your data).
The Problem
Traditional UI frameworks, including React, must diff entire data structures to discover what changed. When you add one item to a 1000-item list, they re-render all 1000 items to find the one that's new.
The Solution
Reactive data sources that implement the CHANGEFEED protocol already know exactly what changed — they provide deltas like "insert item at index 3". Kyneta's compiler transforms natural TypeScript into code that directly consumes these deltas, updating only the affected DOM nodes.
// What you write (natural TypeScript)
div(() => {
h1(doc.title) // Bare reactive ref — no explicit call needed
if (doc.items.length === 0) {
p("No items yet")
}
for (const item of doc.items) {
li(item.text)
}
})
// What happens at runtime:
// - Edit title → O(k) character-level DOM patch (not full replacement)
// - Insert 1 item → 1 DOM insert (not 1000 re-renders)
// - Delete item at index 5 → 1 DOM remove
// - No diffing, no reconciliationBare Reactive Refs
Kyneta supports passing reactive refs directly as children — no explicit call required:
// All three produce identical compiled output:
p(doc.title) // ← Preferred: bare ref (toPrimitive coercion)
p(`${doc.title}`) // Also works: template literal interpolation
p(doc.title()) // Also works: explicit callThe compiler detects that doc.title implements CHANGEFEED (reactive + observable), synthesizes the value read internally, and routes it to textRegion for O(k) surgical text patching. For non-text refs, the compiler synthesizes a value read and uses valueRegion for full-replacement updates.
Server-Side Rendering
Kyneta supports server-side rendering with optional pretty-printing for development:
import { renderToDocument } from '@kyneta/cast/server'
// Load compiled app (Vite plugin auto-compiles to HTML target for SSR)
const { createApp } = await vite.ssrLoadModule('/src/app.ts')
const renderApp = createApp(doc)
// Minified output (default, for production)
const html = renderToDocument(renderApp, doc, {
title: 'My App',
head: '<style>...</style>',
scripts: '<script type="module" src="/src/main.ts"></script>',
})
// Pretty-printed output (for development/debugging)
const prettyHtml = renderToDocument(renderApp, doc, {
title: 'My App',
pretty: true, // Formats the rendered content with indentation
})SSR Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| hydratable | boolean | true | Include hydration markers for client rehydration |
| pretty | boolean | false | Format HTML with indentation (useful for debugging) |
| title | string | - | Document title |
| head | string | - | Additional content for <head> (styles, meta tags) |
| scripts | string | - | Scripts to include before closing </body> |
Components
Kyneta supports user-defined components via the ComponentFactory type. The compiler uses TypeScript's type system to detect component functions — no special syntax or naming conventions required.
import type { ComponentFactory } from "@kyneta/cast"
// Define a component
const Card: ComponentFactory<{ title: string }> = (props) => {
return div(() => {
h2(props.title)
p("Card content")
})
}
// Use it like any HTML element
div(() => {
Card({ title: "Hello" })
Card({ title: "World" })
})Components:
- Are ordinary functions typed as
ComponentFactory - Receive their own
Scopefor subscription cleanup - Work with template cloning (serialized as placeholders, instantiated at runtime)
- Can accept props, a builder callback, both, or neither
Local Reactive State
Use state() to create local reactive refs that participate in the CHANGEFEED protocol:
import { state } from "@kyneta/cast"
const count = state(0)
div(() => {
p(`Count: ${count}`)
button({ onClick: () => count.set(count() + 1) }, "Increment")
})state() returns a LocalRef<T> — a lightweight callable reactive primitive that implements the CHANGEFEED symbol from @kyneta/schema. Call count() to read the current value, or use ${count} in template literals (refs implement Symbol.toPrimitive). The compiler detects it via the same type-level mechanism used for any Changefeed-bearing ref.
Client & Server Code
Inside builder functions, use labeled blocks to mark code as client-only or server-only:
return div(() => {
const count = state(0)
client: {
// Only runs in the browser — stripped from SSR output
setInterval(() => count.set(count() + 1), 1000)
}
server: {
// Only runs during SSR — stripped from client bundle
console.log("Rendered at", new Date().toISOString())
}
h1(`${count}`)
})client: { ... }— browser only (stripped during SSR compilation)server: { ... }— SSR only (stripped from client bundle)- Unlabeled code — runs in both contexts
These are standard TypeScript labeled statements — no custom syntax, no build tool magic. The compiler recognizes client and server labels inside builder functions and filters the IR tree before code generation.
Prototype Status
This is an experimental prototype exploring whether compilation can unlock O(k) UI updates from structured deltas.
| Feature | Status | Notes |
|---------|--------|-------|
| Package scaffolding | ✅ | Build, test, verify tooling |
| Error taxonomy | ✅ | Typed errors with source locations |
| Type definitions | ✅ | Ambient declarations for LSP |
| Runtime primitives | ✅ | Scope, subscriptions, regions |
| Compiler infrastructure | ✅ | IR, analysis, codegen |
| Static compilation | ✅ | Elements, attributes, text |
| Reactive expressions | ✅ | CHANGEFEED-based type detection, bare-ref support |
| List transform | ✅ | O(k) verified with tests |
| Conditional transform | ✅ | if/else-if/else chains |
| Input binding | ✅ | text, checkbox, numeric |
| Template cloning | ✅ | 3-10× faster DOM creation |
| Batch list operations | ✅ | O(1) DOM ops for contiguous changes |
| Component model | ✅ | Type-based ComponentFactory detection |
| Lazy scopes | ✅ | Skip allocation for static list items |
| Target labels | ✅ | client: / server: blocks |
| Local reactive state | ✅ | state() / LocalRef via CHANGEFEED |
| Vite plugin | 🔴 | Placeholder only |
| SSR + Hydration | 🟡 | Codegen + target labels done, hydration wiring needed |
How It Works
Compilation Model
Kyneta uses a multi-phase compiler:
- Analysis (
analyze.ts) — Parses TypeScript AST via ts-morph, produces IR - Walking (
walk.ts) — Generator-based IR traversal yields structural events - Template Extraction (
template.ts) — Collects static HTML + dynamic hole positions - Walk Planning (
template.ts) — Converts hole paths to optimal DOM navigation ops - Code Generation (
codegen/dom.ts,codegen/html.ts) — Transforms IR to JavaScript - Orchestration (
transform.ts) — Coordinates the pipeline, hoists template declarations
The key insight is CHANGEFEED-based reactive detection: the compiler uses TypeScript's type checker to identify expressions whose types carry the CHANGEFEED symbol (from @kyneta/schema), then generates appropriate subscriptions. The delta kind (text, sequence, etc.) is extracted from the Changefeed's change type to dispatch to the optimal region handler. Component functions are also detected via type inspection (ComponentFactory).
Runtime
The compiled code calls into a minimal runtime:
Scope— Ownership tracking with parent-child cleanup (lazy allocation, numeric IDs)subscribe— CHANGEFEED-based subscription (delta-aware)valueRegion— Replace-semantic updates for any Changefeed or computed expressionlistRegion— Delta-based list updates with batch insert/deleteconditionalRegion— Branch swappingtextRegion— Character-level text patching viainsertData/deleteDatainputTextRegion— Surgical<input>/<textarea>value patching viasetRangeTextread(ref)— Universal value accessor:ref[CHANGEFEED].current
Template Cloning
Static DOM trees are created via <template>.content.cloneNode(true) instead of per-element createElement calls. The compiler extracts static HTML, emits a module-level <template> declaration, and generates a walker that grabs references to dynamic "holes" in a single pass.
No Runtime Element Factories
Unlike React or Solid, there are no div(), h1(), etc. functions at runtime. Instead:
src/types/elements.d.tsprovides ambient type declarations for TypeScript LSP- The compiler transforms builder calls into direct DOM manipulation
- You run the compiled output, not the source
This means you cannot currently run Kyneta code without compilation.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ User Source Code │
│ div(() => { if (x) { p("yes") } for (i of list) { ... } })│
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ts-morph Compiler │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Analyze │ │ IR │ │ Generate │ │
│ │ (AST → IR) │ │ (data) │ │ (IR → Code) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Compiled Output │
│ Static DOM creation + delta subscriptions + region mgmt │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Kyneta Runtime │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ mount() │ │ Regions │ │ CHANGEFEED Protocol │ │
│ │ dispose() │ │ Management │ │ (delta handlers) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘Example App
See examples/recipe-book/ — a runnable example demonstrating SSR, hydration, multi-tab sync via WebSocket, and all four delta kinds (text, sequence, replace, increment).
Running Tests
pnpm turbo run verify --filter=@kyneta/cast -- logic
# Run all tests
cd packages/cast && npx vitest run
# Run specific test file
pnpm turbo run verify --filter=@kyneta/cast -- logic -- -t 'list region'Migration Notes
Scope.id type change (string → number)
Scope.id changed from string to number for performance (avoids string allocation in tight loops). If you were comparing scope IDs to strings like "scope-1", use numeric comparisons instead:
// Before
expect(scope.id).toBe("scope-1")
// After
expect(scope.id).toBe(1)CHANGEFEED protocol (replaces REACTIVE + SNAPSHOT)
The reactive protocol has been unified from two symbols (REACTIVE + SNAPSHOT) to a single CHANGEFEED symbol from @kyneta/schema:
// Old (two-symbol design from the predecessor reactive package)
ref[REACTIVE](ref, callback) // subscribe
ref[SNAPSHOT](ref) // read current value
// New (single-symbol CHANGEFEED coalgebra from @kyneta/schema)
ref[CHANGEFEED].subscribe(cb) // subscribe
ref[CHANGEFEED].current // read current valueThe compiler, runtime, and all region handlers now use this single protocol. Any object implementing CHANGEFEED is automatically detected as reactive.
What's Next
The remaining work to complete the prototype:
- Vite Plugin — Enable actual usage in projects
- SSR + Hydration — Server rendering with client hydration
- Integration Test — Full app validation
- Component builder callbacks — Passing children to components
Comparison
| | Uncompiled | Typed | Declarative | Reactive | O(k) Mutations | Template Cloning | |--------------------|------------|-------|-------------|----------|----------------|------------------| | React | ✅ | ✅ | ~ | ~ | ❌ | ❌ | | Solid | ❌ | ✅ | ✅ | ✅ | ~ | ✅ | | Svelte | ❌ | ~ | ✅ | ✅ | ~ | ~ | | Vue | ❌ | ~ | ✅ | ✅ | ❌ | ❌ | | Kyneta | ~* | ✅ | ✅ | ✅ | ✅ | ✅ |
* Source is valid TypeScript (LSP works), but compilation required for execution
License
MIT
