← web · API reference · version 0.4.5
4 file(s), 646 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.
Public items
A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
HttpResponse struct | 8 | — | 15 40 41 57 63 69 … (13 total) |
ok fn | 15 | — | none in this package |
http_get fn | 57 | — | none in this package |
http_post fn | 63 | — | none in this package |
http_put fn | 69 | — | none in this package |
http_delete fn | 75 | — | none in this package |
http_get_h fn | 84 | — | none in this package |
http_post_h fn | 90 | — | none in this package |
http_put_h fn | 96 | — | none in this package |
http_delete_h fn | 102 | — | none in this package |
http_get_range fn | 113 | — | 153 |
http_get_range_h fn | 120 | — | none in this package |
http_size fn | 150 | — | none in this package |
WsHandler struct | 174 | — | 231 234 241 249 256 265 … (9 total) |
ws_handler fn | 231 | WEB-003 | 83 86 |
send fn | 241 | WEB-003 | 99 100 |
send_binary fn | 249 | — | none in this package |
last_opcode fn | 256 | — | none in this package |
pump fn | 265 | — | 100 |
try_recv fn | 279 | — | 101 102 |
close fn | 285 | — | 104 |
sleep_ms fn | 293 | — | none in this package |
pack_reset fn | 319 | WEB-001 | 25 55 11 21 31 40 … (10 total) |
pack_u8 fn | 323 | — | 26 56 57 12 13 41 … (13 total) |
pack_u16_le fn | 327 | — | 27 22 |
pack_u32_le fn | 332 | — | 32 |
pack_take fn | 338 | WEB-001 | 28 58 14 23 33 42 … (12 total) |
byte_at fn | 346 | WEB-002 | 18 19 20 29 31 32 … (19 total) |
WsGroupEvent struct | 373 | — | 407 411 413 |
WsGroup struct | 379 | — | 393 395 399 407 |
ws_group fn | 393 | — | none in this package |
add fn | 399 | — | none in this package |
poll fn | 407 | — | none in this package |
frame_yield fn | 419 | — | none in this package |
src/web.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// --- HTTP Client + WebSocket Client ---
// Blocking HTTP client for consuming web services.
// Polling WebSocket client for browser-and-native communication.
// The result of an HTTP request.
pub struct HttpResponse {
pub status: integer,
pub body: text,
pub headers: vector<text>
}
// Returns true when the status code is in the 200-299 range.
pub fn ok(self: HttpResponse) -> boolean {
self.status >= 200 && self.status < 300
}
// --- Internal native functions returning primitives ---
fn http_do(method: text, url: text, body: text, headers: text) -> integer;
#native
fn http_body() -> text;
#native
// Response headers from the last request, as newline-separated "Key: Value".
fn http_headers() -> text;
#native
// Internal: resolve a URL's byte size via a HEAD request (Content-Length).
// Returns -1 if the request failed or the server omitted the length on HEAD;
// the public `http_size` falls back to a range read in that case.
fn http_head_size(url: text) -> integer;
#native "n_http_size"
// Build the full response from the last native call: status, binary-safe body
// (raw bytes, NUL-preserving via the pack/byte_at convention), and response
// headers split into a vector.
fn make_response(status: integer) -> HttpResponse {
HttpResponse { status: status, body: http_body(), headers: http_headers().split('\n') }
}
// Internal: serialize headers vector to newline-separated "Key: Value" text.
fn join_headers(headers: vector<text>) -> text {
result = "";
for h in headers {
if result.len() > 0 { result += "\n"; }
result += h;
}
result
}
// --- Simple HTTP functions (no custom headers) ---
// Send a GET request.
pub fn http_get(url: text) -> HttpResponse {
status = http_do("GET", url, "", "");
make_response(status)
}
// Send a POST request with a body.
pub fn http_post(url: text, body: text) -> HttpResponse {
status = http_do("POST", url, body, "");
make_response(status)
}
// Send a PUT request with a body.
pub fn http_put(url: text, body: text) -> HttpResponse {
status = http_do("PUT", url, body, "");
make_response(status)
}
// Send a DELETE request.
pub fn http_delete(url: text) -> HttpResponse {
status = http_do("DELETE", url, "", "");
make_response(status)
}
// --- HTTP functions with custom headers ---
// Headers are passed as a vector of "Key: Value" strings.
// GET with custom headers.
pub fn http_get_h(url: text, headers: vector<text>) -> HttpResponse {
status = http_do("GET", url, "", join_headers(headers));
make_response(status)
}
// POST with custom headers.
pub fn http_post_h(url: text, body: text, headers: vector<text>) -> HttpResponse {
status = http_do("POST", url, body, join_headers(headers));
make_response(status)
}
// PUT with custom headers.
pub fn http_put_h(url: text, body: text, headers: vector<text>) -> HttpResponse {
status = http_do("PUT", url, body, join_headers(headers));
make_response(status)
}
// DELETE with custom headers.
pub fn http_delete_h(url: text, headers: vector<text>) -> HttpResponse {
status = http_do("DELETE", url, "", join_headers(headers));
make_response(status)
}
// --- Byte-range reads (partial content) ---
// Read the byte range [offset, offset+len) of a remote file via a Range
// request. Returns status 206 (Partial Content) with the raw bytes in `body`
// when the server honours ranges; a server that ignores Range returns 200 with
// the full body. Read the bytes with `byte_at(i, response.body)`.
pub fn http_get_range(url: text, offset: integer, len: integer) -> HttpResponse {
last = offset + len - 1;
status = http_do("GET", url, "", "Range: bytes={offset}-{last}");
make_response(status)
}
// Byte-range read with extra custom headers (e.g. If-Range / If-None-Match).
pub fn http_get_range_h(url: text, offset: integer, len: integer, headers: vector<text>) -> HttpResponse {
last = offset + len - 1;
range = "Range: bytes={offset}-{last}";
all = join_headers(headers);
combined = if all.len() > 0 { "{all}\n{range}" } else { range };
status = http_do("GET", url, "", combined);
make_response(status)
}
// --- Size query ---
// Extract the total size from a "Content-Range: bytes X-Y/TOTAL" header line,
// or -1 if no such header is present.
fn content_range_total(headers: vector<text>) -> integer {
for h in headers {
if h.to_lowercase().starts_with("content-range:") {
parts = h.split('/');
if parts.len() >= 2 {
return (parts[parts.len() - 1] ?? "").trim() as integer ?? -1;
}
}
}
-1
}
// Resolve a URL's total byte size without downloading it. Tries a HEAD request
// (Content-Length); if the server omits the length on HEAD (some CDNs — e.g.
// github-raw — only report it on GET), falls back to a 1-byte range read and
// takes the total from the Content-Range header. Returns -1 if size is
// unavailable.
pub fn http_size(url: text) -> integer {
n = http_head_size(url);
if n >= 0 { return n; }
r = http_get_range(url, 0, 1);
if r.status == 206 || r.status == 200 {
return content_range_total(r.headers);
}
-1
}
// --- WebSocket client ---
//
// Handler-style API: the programmer opens a session with `ws_handler`,
// then drives event delivery by calling `pump(handler, on_message)`
// from a main loop. The library manages the connection lifecycle:
// connect at registration time, auto-reconnect on transient failure
// (exponential backoff capped at 10 seconds), close on `close()`.
//
// `pump` takes its callback per call, so a handler holds no closure: the
// callback typically captures a `Reference<T>` to state held in a store, and
// updates that state through it for each message.
// An open WebSocket session, from `ws_handler`. It is a slot number on the
// native side; the connection behind it reconnects on its own.
pub struct WsHandler {
pub id: integer // Rust-side connection slot
}
// --- Internal natives (lifecycle primitives) ---
fn ws_client_connect_native(url: text) -> integer;
#native "n_ws_connect"
fn ws_client_send_native(handle: integer, msg: text) -> boolean;
#native "n_ws_client_send"
// Send the byte buffer as a binary WebSocket frame (opcode 2)
// instead of a text frame (opcode 1). Same payload encoding —
// loft `text` is a byte buffer; this just changes the frame type
// the peer sees. Used by TTT v5 + plan-36 for binary world data.
fn ws_client_send_binary_native(handle: integer, msg: text) -> boolean;
#native "n_ws_client_send_binary"
// Returns true when a message has been delivered to the receive
// buffer; false when the queue is empty or the connection is currently
// down. After true, call ws_client_message_native() to read the payload.
fn ws_client_recv_native(handle: integer) -> boolean;
#native "n_ws_client_recv"
fn ws_client_message_native() -> text;
#native "n_ws_client_message"
// Opcode of the last frame surfaced by `ws_client_recv_native`.
// 1 = text, 2 = binary, 8 = close, 9 = ping, 10 = pong.
// Loft programs that handle binary frames check this after a
// successful recv to decide whether the message bytes are text
// (utf-8) or a binary blob.
fn ws_client_opcode_native() -> integer;
#native "n_ws_client_opcode"
fn ws_client_close_native(handle: integer);
#native "n_ws_client_close"
// --- Public API ---
// Open a WebSocket session. Returns null only if the URL is
// malformed; transient connection failures (server not yet up,
// network blip) are absorbed by the auto-reconnect loop in the Rust
// layer, so this call always returns a usable handle for any
// well-formed `ws://...` or `wss://...` URL.
//
// `wss://` (TLS) works on BOTH targets: the browser uses the platform
// WebSocket, and the native client dials TLS through rustls with
// certificate validation ON by default (default port 443). For a self-signed dev endpoint,
// set `LOFT_WS_INSECURE_SKIP_VERIFY=1` in the native process — an
// explicit opt-out, never a URL option, so it cannot be baked into a
// deployed config by accident.
// Example: @WEB-003 — a handle is not a connection; ask the traffic, not the handle.
//
// ⚠ The return is `WsHandler?` because null is the one distinction this call
// exists to make: test it with `== null`.
pub fn ws_handler(url: text) -> WsHandler? {
id = ws_client_connect_native(url);
if id < 0 { return null; }
WsHandler { id: id }
}
// Send a text message to the server. Returns true on success; false
// means the connection is currently in backoff / reconnecting and the
// caller may retry after the next `pump()`.
// Example: @WEB-003 — this false, not a null handle, is what reports the link down.
pub fn send(self: WsHandler, msg: text) -> boolean {
ws_client_send_native(self.id, msg)
}
// Send `msg` as a binary WebSocket frame (opcode 2). Loft `text`
// is a byte buffer — build it with `pack_reset` / `pack_u8` / … /
// `pack_take`, then ship it. Returns true on success; false means
// the connection is currently in backoff / reconnecting.
pub fn send_binary(self: WsHandler, msg: text) -> boolean {
ws_client_send_binary_native(self.id, msg)
}
// Opcode of the last frame surfaced by `try_recv` / `pump` on this
// handler. Use after a successful read to decide whether to
// interpret the payload as utf-8 text or as a binary blob.
pub fn last_opcode(self: WsHandler) -> integer {
_ = self;
ws_client_opcode_native()
}
// Drain any pending received messages, firing `on_message` for each.
// Returns the count of messages delivered this call. Call this from
// a main loop (and yield to the host between iterations on the browser
// target) to drive event delivery without async.
pub fn pump(self: WsHandler, on_message: fn(text)) -> integer {
count = 0;
while ws_client_recv_native(self.id) {
on_message(ws_client_message_native());
count += 1;
}
count
}
// Non-blocking single-message receive. Returns the next pending
// message text if one is available, null otherwise. Equivalent to a
// single iteration of pump without the callback — useful when the
// receiver wants to drive its own loop and accumulate messages
// locally without capturing state inside a closure.
pub fn try_recv(self: WsHandler) -> text? {
if !ws_client_recv_native(self.id) { return null; }
ws_client_message_native()
}
// Permanently close the session (no further reconnect attempts).
pub fn close(self: WsHandler) {
ws_client_close_native(self.id);
}
// Block the calling thread for `ms` milliseconds. Convenience for
// tests and pacing loops; non-positive values are a no-op. In the
// browser it returns at once — the host event loop paces there, so
// call `frame_yield` instead.
pub fn sleep_ms(ms: integer);
#native
// --- Binary buffer construction ---
//
// Loft `text` is a UTF-8 byte buffer; a NUL byte (codepoint 0) is the
// `character` null sentinel, so a `text` built via `"{c}{c}…"`
// interpolation silently drops zero bytes. Binary protocols that
// include zero bytes (e.g. the v5 5-byte blob header `[type:u8]
// [session:u32-LE]` for any small session id) cannot be assembled
// that way.
//
// `pack_*` are a thread-local builder pattern: `pack_reset()` empties
// the buffer, `pack_u8` / `pack_u16_le` / `pack_u32_le` append fields
// by type, and `pack_take()` returns a `text` whose bytes carry the
// binary payload verbatim — NUL bytes preserved. The buffer survives
// across calls within a single thread; reset before each blob. The
// returned `text` lives until the next `pack_take()`, so `send_binary`
// it (or copy it) before reusing the builder.
//
// On the receive side, `byte_at(i, t)` returns the single byte at
// offset `i` of `t` (regardless of UTF-8 interpretation). Use it to
// decode binary frames returned by `try_recv` / `pump` field-by-field.
// Empty the per-thread pack buffer. Call before each blob.
// Example: @WEB-001 — reset, append in wire order, take (which moves, not reads).
pub fn pack_reset();
#native
// Append a single byte to the pack buffer: the low 8 bits of `b`.
pub fn pack_u8(b: integer);
#native
// Append the low 16 bits of `v`, little-endian.
pub fn pack_u16_le(v: integer);
#native
// Append the low 32 bits of `v`, little-endian — so -1 packs as
// FF FF FF FF, the same bytes as 0xFFFFFFFF.
pub fn pack_u32_le(v: integer);
#native
// Take the contents of the pack buffer and return them as a `text`
// whose UTF-8 bytes are exactly the buffer bytes. Resets the buffer.
// Example: @WEB-001 — the three-step protocol, and the NUL that interpolation drops.
pub fn pack_take() -> text;
#native
// Read the byte at `idx` of the text payload. Out-of-range (negative
// or past the end) returns -1. Use to decode binary frames received
// via `try_recv` / `pump`. Argument order is (idx, t), unlike the
// (text, index) order elsewhere.
// Example: @WEB-002 — why the bounds test is `< 0` and never `== 0`.
pub fn byte_at(idx: integer, t: text) -> integer;
#native
// --- Multiplexed client receiver (WsGroup) ---
//
// Watch multiple WsHandler connections and receive from whichever has
// data ready — one scan across all handles instead of polling each
// individually. The key difference: handles with no data are skipped
// after one short timeout, so N idle sockets cost one timeout total
// instead of N timeouts.
//
// group = web::ws_group();
// group.add(h1);
// group.add(h2);
// for _ in 0..1000 {
// ev = group.poll();
// if ev.handle < 0 { break; }
// // ev.message has the data
// }
//
// The group's list lives on the native side and there is one per thread:
// `ws_group()` empties it, so a second group replaces the first. In the
// browser `poll` always answers -1: receive with each handler's `try_recv`
// there.
// What one `poll` found: the handler slot that had a message and the
// message, or `handle == -1` and "" when none had.
pub struct WsGroupEvent {
pub handle: integer,
pub message: text
}
// A set of handlers to receive from in one scan; see above.
pub struct WsGroup {
_dummy: integer // no state on loft side; Rust owns the list
}
fn ws_group_clear_native();
#native "n_ws_group_clear"
fn ws_group_add_native(handle: integer);
#native "n_ws_group_add"
fn ws_group_poll_native() -> integer;
#native "n_ws_group_poll"
// Create a new (empty) group.
pub fn ws_group() -> WsGroup {
ws_group_clear_native();
WsGroup { _dummy: 0 }
}
// Add a handler to the group.
pub fn add(self: WsGroup, h: WsHandler) {
_ = self;
ws_group_add_native(h.id);
}
// Poll all handles round-robin. Returns a WsGroupEvent with
// handle >= 0 and the message if one is ready; handle == -1
// means nothing is available right now.
pub fn poll(self: WsGroup) -> WsGroupEvent {
_ = self;
h = ws_group_poll_native();
if h < 0 {
return WsGroupEvent { handle: -1, message: "" };
}
WsGroupEvent { handle: h, message: ws_client_message_native() }
}
// Yield one frame to the host event loop: a no-op natively, and in the
// browser the suspend point that lets the event loop run. Call once per WS poll
// iteration so the browser event loop can deliver WebSocket messages.
pub fn frame_yield();
#native "n_yield_frame"
tests/byte_at.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Unit tests for `byte_at(idx, text) -> integer` — the raw-byte
// accessor on text values. Used by binary-protocol parsers
// (game_protocol, server) to read individual bytes out of a
// `pack_take()` buffer or a WebSocket binary frame.
//
// `byte_at` returns the byte value (0..255) at `idx` in the
// underlying UTF-8 bytes of `t`. Out-of-range returns -1 (no panic)
// — NOT 0, which is a byte a binary frame legitimately carries; see
// `test_byte_at_out_of_range` below and @WEB-002.
use web::*;
fn test_byte_at_ascii() {
s = "ABC";
assert(web::byte_at(0, s) == 0x41, "byte 0 = 0x41 'A'");
assert(web::byte_at(1, s) == 0x42, "byte 1 = 0x42 'B'");
assert(web::byte_at(2, s) == 0x43, "byte 2 = 0x43 'C'");
}
fn test_byte_at_after_pack() {
// Real-world shape: pack_u8 + pack_u16_le, then byte_at to verify.
web::pack_reset();
web::pack_u8(0xAB);
web::pack_u16_le(0x1234);
buf = web::pack_take();
assert(web::byte_at(0, buf) == 0xAB, "packed byte 0 = 0xAB");
// little-endian: low byte first
assert(web::byte_at(1, buf) == 0x34, "packed byte 1 = 0x34 (LE low)");
assert(web::byte_at(2, buf) == 0x12, "packed byte 2 = 0x12 (LE high)");
}
fn test_byte_at_empty() {
// Out-of-range on empty text: returns the OOB sentinel (currently
// observed as u32::MAX / 4294967295). Valid bytes are 0..=255 so
// the test asserts "not a valid byte value" robustly.
v0 = web::byte_at(0, "");
v5 = web::byte_at(5, "");
assert(v0 < 0 || v0 > 255, "byte 0 of empty must be OOB sentinel, got {v0 ?? -1}");
assert(v5 < 0 || v5 > 255, "byte 5 of empty must be OOB sentinel, got {v5 ?? -1}");
}
fn test_byte_at_out_of_range() {
s = "Hi";
v2 = web::byte_at(2, s);
v100 = web::byte_at(100, s);
assert(v2 < 0 || v2 > 255, "byte 2 past len must be OOB sentinel, got {v2 ?? -1}");
assert(v100 < 0 || v100 > 255, "byte 100 past len must be OOB sentinel, got {v100 ?? -1}");
}
fn test_byte_at_extremes() {
// Bytes at the 0x00 and 0xFF boundary.
web::pack_reset();
web::pack_u8(0);
web::pack_u8(255);
buf = web::pack_take();
assert(web::byte_at(0, buf) == 0, "byte value 0");
assert(web::byte_at(1, buf) == 255, "byte value 255");
}
tests/pack.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Local cdylib smoke — exercises the binary-packing helpers
// without touching the network. The network-dependent HTTP
// tests live in tests-network/http.loft and are not run in CI.
use web::*;
fn test_pack_u8_roundtrip() {
web::pack_reset();
web::pack_u8(0x41);
web::pack_u8(0x42);
s = web::pack_take();
assert(s.size() == 2, "2 bytes, got {s.size()}");
assert(s[0] == 'A', "byte 0 = A");
assert(s[1] == 'B', "byte 1 = B");
}
fn test_pack_u16_le() {
web::pack_reset();
web::pack_u16_le(0x4142);
s = web::pack_take();
assert(s.size() == 2, "2 bytes, got {s.size()}");
// little-endian: low byte first
assert(s[0] == 'B', "byte 0 = B (0x42)");
assert(s[1] == 'A', "byte 1 = A (0x41)");
}
fn test_pack_u32_le() {
web::pack_reset();
web::pack_u32_le(0x41424344);
s = web::pack_take();
assert(s.size() == 4, "4 bytes, got {s.size()}");
assert(s[0] == 'D', "byte 0 = D");
assert(s[3] == 'A', "byte 3 = A");
}
fn test_pack_reset_clears() {
web::pack_reset();
web::pack_u8(0x41);
s1 = web::pack_take();
web::pack_reset();
s2 = web::pack_take();
assert(s1.size() == 1, "first take 1 byte");
assert(s2.len() == 0, "after reset, len 0, got {s2.len()}");
}
// @PLN110 regression: pack_take() returns a BINARY buffer, so its length is a BYTE
// count — size(), not len() (the character count post-flip). Pack the two bytes of a
// UTF-8 "é" (0xC3 0xA9): they form ONE character but TWO bytes. size()==2 is correct;
// len() would read 1 and mis-measure the buffer.
fn test_pack_byte_count_is_bytes_not_chars() {
web::pack_reset();
web::pack_u8(0xC3);
web::pack_u8(0xA9);
s = web::pack_take();
assert(s.size() == 2, "packed buffer is 2 BYTES, got {s.size()}");
assert(s.len() == 1, "...which is 1 UTF-8 character (proves size != len here)");
}
tests/worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for the `web` library (@PLN141). Each `test_*` below is a REAL,
// run-every-CI demonstration of a function whose correct use is not obvious from its
// signature alone; the function documents itself by CITING the tag with
// `// Example: @WEB-00N`, and `check_doc_drift.sh examples` fails if a citation goes
// dangling (no test carries the tag). These are teaching examples, distinct from the
// unit suites in pack.loft / byte_at.loft.
//
// All three run without a network, like the rest of this package's CI tests: the
// HTTP examples live in tests-network/ and are not run here. @WEB-003 is the
// interesting case — the contract it pins is precisely that a DOWN server is not an
// error, so having no server is what makes it demonstrable.
use web::*;
// @WEB-001 — the pack builder exists because interpolation cannot carry a zero byte.
// Loft `text` is a UTF-8 byte buffer and codepoint 0 is the `character` null sentinel,
// so a blob assembled as `"{a}{b}{c}"` SILENTLY drops every NUL — and a binary header
// with a small integer field (a type byte, a session id) is mostly NULs. The builder
// is a three-step protocol on a per-thread buffer: `pack_reset` empties it, the
// `pack_u8` / `pack_u16_le` / `pack_u32_le` calls append in wire order, and `pack_take`
// hands the bytes back AND resets — so `take` is a move, not a read, and calling it
// twice answers an empty buffer the second time. The returned `text` is only valid
// until the next `pack_take`, so send or copy it before building the next blob. Measure
// it with `size()` (bytes); `len()` counts characters and will disagree.
fn test_pack_protocol_carries_the_zero_byte() {
// A three-byte frame whose middle field is zero — the shape interpolation loses.
web::pack_reset();
web::pack_u8(0x01);
web::pack_u8(0x00);
web::pack_u8(0x02);
blob = web::pack_take();
assert(blob.size() == 3, "all three bytes survive the builder: {blob.size()}");
assert(web::byte_at(1, blob) == 0, "including the NUL in the middle");
// The same three bytes by interpolation: the zero is gone, and nothing said so.
one = 1 as character;
zero = 0 as character;
two = 2 as character;
interpolated = "{one}{zero}{two}";
assert(interpolated.size() == 2,
"interpolation silently drops the NUL — 2 bytes, not 3: {interpolated.size()}");
// `take` is a move: the buffer is empty afterwards, so build the next blob from
// `pack_reset` and never expect a second take to repeat the first.
again = web::pack_take();
assert(again.size() == 0, "pack_take resets — a second take answers nothing");
}
// @WEB-002 — an out-of-range `byte_at` answers -1, and that matters BECAUSE 0 is a
// value the buffer legitimately carries. Testing `byte_at(i, buf) == 0` as a bounds
// check reads a real NUL field as the end of the frame, and reads an overrun as a NUL
// field — both wrong, in opposite directions, in exactly the binary protocols this
// helper exists for. Test `< 0` instead. Note the argument order too: `(idx, text)`,
// not the `(text, idx)` every other accessor takes — it is fixed by what the C-ABI
// auto-marshal supports, so it will not change to match.
fn test_byte_at_reports_out_of_range_as_minus_one() {
web::pack_reset();
web::pack_u8(0x41);
web::pack_u8(0x00);
buf = web::pack_take();
// In range: an honest zero at index 1.
assert(web::byte_at(0, buf) == 0x41, "byte 0 is 'A'");
assert(web::byte_at(1, buf) == 0, "byte 1 is a real NUL field, value 0");
// Out of range on either side: -1, distinguishable from that NUL.
assert(web::byte_at(2, buf) == -1, "one past the end answers -1, not 0");
assert(web::byte_at(-1, buf) == -1, "a negative index answers -1 too");
// The bounds test that works, and the one that does not.
assert(web::byte_at(2, buf) < 0, "`< 0` separates an overrun from a NUL field");
assert(web::byte_at(1, buf) == web::byte_at(2, buf) + 1, "…which `== 0` cannot do");
}
// @WEB-003 — a handle is not a connection. `ws_handler` answers null for ONE reason:
// a malformed URL. A well-formed URL whose server is down, not started yet, or
// briefly unreachable still answers a usable handle, because reconnecting (with
// exponential backoff) is the library's job, not the caller's. So a non-null handle
// says the URL parsed — never that anyone is listening, and a caller that treats it as
// "connected" reports itself up against a server that does not exist. What DOES report
// the link state is the traffic: `send` answers false while the connection is in
// backoff (retry after the next `pump`), and `try_recv` answers null for "nothing
// pending", which an idle healthy link answers just as often as a down one.
fn test_ws_handler_is_not_a_connection() {
// The one null case: a URL that cannot be parsed at all.
malformed = web::ws_handler("not-a-url");
assert(malformed == null, "a malformed URL is the only thing that answers null");
// A well-formed URL with nobody listening: still a handle, by design.
down = web::ws_handler("ws://127.0.0.1:47823/");
// ⚠ The GUARD CLAUSE is the example, not ceremony around it. `ws_handler` answers
// `WsHandler?` (0.4.0), so the null it is documented to return is now in the type —
// and a bare `assert(down != null, …)` does NOT make the handle usable: an assert is
// not a narrowing guard, so every method call after it stores a nullable into a
// non-null parameter. `if down == null { … return; }` IS one: the fall-through
// proves `down` non-null for the rest of this block, which is what lets the calls
// below read as they do.
if down == null {
assert(false, "a well-formed URL always answers a handle, server or no server");
return;
}
// Ask the traffic, not the handle.
sent = down.send("hello");
assert(!sent, "send answers false while reconnecting — retry after the next pump");
pending = down.try_recv();
assert(pending == null, "try_recv answers null for idle, which says nothing about health");
// `close` is what actually ends it: it stops the reconnect loop for good.
down.close();
}