Go SDK
Complete guide to building Hypen servers with Go
Go SDK
The Go SDK (github.com/hypen-space/core) provides a server-side Hypen implementation for Go applications with reactive state management, routing, cross-module communication, and Remote UI streaming over WebSocket.
Installation
go get github.com/hypen-space/coreQuick Start
Create a server that streams a counter UI over WebSocket. Module state is a
plain Go struct, and action handlers mutate it directly — no any casts, no
string-keyed Get/Set, typos caught at compile time:
package main
import (
"fmt"
core "github.com/hypen-space/core"
"github.com/hypen-space/core/remote"
)
// Module state as a Go struct. json tags match the @{state.xxx}
// references used in the Hypen DSL template.
type CounterState struct {
Count int `json:"count"`
}
func main() {
// 1. Define the module — .Name(...).Build() auto-registers onto the
// package-level core.App singleton.
counter := core.NewApp(CounterState{Count: 0}).
Name("Counter").
OnAction("increment", func(ctx core.TypedActionContext[CounterState]) {
ctx.State.Count++ // direct field mutation — type-safe
}).
OnAction("decrement", func(ctx core.TypedActionContext[CounterState]) {
ctx.State.Count--
}).
UI(`
Column {
Text("Count: @{state.count}")
.fontSize(32)
.fontWeight("bold")
Row {
Button("@actions.decrement") { Text("-") }.padding(16)
Button("@actions.increment") { Text("+") }.padding(16)
}
.gap(16)
}
.padding(32)
`).
Build()
// 2. Serve the app. WithDefinition picks the primary module; any other
// modules you built with .Name(...).Build() are auto-discovered from
// core.App as nested modules.
server := remote.NewRemoteServer().
WithDefinition(counter).
OnConnection(func(client *remote.Client) {
fmt.Printf("Connected: %s\n", client.ID)
}).
OnDisconnection(func(client *remote.Client) {
fmt.Printf("Disconnected: %s\n", client.ID)
}).
Listen(3000)
defer server.Stop()
fmt.Println("Server running on ws://localhost:3000")
select {} // Block forever
}Single-module demo? The lower-level
RemoteServer.WithState("Name", map[string]any{...}).OnAction(...).UI(...).Listen(port)form skips the typed builder and the registry — fine for one-off experiments.
Mutations made to ctx.State are diffed against a pre-handler snapshot when
the handler returns; only changed top-level keys are committed to the
underlying ObservableState, so the engine still sees minimal deltas.
Any Hypen client (web, Android, iOS) can connect to ws://localhost:3000 and render this UI. See the Remote App Tutorial for connecting clients.
Serve() Helper
For simpler cases, use the Serve() convenience function:
server := remote.Serve(remote.ServeOptions{
ModuleName: "Counter",
InitialState: map[string]any{"count": 0},
UI: `
Column {
Text("Count: @{state.count}")
Button { Text("+1") }
.onClick(@actions.increment)
}
`,
Port: 3000,
OnAction: func(action string, payload any, state map[string]any) map[string]any {
if action == "increment" {
state["count"] = state["count"].(int) + 1
}
return state
},
})
defer server.Stop()App Builder API
The Go SDK ships two builder APIs:
NewApp[T](recommended) — a generic, type-safe builder where module state is expressed as a Go struct. Handlers receive a*Tand mutate fields directly. This is the idiomatic Go API and the right default for all new code.NewAppBuilder(low-level) — the originalmap[string]any-based builder. Still supported for dynamic state shapes or when you don't know the state keys at compile time.NewApp[T]is a thin wrapper on top of it, so behaviour is identical.
Every method on both builders returns the builder for chaining:
module := core.NewApp(MyState{…}).
OnCreated(createdHandler).
OnAction("name", actionHandler).
OnDestroyed(destroyedHandler).
OnError(errorHandler).
UI(template)NewApp
func NewApp[T any](initial T, options ...*ModuleOptions) *TypedAppBuilder[T]initial— initial state as a Go struct. Fields should carryjson:"…"tags matching the names referenced from the DSL (@{state.xxx}). The struct is serialized to amap[string]anyviaencoding/json.options— optional module configuration (Name,Persist,Version).
NewAppBuilder (low-level)
func NewAppBuilder(initialState map[string]any, options *ModuleOptions, app ...*HypenApp) *AppBuilderinitialState— initial state map (required)options— module configuration (can benil)app— optionalHypenAppregistry for named module registration
OnCreated
Lifecycle hook called when the module is initialized. Mutations made to
state are committed after the handler returns:
.OnCreated(func(state *MyState, ctx core.GlobalContext) {
// Fetch initial data
data, _ := fetchData()
state.Items = data
// Access router
if ctx != nil {
if router := ctx.GetRouter(); router != nil {
router.Push("/home")
}
}
})OnAction
Registers a handler for a named action dispatched from the UI:
.OnAction("submit", func(ctx core.TypedActionContext[MyState]) {
// ctx.Action.Name - the action name
// ctx.Action.Payload - data from the UI (any)
// ctx.Action.Sender - component that dispatched the action
// ctx.State - *MyState — mutate fields directly
// ctx.Context - GlobalContext for cross-module access
if payload, ok := ctx.Action.Payload.(map[string]any); ok {
ctx.State.Title, _ = payload["title"].(string)
}
ctx.State.Submitted = true
})After the handler returns, the struct is diffed against a pre-handler
snapshot and only changed top-level keys are written back to the
underlying ObservableState, so the engine sees a minimal delta.
OnDestroyed
Lifecycle hook called when the module is unmounted:
.OnDestroyed(func(state *MyState, ctx core.GlobalContext) {
// Cleanup resources — mutations here are not observable,
// since the module is about to go away.
fmt.Println("Module destroyed")
})OnError
Catches errors (panics) from action handlers and lifecycle hooks:
.OnError(func(ctx core.ErrorContext) *core.ErrorHandlerResult {
if ctx.ActionName != "" {
fmt.Printf("Action %q failed: %v\n", ctx.ActionName, ctx.Error)
} else if ctx.Lifecycle != "" {
fmt.Printf("Lifecycle %q failed: %v\n", ctx.Lifecycle, ctx.Error)
}
return &core.ErrorHandlerResult{Handled: true}
})The ErrorContext fields:
| Field | Type | Description |
|---|---|---|
Error | error | The error or recovered panic |
State | *ObservableState | Current state (for inspection) |
ActionName | string | Set if error came from an action handler |
Lifecycle | string | "created" or "destroyed" if from a lifecycle hook |
Return values:
| Return | Effect |
|---|---|
nil | Default: log and emit global error event |
{Handled: true} | Suppress the error |
{Rethrow: true} | Re-panic the error |
UI / Build
UI(template) sets the inline Hypen DSL template and finalizes the module
definition in one call — the recommended way to create single-file
components. UIFile(path) does the same but reads the template from
disk. Build() finalizes without a template.
module := core.NewApp(MyState{Count: 0}).
OnAction("increment", handler).
UI(`Column { Text("@{state.count}") }`)
// Returns *ModuleDefinitionState Management
The SDK provides reactive ObservableState with automatic change tracking. State is accessed through dot-separated paths.
Creating State
state := core.NewObservableState(map[string]any{
"user": map[string]any{
"name": "Alice",
"email": "alice@example.com",
},
"items": []any{},
"loading": false,
}, &core.StateObserverOptions{
OnChange: func(change core.StateChange) {
fmt.Printf("Changed paths: %v\n", change.Paths)
},
})Reading State
// Get a value at a dot-separated path
name := state.Get("user.name").(string) // "Alice"
email := state.Get("user.email").(string) // "alice@example.com"
// Get entire state as a deep clone
all := state.GetAll() // map[string]any{...}
// Get a deep snapshot
snapshot := state.Snapshot()Writing State
// Set a value (triggers OnChange)
state.Set("user.name", "Bob")
// Set a nested path
state.Set("user.profile.avatar", "https://...")
// Replace entire state
state.SetAll(map[string]any{
"user": map[string]any{"name": "Charlie"},
"items": []any{},
})
// Delete a key
state.Delete("user.email")Batch Updates
Group multiple changes into a single notification:
// Using BatchUpdate helper
core.BatchStateUpdates(state, func() {
state.Set("user.name", "Dave")
state.Set("user.email", "dave@example.com")
state.Set("loading", false)
})
// Single OnChange notification with all paths
// Or manually
state.BeginBatch()
state.Set("a", 1)
state.Set("b", 2)
state.Set("c", 3)
state.EndBatch() // Triggers single notificationChange Tracking
type StateChange struct {
Paths []StatePath // Changed paths (e.g., ["user.name", "loading"])
NewValues map[StatePath]any // Map of path → new value
}Utility Functions
// Deep clone state
clone := core.GetStateSnapshot(state)
// Deep clone any value
value := core.DeepCloneAny(original)Action Handlers
Action handlers respond to UI interactions and modify state. With the
typed builder, mutate ctx.State fields directly; the diff against a
pre-handler snapshot is committed automatically on return.
Handler Context
// Typed builder (recommended)
type TypedActionContext[T any] struct {
Action ActionContext
State *T // mutate fields directly
Context GlobalContext
}
// Low-level untyped builder
type ActionHandlerContext struct {
Action ActionContext
State *ObservableState // string-keyed Get/Set
Context GlobalContext
}
type ActionContext struct {
Name string // Action name
Payload any // Data from the UI
Sender string // Component ID that dispatched the action
}Examples
Simple handler (typed):
type CounterState struct {
Count int `json:"count"`
}
.OnAction("increment", func(ctx core.TypedActionContext[CounterState]) {
ctx.State.Count++
})With payload:
type TodoState struct {
Items []TodoItem `json:"items"`
}
type TodoItem struct {
ID string `json:"id"`
Title string `json:"title"`
}
.OnAction("deleteItem", func(ctx core.TypedActionContext[TodoState]) {
payload, _ := ctx.Action.Payload.(map[string]any)
itemID, _ := payload["id"].(string)
filtered := ctx.State.Items[:0]
for _, item := range ctx.State.Items {
if item.ID != itemID {
filtered = append(filtered, item)
}
}
ctx.State.Items = filtered
})With navigation:
type AuthState struct {
IsLoggedIn bool `json:"isLoggedIn"`
Error string `json:"error"`
}
.OnAction("login", func(ctx core.TypedActionContext[AuthState]) {
payload, _ := ctx.Action.Payload.(map[string]any)
email, _ := payload["email"].(string)
password, _ := payload["password"].(string)
if authenticate(email, password) {
ctx.State.IsLoggedIn = true
if ctx.Context != nil {
ctx.Context.GetRouter().Push("/dashboard")
}
} else {
ctx.State.Error = "Invalid credentials"
}
})Named Modules
Register modules by name using HypenApp for component-based routing.
The typed builder does not auto-register with an HypenApp (that path
lives on the low-level AppBuilder), so with NewApp[T] you register
the returned *ModuleDefinition explicitly:
type HomeState struct {
Title string `json:"title"`
}
app := &core.HypenApp{}
homePage := core.NewApp(
HomeState{Title: "Home"},
&core.ModuleOptions{Name: "HomePage"},
).
OnCreated(func(state *HomeState, ctx core.GlobalContext) {
fmt.Println("HomePage mounted")
}).
UI(`Column { Text("@{state.title}") }`)
app.Register("HomePage", homePage)
// Query the registry
app.Has("HomePage") // true
app.Get("HomePage") // *ModuleDefinition
app.GetNames() // ["HomePage"]
app.Size() // 1
// Manual registration
app.Register("Settings", settingsModule)
app.Unregister("Settings")
app.Clear()The low-level app.Module("HomePage").DefineState(map[string]any{…}, nil)…
helper still exists and auto-registers when used through an HypenApp.
Routing
The HypenRouter provides URL-based navigation with pattern matching.
Basic Navigation
router := core.NewHypenRouter()
// Navigate
router.Push("/users/123")
router.Replace("/login")
router.Back()
router.Forward()Querying Routes
// Current path
router.GetCurrentPath() // "/users/123"
// Route parameters (from pattern matching)
router.GetParams() // map[string]string{"id": "123"}
// Query parameters
router.GetQuery() // map[string]string{"tab": "posts"}
// Full state snapshot
state := router.GetState()
// state.CurrentPath, state.Params, state.Query, state.PreviousPathPattern Matching
// Exact match
match := router.MatchPath("/users/:id", "/users/42")
// match.Params = {"id": "42"}
// Wildcard
match = router.MatchPath("/dashboard/*", "/dashboard/settings/profile")
// match.Path = "/dashboard/settings/profile"
// Check if pattern matches current path
router.IsActive("/users/:id") // true
// Build URL with query parameters
url := router.BuildURL("/search", map[string]string{"q": "hello", "page": "2"})
// "/search?page=2&q=hello"Listening for Changes
unsubscribe := router.OnNavigate(func(route core.RouteState) {
fmt.Printf("Navigated to: %s (from: %s)\n", route.CurrentPath, route.PreviousPath)
})
// Stop listening
unsubscribe()Auto-wire from the DSL
RemoteServer auto-discovers every Router { Route ... } block in
the primary template and child components, spins up a per-session
ManagedRouter, and wires the reserved @router.* namespace. DSL
authors dispatch nav directly:
Button { Text("Search") }.onClick(@router.push, to: "/search")
Button { Text("←") }.onClick(@router.back)Per-route module handlers use ctx.Context.GetRouter() to read
route params in OnActivated:
core.NewApp(UserProfileState{}).
Name("UserProfile").
OnActivated(func(state *UserProfileState, ctx core.GlobalContext) {
if ctx == nil { return }
r := ctx.GetRouter()
if r == nil { return }
if m := r.MatchPath("/users/:id", r.GetCurrentPath()); m != nil {
state.User = loadUser(m.Params["id"])
}
}).
Build()Opt out with server.DisableAutoRouter() to wire routing manually.
See the Routing Guide for the DSL-level
contract.
Managed Router (manual)
For bespoke wiring, construct a ManagedRouter directly:
managed := core.NewManagedRouter(router, engine, app, globalCtx).
AddRoute(core.RouteDefinition{
Path: "/",
Component: "HomePage",
}).
AddRoute(core.RouteDefinition{
Path: "/profile/:id",
Component: "ProfilePage",
}).
AddRoute(core.RouteDefinition{
Path: "/settings",
Module: settingsModule, // Direct module reference instead of registry
})
managed.Start()
// Navigate — ProfilePage module mounts automatically
router.Push("/profile/123")
active := managed.GetActiveModule()
activeRoute := managed.GetActiveRoute()
managed.Stop()Event System
Pub/sub messaging for decoupled communication:
emitter := core.CreateEventEmitter()
// Subscribe
unsubscribe := emitter.On("userLoggedIn", func(payload any) {
user := payload.(map[string]any)
fmt.Printf("User logged in: %s\n", user["name"])
})
// Emit
emitter.Emit("userLoggedIn", map[string]any{"name": "Alice"})
// Unsubscribe
unsubscribe()
// One-time listener (auto-unsubscribes after first call)
emitter.Once("ready", func(payload any) {
fmt.Println("Ready!")
})
// Query
emitter.ListenerCount("userLoggedIn") // int
emitter.EventNames() // []string
// Cleanup
emitter.RemoveAllListeners("userLoggedIn")
emitter.ClearAll()Built-in Framework Events
const (
EventModuleCreated = "module:created"
EventModuleDestroyed = "module:destroyed"
EventRouteChanged = "route:changed"
EventStateUpdated = "state:updated"
EventActionDispatched = "action:dispatched"
EventError = "error"
)Global Context
Cross-module communication using HypenGlobalContext:
globalCtx := core.NewHypenGlobalContext()
// Register modules
globalCtx.RegisterModule("counter", counterInstance)
globalCtx.RegisterModule("auth", authInstance)
// Access other modules
counterRef := globalCtx.GetModule("counter")
counterRef.State() // *ObservableState
counterRef.GetState() // map[string]any (snapshot)
counterRef.SetState(map[string]any{"count": 42}) // update state
// Query
globalCtx.HasModule("counter") // true
globalCtx.GetModuleIds() // ["counter", "auth"]
globalCtx.GetGlobalState() // combined state from all modules
// Router
globalCtx.SetRouter(router)
globalCtx.GetRouter()
// Events (via global context)
globalCtx.Emit("countChanged", 42)
unsub := globalCtx.On("countChanged", func(payload any) {
fmt.Printf("Count: %v\n", payload)
})
unsub()
// Typed event emitter
globalCtx.Events() // *TypedEventEmitter
// Debug info
info := globalCtx.Debug()
// { ModuleCount, ModuleIDs, HasRouter, EventCount }Component Loading
Discover and load .hypen components from the filesystem:
loader := core.NewComponentLoader()
// Register manually
loader.Register("Counter", counterModule, counterTemplate, "./components/Counter")
// Register with just module + template
loader.RegisterWithModule("Counter", counterModule, template)
// Load from a single component directory
err := loader.LoadFromDirectory("Counter", "./components/Counter")
// Expects component.hypen in the directory
// Auto-discover and load all components from a base directory
err = loader.LoadFromComponentsDir("./components")
// Scans for subdirectories containing component.hypen files
// Query
loader.Has("Counter") // true
loader.Get("Counter") // *ComponentDefinition
loader.GetNames() // []string
loader.GetAll() // []*ComponentDefinition
// Global instance
core.ComponentLoaderInstance.LoadFromComponentsDir("./components")Component Resolver
Resolve imports from Hypen DSL templates:
resolver := core.NewComponentResolver(&core.ResolverOptions{
BaseDir: "./components",
Cache: true,
App: app, // HypenApp registry for lookup
})
// Parse imports from DSL
imports := core.ParseImports(`
import { Header, Footer } from "./shared"
import MainContent from "./pages/main"
`)
// Resolve imports
for _, stmt := range imports {
resolved, err := resolver.Resolve(stmt)
// resolved: map[string]*ResolvedComponent
}
// Remove imports from DSL (for engine rendering)
cleanDSL := core.RemoveImports(dslWithImports)
// Cache management
resolver.ClearCache()
resolver.GetCacheSize()RemoteServer API
RemoteServer streams UI over WebSocket to any Hypen client using a fluent builder pattern:
server := remote.NewRemoteServer().
Module("MyApp", moduleConfig).
UI(uiTemplate).
Config(remote.ServerConfig{Port: 3000, Hostname: "0.0.0.0"}).
OnConnection(connectHandler).
OnDisconnection(disconnectHandler).
Listen(3000)Builder Methods
| Method | Description |
|---|---|
.Module(name, config) | Set the module to serve |
.WithState(name, state) | Quick setup with just name and state |
.OnAction(handler) | Set action handler (simple mode) |
.UI(dsl) | Set the Hypen DSL template |
.Config(config) | Server configuration (port, hostname) |
.OnConnection(callback) | Called when a client connects |
.OnDisconnection(callback) | Called when a client disconnects |
.Listen(port?) | Start the WebSocket server |
.Stop() | Stop the server |
Server Methods
server.GetURL() // WebSocket URL string
server.GetClientCount() // number of active connections
// Send to specific client
server.SendPatches(conn, patches)
server.UpdateClientState(conn, state)
// Broadcast to all clients
server.Broadcast(msg)
server.BroadcastPatches(patches)
server.BroadcastState(state)Configuration
remote.ServerConfig{
Port: 3000, // default: 3000
Hostname: "0.0.0.0", // default: "0.0.0.0"
}RemoteEngine (Client)
Connect to a Hypen server from Go:
client := remote.NewRemoteEngine("ws://localhost:3000/ws", &remote.EngineOptions{
AutoReconnect: true,
ReconnectInterval: 3 * time.Second,
MaxReconnectAttempts: 10,
}).
OnPatches(func(patches []remote.Patch) {
for _, p := range patches {
fmt.Printf("Patch: %s %s\n", p.Type, p.ID)
}
}).
OnStateUpdate(func(state any) {
fmt.Printf("State: %v\n", state)
}).
OnConnect(func() {
fmt.Println("Connected!")
}).
OnDisconnect(func() {
fmt.Println("Disconnected")
}).
OnError(func(err error) {
fmt.Printf("Error: %v\n", err)
})
err := client.Connect()
defer client.Disconnect()
// Dispatch actions to the server
client.DispatchAction("increment", map[string]any{"step": 5})
// Query
client.GetConnectionState() // "connected" | "connecting" | "disconnected" | "error"
client.GetCurrentState() // current state from server
client.GetRevision() // current revision numberWebSocket Protocol
Client → Server Messages
| Type | Fields | Description |
|---|---|---|
dispatchAction | module, action, payload? | Dispatch action to module |
Server → Client Messages
| Type | Fields | Description |
|---|---|---|
initialTree | module, state, patches, revision | Full UI tree on connect |
patch | module, patches, revision | Incremental UI updates |
stateUpdate | module, state, revision | Full state snapshot |
WASM Engine
Use the WASM engine for server-side rendering with the full Hypen parser and reconciler:
engine, err := core.NewWasmEngine(core.WasmEngineConfig{
WasmPath: "./hypen_engine.wasm",
Primitives: []string{"Text", "Column", "Row", "Button", "Input", "Image"},
})
if err != nil {
log.Fatal(err)
}
defer engine.Close()
// Register a component
engine.RegisterComponent("Header", headerDSL, "/components/Header")
// Set up module
engine.SetModule("Counter", []string{"increment"}, []string{"count"}, map[string]any{"count": 0})
// Register action handler
engine.OnAction("increment", func(action core.Action) {
// Handle action
})
// Set render callback
engine.SetPatchCallback(func(patches []core.Patch) {
// Send patches to clients
})
// Render
patches, err := engine.RenderSource(`
Column {
Text("Hello, world!")
}
`)
// Update state and get new patches
patches, err = engine.UpdateState(map[string]any{"count": 1})
// Document rendering with import resolution
patches, err = engine.RenderDocument(source, resolver)
// Debug
jsonAST, err := engine.ParseToJSON(source)
rev := engine.GetRevision()
engine.ClearTree()WASM Configuration
type WasmEngineConfig struct {
WasmPath string // Path to hypen_engine.wasm file
WasmBytes []byte // Alternative: raw WASM bytes (skips file read)
Primitives []string // Element types to register
}Error Types
The SDK provides typed error classes:
// Check error type
if core.IsEngineError(err, core.ErrParse) {
fmt.Println("DSL parsing failed:", err)
}
// Error codes
core.ErrParse // DSL parsing failed
core.ErrComponentNotFound // Component not in registry
core.ErrRender // Rendering/reconciliation failed
core.ErrActionNotFound // No handler for action
core.ErrState // State operation failed
core.ErrExpression // Expression evaluation failed
core.ErrNotInitialized // Engine not initializedTesting
MockEngine
Test modules without the WASM engine:
type CounterState struct {
Count int `json:"count"`
}
func TestCounter(t *testing.T) {
engine := core.NewMockEngine()
counter := core.NewApp(CounterState{Count: 0}).
OnAction("increment", func(ctx core.TypedActionContext[CounterState]) {
ctx.State.Count++
}).
Build()
instance := core.NewModuleInstance(engine, counter)
// Dispatch action
engine.TriggerAction("increment", nil)
// Verify state — snapshot values round-trip as float64 because the
// underlying state is JSON-shaped map[string]any.
count, _ := instance.GetState()["count"].(float64)
if int(count) != 1 {
t.Errorf("Expected count=1, got %d", int(count))
}
// Inspect recorded changes
changes := engine.GetStateChanges()
actions := engine.GetDispatchedActions()
// Check registrations
engine.HasAction("increment") // true
engine.GetRegisteredActions() // ["increment"]
// Reset for next test
engine.Reset()
}TestRenderer
Capture render events for assertions:
func TestRendering(t *testing.T) {
renderer := core.NewTestRenderer()
patches := []core.Patch{
{Type: core.PatchCreate, ID: "1", ElementType: "Column"},
{Type: core.PatchCreate, ID: "2", ElementType: "Text"},
{Type: core.PatchSetText, ID: "2", Text: "Hello"},
{Type: core.PatchInsert, ParentID: "1", ID: "2"},
}
renderer.ApplyPatches(patches)
// Inspect events
events := renderer.Events
if len(events) != 4 {
t.Errorf("Expected 4 events, got %d", len(events))
}
// Check event types
types := renderer.EventTypes()
// ["create", "create", "setText", "insert"]
// Clear for next test
renderer.ClearEvents()
}ConsoleRenderer
Print patches to stdout for debugging:
renderer := core.NewConsoleRenderer()
renderer.ApplyPatches(patches)
// Prints each patch to consoleLogging
Configure logging for debugging:
// Enable debug mode (verbose output)
core.SetDebugMode(true)
// Set global log level
core.SetLogLevel(core.LogLevelDebug) // Debug, Info, Warn, Error, None
// Custom output
core.SetLogOutput(logFile)
// Check mode
core.IsDebugMode()
core.GetLogLevel()Framework subsystems have individual loggers:
// Available loggers
core.LogEngine // Engine operations
core.LogRouter // Navigation events
core.LogState // State mutations
core.LogEvents // Event emissions
core.LogRemote // Remote server/client
core.LogRenderer // Patch application
core.LogModule // Module lifecycle
core.LogLifecycle // Lifecycle hooks
core.LogLoader // Component loading
core.LogContext // Global context
core.LogDiscovery // Component discovery
// Per-logger level
core.LogState.SetLevel(core.LogLevelWarn)
// Child loggers
childLog := core.LogEngine.Child("WASM")
childLog.Debug("Initializing WASM runtime")Deployment
Docker
FROM golang:1.21-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM alpine:latest
WORKDIR /app
COPY --from=build /app/server .
COPY --from=build /app/hypen_engine.wasm .
COPY --from=build /app/components ./components
EXPOSE 3000
CMD ["./server"]Production Checklist
- Include the
hypen_engine.wasmfile in your deployment - Each client gets its own module instance — plan memory accordingly
- Use
defer server.Stop()for graceful shutdown - Monitor active connections via
server.GetClientCount() - Set appropriate
MaxConnectionsin server options for capacity planning - Add authentication at the HTTP layer before WebSocket upgrade
Requirements
- Go 1.21+
gorilla/websocket(WebSocket support)tetratelabs/wazero(WASM runtime, no CGo required)
See Also
- TypeScript SDK — TypeScript server SDK with sessions and component discovery
- Remote App Tutorial — Step-by-step guide to building a remote app
- Error Handling — Error recovery patterns across SDKs
- Web Adapter — Client-side rendering for browsers