Reactive Web Framework (RWeb)
RWeb is Nostos's built-in reactive web framework that enables you to build interactive web applications with automatic UI updates. It combines reactive records, WebSocket communication, and server-side rendering to create a seamless developer experience without requiring a separate frontend framework.
What Makes RWeb Special?
- Process per Session: Each browser session gets its own Nostos process with isolated state
- Automatic Updates: Changes to reactive records automatically update the browser UI via WebSocket
- Server-Side Logic: All application logic runs on the server - no JavaScript framework needed
- Component-Based: Define reusable components that track their dependencies and re-render efficiently
Prerequisites
Before learning RWeb, make sure you understand:
- Reactive Records - The foundation of RWeb state management
- HTML Templating - The rhtml module for building HTML
Getting Started: A Simple Counter
Let's start with the classic counter example. This demonstrates the core concepts of RWeb:
use stdlib.rweb
use stdlib.rhtml
# Define reactive state - each session gets its own instance
reactive Counter = { count: Int }
# Session function - called once per browser session
session() = {
counter = Counter(count: 0)
# Return tuple: (render function, action handler)
(
# Render function - returns the UI
() => RHtml(div([
h1("Simple Counter"),
p("Count: " ++ show(counter.count)),
button("+1", dataAction: "increment"),
button("-1", dataAction: "decrement"),
button("Reset", dataAction: "reset")
])),
# Action handler - processes user interactions
(action, params) => match action {
"increment" -> counter.count = counter.count + 1
"decrement" -> counter.count = counter.count - 1
"reset" -> counter.count = 0
_ -> ()
}
)
}
main() = {
println("Starting counter app on port 8080...")
startRWeb(8080, "Counter App", session)
}
Run this with ./target/release/nostos counter.nos and open http://localhost:8080 in your browser.
Running RWeb Applications
RWeb provides several functions for starting servers, designed for different use cases:
| Function | Behavior | Use Case |
|---|---|---|
runRWeb |
Auto-detects mode: background in TUI, foreground in scripts | Recommended - works everywhere |
startRWeb |
Blocks forever serving requests | Scripts that run as servers |
restartRWebBackground |
Runs in background, auto-kills previous server | TUI/REPL development with hot-reload |
startRWebBackground |
Runs in background, returns Pid | Manual process management |
Smart Mode with runRWeb
The runRWeb function automatically chooses the right behavior based on context:
use stdlib.rweb
use stdlib.rhtml
reactive Counter = { count: Int }
session() = {
counter = Counter(count: 0)
(
() => RHtml(div([
h1("Counter: " ++ show(counter.count)),
button("+1", dataAction: "inc")
])),
(action, _) => match action {
"inc" -> counter.count = counter.count + 1
_ -> ()
}
)
}
# This single line works in both script and TUI/REPL!
main() = runRWeb(8080, "Counter", session)
How runRWeb Works
- In TUI/REPL: Uses
restartRWebBackground- runs in background, auto-kills previous server when you re-run - In scripts: Uses
startRWeb- blocks and serves requests until killed - Detection: Uses
Env.isInteractive()to check if running in TUI
Hot-Reload Development Workflow
When developing in the TUI, you can modify your code and re-run to see changes instantly:
- Open your RWeb file in the TUI editor
- Run with Ctrl+R
- Open
http://localhost:8080in your browser - Edit code, press Ctrl+R again
- Refresh browser - previous server is auto-killed, new one starts
Custom Mode Detection
You can use Env.isInteractive() for custom logic:
# Check if running in TUI/REPL
main() = {
if Env.isInteractive() then {
println("Running in TUI - starting background server")
pid = restartRWebBackground(8080, "Dev Server", session)
println("Server running, pid stored for restart")
pid
} else {
println("Running as script - blocking mode")
startRWeb(8080, "Production Server", session)
}
}
How It Works
- When a browser connects, RWeb spawns a new process and calls your
session()function - The render function is called to generate the initial HTML
- When the user clicks a button with
dataAction, a WebSocket message is sent to the server - Your action handler receives the action name and modifies the reactive record
- RWeb detects the change and sends an updated HTML fragment back to the browser
Understanding the Session Pattern
The session() function is the heart of an RWeb application. It returns a tuple containing two functions:
# The session function signature
session: () -> (() -> RHtml | RHtmlResult, (String, Map[String, String]) -> ())
# Breaking it down:
# 1. render: () -> RHtml | RHtmlResult
# - Takes no arguments
# - Returns HTML (either simple RHtml or component-based RHtmlResult)
#
# 2. handler: (action, params) -> ()
# - action: String - the action name from dataAction/dataOnchange/etc
# - params: Map[String, String] - parameters sent with the action
# - Returns nothing (side effect: modifies reactive records)
The key insight is that your reactive record (like Counter) is captured in the closure. When the action handler modifies it, RWeb automatically detects the change and re-renders the affected parts of the UI.
Using Components for Efficient Updates
Simple RWeb apps re-render the entire page on every change. For larger apps, use components to update only the parts that changed:
use stdlib.rweb
use stdlib.rhtml
reactive State = { counter: Int, message: String }
session() = {
state = State(counter: 0, message: "Hello!")
(
() => RHtml(div([
h1("Component Demo"),
# This component only re-renders when state.counter changes
component("counter-display", () => RHtml(div([
h3("Counter"),
p("Value: " ++ show(state.counter)),
button("+", dataAction: "inc"),
button("-", dataAction: "dec")
]))),
# This component only re-renders when state.message changes
component("message-display", () => RHtml(div([
h3("Message"),
p(state.message),
button("Change", dataAction: "changeMsg")
])))
])),
(action, params) => match action {
"inc" -> state.counter = state.counter + 1
"dec" -> state.counter = state.counter - 1
"changeMsg" -> state.message = "Updated at " ++ show(state.counter)
_ -> ()
}
)
}
main() = startRWeb(8080, "Component Demo", session)
The component(id, renderFn) function:
- id: A unique identifier for this component (becomes the HTML element's id)
- renderFn: A function that returns RHtml for this component
- Dependency tracking: RWeb tracks which reactive fields each component reads
- Selective updates: Only components affected by a change are re-rendered
Form Handling
RWeb supports various form elements with automatic WebSocket communication. Use named parameters to configure element behavior:
Form Submission
use stdlib.rweb
use stdlib.rhtml
reactive FormData = { name: String, email: String }
session() = {
data = FormData(name: "", email: "")
(
() => RHtml(div([
h1("Registration Form"),
# Display submitted data
p("Name: " ++ data.name),
p("Email: " ++ data.email),
# The form with dataAction triggers on submit
form([
div([
label("Name: "),
input(inputType: "text", name: "name", placeholder: "Your name")
]),
div([
label("Email: "),
input(inputType: "email", name: "email", placeholder: "you@example.com")
]),
button("Submit", btnType: "submit")
], dataAction: "submitForm")
])),
(action, params) => match action {
"submitForm" -> {
# Form data is automatically collected into params
data.name = Map.get(params, "name")
data.email = Map.get(params, "email")
}
_ -> ()
}
)
}
main() = startRWeb(8080, "Form Demo", session)
Note: When a form with dataAction is submitted, RWeb automatically collects all form field values into the params map, keyed by the input's name attribute.
Checkboxes and Radio Buttons
use stdlib.rweb
use stdlib.rhtml
reactive Prefs = { darkMode: Bool, newsletter: Bool }
session() = {
prefs = Prefs(darkMode: false, newsletter: true)
(
() => RHtml(div([
h1("Preferences"),
p("Dark Mode: " ++ if prefs.darkMode then "ON" else "OFF"),
p("Newsletter: " ++ if prefs.newsletter then "Subscribed" else "Unsubscribed"),
div([
input(inputType: "checkbox", name: "darkMode", dataOnchange: "toggleDark"),
label(" Enable Dark Mode")
]),
div([
input(inputType: "checkbox", name: "newsletter", dataOnchange: "toggleNews"),
label(" Subscribe to Newsletter")
])
])),
(action, params) => match action {
"toggleDark" -> {
# params.checked is "true" or "false" for checkboxes
checked = Map.get(params, "checked")
prefs.darkMode = checked == "true"
}
"toggleNews" -> {
checked = Map.get(params, "checked")
prefs.newsletter = checked == "true"
}
_ -> ()
}
)
}
main() = startRWeb(8080, "Checkbox Demo", session)
Select Dropdowns
use stdlib.rweb
use stdlib.rhtml
reactive Selection = { country: String }
session() = {
sel = Selection(country: "")
(
() => RHtml(div([
h1("Country Selector"),
p("Selected: " ++ if sel.country == "" then "None" else sel.country),
select([
option("Choose a country...", value: ""),
option("United States", value: "us"),
option("United Kingdom", value: "uk"),
option("Canada", value: "ca"),
option("Australia", value: "au")
], name: "country", dataOnchange: "countryChanged")
])),
(action, params) => match action {
"countryChanged" -> sel.country = Map.get(params, "value")
_ -> ()
}
)
}
main() = startRWeb(8080, "Select Demo", session)
Real-Time Input (oninput)
For instant feedback as the user types, use dataOninput instead of dataOnchange:
use stdlib.rweb
use stdlib.rhtml
reactive Search = { query: String }
session() = {
search = Search(query: "")
(
() => RHtml(div([
h1("Live Search"),
input(inputType: "text", placeholder: "Type to search...", dataOninput: "search"),
p("Searching for: " ++ search.query),
p("Results: " ++ show(String.length(search.query)) ++ " characters typed")
])),
(action, params) => match action {
"search" -> search.query = Map.get(params, "value")
_ -> ()
}
)
}
main() = startRWeb(8080, "Live Search", session)
Event Attributes Reference
| Attribute | Elements | Params Sent | Use Case |
|---|---|---|---|
dataAction |
button, a, div, form | Click: data-param-* attrs Form: all input values |
Buttons, links, form submission |
dataOnchange |
input, select, textarea | value, checked (for checkbox/radio) | Form field changes (on blur or selection) |
dataOninput |
input, textarea | value | Real-time input feedback (every keystroke) |
Form Validation
RWeb supports a layered validation approach for robust form handling:
HTML5 Client-Side Validation
Form inputs support HTML5 validation attributes for instant client-side feedback:
# HTML5 validation attributes on form inputs
form([
# Required field with minimum length
input(inputType: "text", name: "username", required: "true", minLength: "3", maxLength: "20"),
# Email validation
input(inputType: "email", name: "email", required: "true"),
# Number with range
input(inputType: "number", name: "age", min: "18", max: "120"),
# Pattern matching (regex)
input(inputType: "text", name: "phone", pattern: "[0-9]{10}", placeholder: "10 digits"),
button("Submit", btnType: "submit")
], dataAction: "register")
Tip: Use novalidate: "novalidate" on the form to disable HTML5 validation for server-side only validation.
Server-Side Validation
For defense in depth, use the stdlib.validation library for server-side validation:
use stdlib.validation.{validate, required, minLength, maxLength, email, minValue, maxValue, Valid, Invalid, firstError}
# Define validation rules
makeRules() = {
r1 = Map.insert(%{}, "username", [required(), minLength(3), maxLength(20)])
r2 = Map.insert(r1, "email", [required(), email()])
Map.insert(r2, "age", [required(), minValue(18), maxValue(120)])
}
# In your action handler:
(action, params) => match action {
"register" -> {
result = validate(params, makeRules())
match result {
Valid(data) -> {
state.success = true
state.username = Map.get(data, "username")
}
Invalid(errors) -> {
state.error = firstError(result)
}
}
}
_ -> ()
}
Available Validators
| Validator | Description |
|---|---|
required() |
Field must not be empty |
minLength(n) |
Minimum string length |
maxLength(n) |
Maximum string length |
email() |
Must contain @ and . |
minValue(n) |
Minimum numeric value |
maxValue(n) |
Maximum numeric value |
range(min, max) |
Value must be within range |
matchesField(field, params) |
Must match another field (e.g., password confirmation) |
custom(predicate, msg) |
Custom validation with predicate function |
Multi-Route Applications
For applications with multiple pages, use startRWebWithRoutes with resolver functions:
use stdlib.rweb.{startRWebWithRoutes}
use stdlib.rhtml.{div, h1, p, button, a}
use stdlib.server.{respondHtml, respondJson}
reactive HomeState = { count: Int }
reactive DashState = { total: Int }
# Reactive session for home page
# Receives writerId (for external push) and cookies from HTTP request
homeSession(writerId, cookies) = {
state = HomeState(count: 0)
(
() => RHtml(div([
h1("Home"),
p("Counter: " ++ show(state.count)),
button("+", dataAction: "inc"),
a("Go to Dashboard", href: "/dashboard")
])),
(action, _) => match action {
"inc" -> state.count = state.count + 1
_ -> ()
}
)
}
# Reactive session for dashboard
dashSession(writerId, cookies) = {
state = DashState(total: 100)
(
() => RHtml(div([
h1("Dashboard"),
p("Total: $" ++ show(state.total)),
button("Add $10", dataAction: "add10"),
a("Go to Home", href: "/")
])),
(action, _) => match action {
"add10" -> state.total = state.total + 10
_ -> ()
}
)
}
# Reactive resolver: path -> Option[sessionFn]
reactiveResolver(path) = match path {
"/" -> Some(homeSession)
"/dashboard" -> Some(dashSession)
_ -> None()
}
# Plain resolver: path -> Option[handler]
# For static HTML, API endpoints, etc.
plainResolver(path) = match path {
"/about" -> Some(req => respondHtml(req, "<h1>About Us</h1>"))
"/api/health" -> Some(req => respondJson(req, "{\"status\":\"ok\"}"))
_ -> None()
}
main() = {
println("Starting multi-route app on port 8080...")
startRWebWithRoutes(8080, "Multi-Route App", reactiveResolver, plainResolver)
}
Resolver Functions
reactiveResolver(path) -> Option[(writerId, cookies) -> Session]- Returns a session setup functionplainResolver(path) -> Option[Request -> ()]- Returns a plain HTTP handler- Session functions receive
writerId(Int) andcookies(List[(String, String)]) - Return
None()for paths you don't handle
Static File Serving
For applications that need to serve CSS, JavaScript, images, and other static assets, use startRWebFull:
use stdlib.rweb.{startRWebFull}
use stdlib.rhtml.{div, h1, link, script, img}
use stdlib.server.{respondJson}
reactive AppState = { theme: String }
appSession(writerId, cookies) = {
state = AppState(theme: "light")
(
() => RHtml(div([
# Link to static CSS file
link(href: "/static/css/style.css", rel: "stylesheet"),
h1("My App"),
img(src: "/static/images/logo.png", alt: "Logo"),
p("Current theme: " ++ state.theme),
button("Toggle Theme", dataAction: "toggle"),
# Link to static JavaScript
script(src: "/static/js/app.js")
])),
(action, _) => match action {
"toggle" -> state.theme = if state.theme == "light" then "dark" else "light"
_ -> ()
}
)
}
reactiveResolver(path) = match path {
"/" -> Some(appSession)
_ -> None()
}
plainResolver(path) = match path {
"/api/status" -> Some(req => respondJson(req, "{\"ok\":true}"))
_ -> None()
}
# Static file configuration: list of (urlPrefix, directory) tuples
staticConfig = [
("/static", "./public"), # /static/css/style.css -> ./public/css/style.css
("/assets", "./assets") # /assets/logo.png -> ./assets/logo.png
]
main() = {
startRWebFull(8080, "My App", reactiveResolver, plainResolver, staticConfig)
}
Request Routing Order
When a request comes in, RWeb checks handlers in this order:
- Reactive pages - WebSocket-enabled pages with live updates
- Plain handlers - API endpoints, static HTML pages
- Static files - CSS, JS, images from configured directories
- 404 fallback - If nothing matches
Supported MIME Types
Static files are served with correct Content-Type headers:
- Text: .html, .css, .js, .json, .svg, .txt, .xml
- Images: .png, .jpg, .jpeg, .gif, .webp, .ico
- Fonts: .woff, .woff2, .ttf, .otf, .eot
- Media: .mp3, .mp4, .webm
- Other: .pdf, .zip, .wasm
Cookie Handling
RWeb passes cookies from the HTTP request to your session setup function. This enables authentication flows and persistent user preferences:
use stdlib.rweb.{startRWebWithRoutes}
use stdlib.rhtml.{div, h1, p, button, form, input}
use stdlib.server.{getCookie, hasCookie, setCookie, setCookieWithAge, clearCookie, respondHtmlWith, redirect, redirectWith}
reactive UserState = { username: String, loggedIn: Bool }
# Check authentication on session start
appSession(writerId, cookies) = {
# Read session token from cookies
sessionToken = getCookie(cookies, "session")
isLoggedIn = sessionToken != ""
state = UserState(
username: if isLoggedIn then "User" else "",
loggedIn: isLoggedIn
)
(
() => RHtml(div([
h1("Welcome!"),
if state.loggedIn then
div([
p("Hello, " ++ state.username),
button("Logout", dataAction: "logout")
])
else
form([
input(inputType: "text", name: "username", placeholder: "Username"),
button("Login", btnType: "submit")
], dataAction: "login")
])),
(action, params) => match action {
"login" -> {
state.username = Map.get(params, "username")
state.loggedIn = true
}
"logout" -> {
state.loggedIn = false
state.username = ""
}
_ -> ()
}
)
}
reactiveResolver(path) = match path {
"/" -> Some(appSession)
_ -> None()
}
# Plain resolver for login/logout that sets cookies
plainResolver(path) = match path {
"/auth/login" -> Some(req => {
# In real app: validate credentials, create session
cookie = setCookieWithAge("session", "abc123", 86400) # 24 hours
redirectWith(req, "/", [cookie])
})
"/auth/logout" -> Some(req => {
cookie = clearCookie("session")
redirectWith(req, "/", [cookie])
})
_ -> None()
}
main() = startRWebWithRoutes(8080, "Auth Demo", reactiveResolver, plainResolver)
Cookie Helper Functions (from stdlib.server)
getCookie(cookies, name)- Get cookie value (returns "" if not found)hasCookie(cookies, name)- Check if cookie existssetCookie(name, value)- Create Set-Cookie header (HttpOnly, Path=/)setCookieWithAge(name, value, maxAge)- With max age in secondssetSecureCookie(name, value, maxAge)- HTTPS only with SameSite=StrictclearCookie(name)- Delete cookie (Max-Age=0)
Setting Cookies via WebSocket
For simpler flows, you can set cookies directly from within your reactive action handler using rwebSetCookie. This sends a WebSocket message that tells the browser to set the cookie via JavaScript:
use stdlib.rweb.{startRWeb, rwebSetCookie, rwebClearCookie}
use stdlib.rhtml.{div, h1, p, button, form, input}
use stdlib.server.{getCookie}
reactive AppState = { username: String, loggedIn: Bool }
appSession(writerId, cookies) = {
# Read cookie on page load
savedUser = getCookie(cookies, "user")
state = AppState(
username: if savedUser != "" then savedUser else "",
loggedIn: savedUser != ""
)
(
() => RHtml(div([
h1("Login Demo"),
if state.loggedIn then
div([
p("Hello, " ++ state.username ++ "!"),
button("Logout", dataAction: "logout")
])
else
form([
input(inputType: "text", name: "username", placeholder: "Username"),
button("Login", btnType: "submit")
], dataAction: "login")
])),
(action, params) => match action {
"login" -> {
username = Map.get(params, "username")
# Set cookie via WebSocket - browser executes document.cookie
rwebSetCookie(writerId, "user", username, 3600) # 1 hour
state.username = username
state.loggedIn = true
}
"logout" -> {
# Clear cookie via WebSocket
rwebClearCookie(writerId, "user")
state.username = ""
state.loggedIn = false
}
_ -> ()
}
)
}
main() = startRWeb(8080, "Login Demo", appSession)
WebSocket Cookie Functions (from stdlib.rweb)
rwebSetCookie(writerId, name, value, maxAge)- Set cookie (maxAge in seconds)rwebSetSecureCookie(writerId, name, value, maxAge)- HTTPS with SameSite=StrictrwebClearCookie(writerId, name)- Delete cookie
Security Note
Cookies set via rwebSetCookie are not HttpOnly (they're set via JavaScript). For sensitive authentication tokens, use the HTTP redirect approach with setCookieWithAge instead, which creates HttpOnly cookies that JavaScript cannot access.
Complete Secure Auth Example
For a production-ready authentication pattern with HttpOnly cookies and server-side sessions, see:
examples/rweb_secure_auth.nos
Features: HttpOnly cookies, server-side session storage, login/logout flow, protected routes, credential validation.
Passing Data via Action Parameters
You can pass data from buttons using data-param-* attributes:
use stdlib.rweb
use stdlib.rhtml
reactive State = { selected: String }
session() = {
state = State(selected: "none")
(
() => RHtml(div([
h1("Item Selector"),
p("Selected: " ++ state.selected),
# Each button passes its item ID
button("Apple", dataAction: "select", attrs: [("data-param-item", "apple")]),
button("Banana", dataAction: "select", attrs: [("data-param-item", "banana")]),
button("Cherry", dataAction: "select", attrs: [("data-param-item", "cherry")])
])),
(action, params) => match action {
"select" -> state.selected = Map.get(params, "item")
_ -> ()
}
)
}
main() = startRWeb(8080, "Item Selector", session)
Complete Example: Todo Application
Here's a complete todo application demonstrating multiple reactive records, components, and various interactions:
use stdlib.rweb
use stdlib.rhtml
use stdlib.list
reactive Todo = { id: Int, text: String, done: Bool }
reactive AppState = {
todos: List[Todo],
nextId: Int,
filter: String # "all", "active", "done"
}
# Helper to create a todo item element
todoItem(todo: Todo) = div([
span(
todo.text ++ if todo.done then " [DONE]" else "",
class: if todo.done then "line-through text-slate-500" else ""
),
button("Toggle", dataAction: "toggle", attrs: [("data-param-id", show(todo.id))]),
button("Delete", dataAction: "delete", attrs: [("data-param-id", show(todo.id))])
], class: "flex gap-2 items-center p-2 border-b border-slate-700")
session() = {
state = AppState(todos: [], nextId: 1, filter: "all")
# Filter todos based on current filter setting
filteredTodos() = match state.filter {
"active" -> state.todos.filter(t => !t.done)
"done" -> state.todos.filter(t => t.done)
_ -> state.todos
}
(
() => RHtml(div([
h1("Todo App"),
# Add todo form
form([
input(inputType: "text", name: "text", placeholder: "What needs to be done?"),
button("Add", btnType: "submit")
], dataAction: "addTodo"),
# Stats component
component("stats", () => RHtml(div([
p("Total: " ++ show(List.length(state.todos))),
p("Done: " ++ show(List.length(state.todos.filter(t => t.done))))
]))),
# Filter buttons
div([
button("All", dataAction: "setFilter", attrs: [("data-param-filter", "all")]),
button("Active", dataAction: "setFilter", attrs: [("data-param-filter", "active")]),
button("Done", dataAction: "setFilter", attrs: [("data-param-filter", "done")]),
p("Filter: " ++ state.filter)
]),
# Todo list component
component("todo-list", () => RHtml(div(
filteredTodos().map(todoItem)
)))
])),
(action, params) => match action {
"addTodo" -> {
text = Map.get(params, "text")
if String.length(text) > 0 then {
newTodo = Todo(id: state.nextId, text: text, done: false)
state.todos = state.todos ++ [newTodo]
state.nextId = state.nextId + 1
} else ()
}
"toggle" -> {
id = parseInt(Map.get(params, "id"))
state.todos = state.todos.map(t =>
if t.id == id then { t.done = !t.done; t } else t
)
}
"delete" -> {
id = parseInt(Map.get(params, "id"))
state.todos = state.todos.filter(t => t.id != id)
}
"setFilter" -> state.filter = Map.get(params, "filter")
_ -> ()
}
)
}
main() = {
println("Starting Todo app on port 8080...")
startRWeb(8080, "Todo App", session)
}
Best Practices
1. Use Components for Large UIs
Wrap sections of your UI in component() to enable targeted updates. Each component tracks its dependencies and only re-renders when those specific reactive fields change.
2. Keep Action Handlers Simple
Action handlers should just update state. Put complex logic in helper functions. The handler runs synchronously, so keep it fast.
3. Use Meaningful Component IDs
Component IDs become HTML element IDs. Use descriptive names like "user-profile" or "cart-summary" rather than generic names.
4. Separate Concerns with Multiple Records
Use separate reactive records for different parts of your state. This makes dependencies clearer and can improve update efficiency.
External Push: Server-Side Updates
RWeb supports pushing updates to connected clients from external processes. This is useful for:
- Real-time notifications from background workers
- Broadcasting updates to specific sessions
- Integrating with external systems (message queues, webhooks, etc.)
How It Works
When a client connects, RWeb splits the WebSocket into separate read/write handles. The writerId is logged and can be used by any process to push messages:
Push Functions
Three helper functions are available for pushing content:
# Push a full page update (replaces entire rweb-root content)
rwebPushFull(writerId, "<h1>New Content</h1>")
# Push a component update (updates specific component by ID)
rwebPushComponent(writerId, "notifications", "<span>You have 3 new messages</span>")
# Push raw JSON message (for custom client-side handling)
rwebPushRaw(writerId, "{\"type\":\"custom\",\"data\":\"hello\"}")
Example: Background Notification Pusher
Here's an example that sends notifications to a connected session from a background process:
use stdlib.rweb
use stdlib.rhtml
# Background worker that pushes notifications
notificationWorker(writerId) = {
sleep(5000) # Wait 5 seconds
rwebPushComponent(writerId, "alerts",
"<div class='alert'>New notification from server!</div>")
notificationWorker(writerId) # Loop forever
}
# Main session with an alerts component
session() = {
state = State(message: "Waiting...")
(
() => RHtml(div([
h1("Push Demo"),
component("alerts", () => RHtml(
div("No alerts yet", class: "alerts")
)),
p(state.message)
])),
(action, params) => ()
)
}
# Note: In practice, you'd capture writerId from session logs
# or pass it via inter-process communication
📁 Full Example
See examples/rweb_external_push.nos for a complete working example demonstrating external push capabilities.
Low-Level API
For advanced use cases, you can use the WebSocket builtins directly:
# Split a WebSocket into read/write handles
splitResult = WebSocket.split(ws)
requestId = splitResult.requestId # For receiving messages
writerId = splitResult.writerId # For sending (shareable)
# Send via shared writer (thread-safe, can be called from any process)
WebSocket.sendShared(writerId, "Hello from another process!")
Testing RWeb Applications
RWeb applications can be tested using the Selenium WebDriver bindings. See the Selenium chapter for details. Here's a quick example:
# test_counter.nos - Test for the counter app
main() = {
driver = Selenium.connect("http://localhost:4444")
Selenium.goto(driver, "http://localhost:8080")
Selenium.waitFor(driver, "button", 5000)
# Test initial state
text = Selenium.text(driver, "p")
assert(String.contains(text, "Count: 0"))
# Test increment
Selenium.click(driver, "button") # Click +1
sleep(500)
text = Selenium.text(driver, "p")
assert(String.contains(text, "Count: 1"))
Selenium.close(driver)
println("All tests passed!")
0
}