feat: add buildmanual target, faker demo, subprocess examples, and contributing sidebar to manual

Add Makefile buildmanual target for manual generation and include faker_demo.wren, subprocess_async_demo.wren, subprocess_concurrent_demo.wren, and subprocess_fiber_demo.wren example scripts. Update manual sidebar across api pages to link new web-server tutorial and contributing section with module architecture, pure-wren/c-backed modules, foreign classes, async patterns, testing, and documentation pages.
This commit is contained in:
2026-01-25 18:02:02 +00:00
parent 819bbd5091
commit 5c2269f6a7
92 changed files with 10955 additions and 621 deletions
+562 -3
View File
@@ -85,6 +85,7 @@
<li><a href="../tutorials/database-app.html">Database App</a></li>
<li><a href="../tutorials/template-rendering.html">Templates</a></li>
<li><a href="../tutorials/cli-tool.html">CLI Tool</a></li>
<li><a href="../tutorials/web-server.html">Web Server</a></li>
</ul>
</div>
<div class="section">
@@ -99,6 +100,19 @@
<li><a href="../howto/error-handling.html">Error Handling</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Contributing</span>
<ul>
<li><a href="../contributing/index.html">Overview</a></li>
<li><a href="../contributing/module-overview.html">Module Architecture</a></li>
<li><a href="../contributing/pure-wren-module.html">Pure-Wren Modules</a></li>
<li><a href="../contributing/c-backed-module.html">C-Backed Modules</a></li>
<li><a href="../contributing/foreign-classes.html">Foreign Classes</a></li>
<li><a href="../contributing/async-patterns.html">Async Patterns</a></li>
<li><a href="../contributing/testing.html">Writing Tests</a></li>
<li><a href="../contributing/documentation.html">Documentation</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
@@ -115,7 +129,7 @@
<p>The <code>web</code> module provides a web framework for building HTTP servers and a client for making HTTP requests. It includes routing, middleware, sessions, static file serving, and class-based views.</p>
<pre><code>import "web" for Application, Router, Request, Response, View, Session, Client</code></pre>
<pre><code>import "web" for Application, Router, Request, Response, View, Session, Client, WebSocketResponse</code></pre>
<h2>Application Class</h2>
@@ -177,6 +191,21 @@
<p>Serves static files from a directory.</p>
<pre><code>app.static_("/assets", "./public")</code></pre>
<div class="method-signature">
<span class="method-name">websocket</span>(<span class="param">path</span>, <span class="param">handler</span>) → <span class="type">Application</span>
</div>
<p>Registers a WebSocket handler for the given path. Handler receives Request and should use WebSocketResponse to handle the connection.</p>
<pre><code>app.websocket("/ws", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isText) ws.send("Echo: " + msg.text)
}
return ws
})</code></pre>
<div class="method-signature">
<span class="method-name">use</span>(<span class="param">middleware</span>) → <span class="type">Application</span>
</div>
@@ -345,8 +374,300 @@
var data = request.json
return Response.json({"created": data})
}
websocket(request) {
var ws = WebSocketResponse.new()
ws.prepare(request)
ws.iterate(Fn.new { |msg|
if (msg.isText) ws.send("Echo: " + msg.text)
})
return ws
}
}</code></pre>
<h2>WebSocketResponse Class</h2>
<div class="class-header">
<h3>WebSocketResponse</h3>
<p>Server-side WebSocket connection handler (aiohttp-style API)</p>
</div>
<p>The <code>WebSocketResponse</code> class provides a server-side WebSocket handler following the aiohttp-style API pattern. It handles the WebSocket handshake, frame encoding/decoding, and provides convenient methods for sending and receiving messages.</p>
<h3>Constructor</h3>
<div class="method-signature">
<span class="method-name">WebSocketResponse.new</span>() → <span class="type">WebSocketResponse</span>
</div>
<p>Creates a new WebSocket response handler. Call <code>prepare(request)</code> to complete the handshake before sending or receiving messages.</p>
<h3>Properties</h3>
<table>
<tr>
<th>Property</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>isPrepared</code></td>
<td>Bool</td>
<td>True if the WebSocket handshake has been performed</td>
</tr>
<tr>
<td><code>isOpen</code></td>
<td>Bool</td>
<td>True if the connection is open and ready for messages</td>
</tr>
</table>
<h3>Connection Methods</h3>
<div class="method-signature">
<span class="method-name">prepare</span>(<span class="param">request</span>) → <span class="type">WebSocketResponse</span>
</div>
<p>Performs the WebSocket handshake using the request's socket. Must be called before sending or receiving messages. Returns <code>this</code> for method chaining.</p>
<ul class="param-list">
<li><span class="param-name">request</span> <span class="param-type">(Request)</span> - The HTTP request containing the WebSocket upgrade headers</li>
</ul>
<pre><code>var ws = WebSocketResponse.new()
ws.prepare(request)</code></pre>
<h3>Sending Methods</h3>
<div class="method-signature">
<span class="method-name">send</span>(<span class="param">text</span>)
</div>
<div class="method-signature">
<span class="method-name">sendText</span>(<span class="param">text</span>)
</div>
<p>Sends a text message to the client. Both methods are equivalent.</p>
<ul class="param-list">
<li><span class="param-name">text</span> <span class="param-type">(String)</span> - Text message to send</li>
</ul>
<pre><code>ws.send("Hello, client!")
ws.sendText("Another message")</code></pre>
<div class="method-signature">
<span class="method-name">sendJson</span>(<span class="param">data</span>)
</div>
<p>Serializes the data to JSON and sends it as a text message. Convenience method for JSON-based protocols.</p>
<ul class="param-list">
<li><span class="param-name">data</span> <span class="param-type">(Map|List)</span> - Data to serialize and send</li>
</ul>
<pre><code>ws.sendJson({"type": "update", "value": 42})
ws.sendJson(["item1", "item2", "item3"])</code></pre>
<div class="method-signature">
<span class="method-name">sendBinary</span>(<span class="param">bytes</span>)
</div>
<p>Sends a binary message to the client.</p>
<ul class="param-list">
<li><span class="param-name">bytes</span> <span class="param-type">(List)</span> - List of bytes to send</li>
</ul>
<pre><code>ws.sendBinary([0x01, 0x02, 0x03, 0x04])
var imageData = File.readBytes("image.png")
ws.sendBinary(imageData)</code></pre>
<h3>Receiving Methods</h3>
<div class="method-signature">
<span class="method-name">receive</span>() → <span class="type">WebSocketMessage|null</span>
</div>
<p>Receives the next message from the client. Blocks until a message arrives. Returns a <code>WebSocketMessage</code> object, or <code>null</code> if the connection closed unexpectedly. Ping frames are automatically answered with pong.</p>
<pre><code>var msg = ws.receive()
if (msg != null && msg.isText) {
System.print("Got: %(msg.text)")
}</code></pre>
<div class="method-signature">
<span class="method-name">receiveJson</span>() → <span class="type">Map|List|WebSocketMessage|null</span>
</div>
<p>Receives a text message and parses it as JSON. Returns the parsed JSON data, the close frame if the connection was closed, or <code>null</code> if the connection closed unexpectedly. Aborts if a binary frame is received.</p>
<pre><code>var data = ws.receiveJson()
if (data is Map && data.containsKey("type")) {
if (data["type"] == "message") {
System.print("Message: %(data["text"])")
}
}</code></pre>
<div class="method-signature">
<span class="method-name">receiveText</span>() → <span class="type">String|null</span>
</div>
<p>Receives a message and returns only the text content. Returns <code>null</code> if the message is not a text frame, if the connection was closed, or if the connection closed unexpectedly.</p>
<pre><code>var text = ws.receiveText()
if (text != null) {
System.print("Received: %(text)")
}</code></pre>
<div class="method-signature">
<span class="method-name">receiveBinary</span>() → <span class="type">List|null</span>
</div>
<p>Receives a message and returns only the binary content. Returns <code>null</code> if the message is not a binary frame, if the connection was closed, or if the connection closed unexpectedly.</p>
<pre><code>var bytes = ws.receiveBinary()
if (bytes != null) {
System.print("Received %(bytes.count) bytes")
}</code></pre>
<div class="method-signature">
<span class="method-name">iterate</span>(<span class="param">callback</span>)
</div>
<p>Helper method that loops while the connection is open, calling the callback for each received message (excluding close frames). Simplifies the common receive loop pattern.</p>
<ul class="param-list">
<li><span class="param-name">callback</span> <span class="param-type">(Fn)</span> - Function to call with each WebSocketMessage</li>
</ul>
<pre><code>ws.iterate(Fn.new { |msg|
if (msg.isText) {
ws.send("Echo: " + msg.text)
}
})</code></pre>
<h3>Control Methods</h3>
<div class="method-signature">
<span class="method-name">ping</span>()
</div>
<div class="method-signature">
<span class="method-name">ping</span>(<span class="param">data</span>)
</div>
<p>Sends a ping frame to check if the client is still connected. Payload must be 125 bytes or less.</p>
<ul class="param-list">
<li><span class="param-name">data</span> <span class="param-type">(String|List)</span> - Optional payload data</li>
</ul>
<div class="method-signature">
<span class="method-name">pong</span>(<span class="param">data</span>)
</div>
<p>Sends a pong frame. Usually automatic in response to ping, but can be sent manually.</p>
<div class="method-signature">
<span class="method-name">close</span>()
</div>
<div class="method-signature">
<span class="method-name">close</span>(<span class="param">code</span>, <span class="param">reason</span>)
</div>
<p>Closes the WebSocket connection. Default code is 1000 (normal closure).</p>
<ul class="param-list">
<li><span class="param-name">code</span> <span class="param-type">(Num)</span> - Close status code (1000-4999)</li>
<li><span class="param-name">reason</span> <span class="param-type">(String)</span> - Human-readable close reason</li>
</ul>
<pre><code>ws.close()
ws.close(1000, "Goodbye")</code></pre>
<h3>WebSocketMessage Reference</h3>
<p>The <code>receive()</code> method returns a <code>WebSocketMessage</code> object with the following properties:</p>
<table>
<tr>
<th>Property</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>opcode</code></td>
<td>Num</td>
<td>Frame opcode (1=text, 2=binary, 8=close, 9=ping, 10=pong)</td>
</tr>
<tr>
<td><code>fin</code></td>
<td>Bool</td>
<td>True if this is the final fragment</td>
</tr>
<tr>
<td><code>isText</code></td>
<td>Bool</td>
<td>True if this is a text frame</td>
</tr>
<tr>
<td><code>isBinary</code></td>
<td>Bool</td>
<td>True if this is a binary frame</td>
</tr>
<tr>
<td><code>isClose</code></td>
<td>Bool</td>
<td>True if this is a close frame</td>
</tr>
<tr>
<td><code>isPing</code></td>
<td>Bool</td>
<td>True if this is a ping frame</td>
</tr>
<tr>
<td><code>isPong</code></td>
<td>Bool</td>
<td>True if this is a pong frame</td>
</tr>
<tr>
<td><code>text</code></td>
<td>String</td>
<td>Payload as string (for text frames)</td>
</tr>
<tr>
<td><code>bytes</code></td>
<td>List</td>
<td>Payload as byte list</td>
</tr>
<tr>
<td><code>closeCode</code></td>
<td>Num</td>
<td>Close status code (for close frames)</td>
</tr>
<tr>
<td><code>closeReason</code></td>
<td>String</td>
<td>Close reason text (for close frames)</td>
</tr>
</table>
<h3>Close Codes Reference</h3>
<table>
<tr>
<th>Code</th>
<th>Meaning</th>
</tr>
<tr>
<td>1000</td>
<td>Normal closure</td>
</tr>
<tr>
<td>1001</td>
<td>Going away (server shutdown)</td>
</tr>
<tr>
<td>1002</td>
<td>Protocol error</td>
</tr>
<tr>
<td>1003</td>
<td>Unsupported data type</td>
</tr>
<tr>
<td>1005</td>
<td>No status received</td>
</tr>
<tr>
<td>1006</td>
<td>Abnormal closure</td>
</tr>
<tr>
<td>1011</td>
<td>Server error</td>
</tr>
<tr>
<td>4000-4999</td>
<td>Application-specific codes</td>
</tr>
</table>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Ping frames received from clients are automatically answered with pong frames. You typically do not need to handle ping/pong manually unless implementing custom keepalive logic.</p>
</div>
<h2>Session Class</h2>
<div class="class-header">
@@ -551,6 +872,244 @@ var postResponse = Client.post("https://api.example.com/users", {
var data = Json.parse(postResponse["body"])</code></pre>
<h3>Concurrent HTTP Requests</h3>
<p>Use the <code>async</code> keyword to create futures and <code>await</code> to wait for results. Each request runs in its own fiber and executes in parallel while waiting for I/O.</p>
<pre><code>import "web" for Client
import "scheduler" for Scheduler, Future
var urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments"
]
var futures = []
for (url in urls) {
futures.add(async { Client.get(url) })
}
var responses = []
for (future in futures) {
responses.add(await future)
}
for (i in 0...urls.count) {
System.print("%(urls[i]): %(responses[i]["status"])")
}</code></pre>
<h3>Parallel Batch Requests</h3>
<p>For batch operations, create all futures first, then await them. The requests execute concurrently.</p>
<pre><code>import "web" for Client
import "scheduler" for Scheduler, Future
import "json" for Json
var fetchUrl = async { |url| Client.get(url) }
class BatchClient {
static getAll(urls) {
var futures = []
for (url in urls) {
futures.add(async { Client.get(url) })
}
var results = []
for (f in futures) {
results.add(await f)
}
return results
}
static postAll(requests) {
var futures = []
for (req in requests) {
futures.add(async { Client.post(req["url"], req["options"]) })
}
var results = []
for (f in futures) {
results.add(await f)
}
return results
}
}
var urls = [
"https://api.example.com/endpoint1",
"https://api.example.com/endpoint2",
"https://api.example.com/endpoint3"
]
var results = BatchClient.getAll(urls)
for (result in results) {
System.print("Status: %(result["status"])")
}</code></pre>
<h3>Parameterized Async Functions</h3>
<p>Create reusable async functions with parameters using <code>async { |args| ... }</code>. There are two ways to invoke these functions:</p>
<ul>
<li><strong><code>await fn(args)</code></strong> — Direct call, waits immediately (sequential execution)</li>
<li><strong><code>fn.call(args)</code></strong> — Returns a Future, starts execution without waiting (concurrent execution)</li>
</ul>
<pre><code>import "web" for Client
import "scheduler" for Scheduler, Future
import "json" for Json
var fetchJson = async { |url|
var response = Client.get(url)
return Json.parse(response["body"])
}
var postJson = async { |url, data|
var response = Client.post(url, {"json": data})
return Json.parse(response["body"])
}
// DIRECT CALLING (preferred for sequential operations)
// Waits for each request to complete before continuing
var user = await fetchJson("https://api.example.com/user/1")
System.print(user["name"])
// CONCURRENT EXECUTION (use .call() to start without waiting)
// Both requests run at the same time
var f1 = fetchJson.call("https://api.example.com/posts")
var f2 = fetchJson.call("https://api.example.com/comments")
// Now wait for results
var posts = await f1
var comments = await f2</code></pre>
<div class="admonition tip">
<div class="admonition-title">Direct Calling vs .call()</div>
<p>Use <code>await fn(args)</code> when you want sequential execution. Use <code>fn.call(args)</code> when you want to start multiple operations concurrently, then <code>await</code> their results later.</p>
</div>
<h3>WebSocket Echo Server</h3>
<pre><code>import "web" for Application, Response, WebSocketResponse
var app = Application.new()
app.get("/", Fn.new { |req|
return Response.html("&lt;script&gt;var ws=new WebSocket('ws://localhost:8080/ws');ws.onmessage=e=&gt;console.log(e.data);ws.onopen=()=&gt;ws.send('hello');&lt;/script&gt;")
})
app.websocket("/ws", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isText) {
ws.send("Echo: " + msg.text)
}
}
ws.close()
return ws
})
app.run("0.0.0.0", 8080)</code></pre>
<h3>WebSocket JSON API</h3>
<pre><code>import "web" for Application, Response, WebSocketResponse
var app = Application.new()
app.websocket("/api", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
ws.sendJson({"type": "welcome", "message": "Connected to API"})
while (ws.isOpen) {
var data = ws.receiveJson()
if (data == null) break
if (data is Map &amp;&amp; data["type"] == "ping") {
ws.sendJson({"type": "pong", "timestamp": data["timestamp"]})
} else if (data is Map &amp;&amp; data["type"] == "echo") {
ws.sendJson({"type": "echo", "data": data["data"]})
}
}
return ws
})
app.run("0.0.0.0", 8080)</code></pre>
<h3>WebSocket Binary Data Transfer</h3>
<pre><code>import "web" for Application, Response, WebSocketResponse
import "io" for File
var app = Application.new()
app.websocket("/binary", Fn.new { |req|
var ws = WebSocketResponse.new()
ws.prepare(req)
ws.send("Ready to receive binary data")
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isBinary) {
System.print("Received %(msg.bytes.count) bytes")
ws.sendJson({"received": msg.bytes.count, "status": "ok"})
} else if (msg.isText) {
if (msg.text == "send-file") {
var data = File.readBytes("example.bin")
ws.sendBinary(data)
}
}
}
return ws
})
app.run("0.0.0.0", 8080)</code></pre>
<h3>WebSocket in Class-Based View</h3>
<pre><code>import "web" for Application, Response, View, WebSocketResponse
class ChatView is View {
get(request) {
return Response.html("&lt;h1&gt;Chat Room&lt;/h1&gt;&lt;p&gt;Connect via WebSocket&lt;/p&gt;")
}
websocket(request) {
var ws = WebSocketResponse.new()
ws.prepare(request)
ws.sendJson({"type": "connected", "room": "general"})
ws.iterate(Fn.new { |msg|
if (msg.isText) {
var data = Json.parse(msg.text)
if (data["type"] == "message") {
ws.sendJson({
"type": "broadcast",
"from": data["from"],
"text": data["text"]
})
}
}
})
return ws
}
}
var app = Application.new()
app.addView("/chat", ChatView)
app.run("0.0.0.0", 8080)</code></pre>
<div class="admonition warning">
<div class="admonition-title">WebSocket Error Handling</div>
<p>Always check for <code>null</code> or close frames when receiving messages. The connection can close at any time due to network issues or client disconnection. Wrap WebSocket handling in proper error handling to prevent server crashes.</p>
</div>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Sessions are stored in memory by default. Data is lost when the server restarts. For production, consider persisting session data to a database.</p>
@@ -558,8 +1117,8 @@ var data = Json.parse(postResponse["body"])</code></pre>
</article>
<footer class="page-footer">
<a href="markdown.html" class="prev">markdown</a>
<a href="index.html" class="next">Overview</a>
<a href="wdantic.html" class="prev">wdantic</a>
<a href="websocket.html" class="next">websocket</a>
</footer>
</main>
</div>