ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
spa_router.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file spa_router.h
6 * @brief Single-page-app micro-routing + conditional UI streaming (PROTOCORE_ENABLE_SPA_ROUTER).
7 *
8 * A single-page web UI does its own client-side routing: the browser navigates to `/dashboard` or
9 * `/devices/42`, but there is no such file on the device - the server must return the app shell
10 * (`index.html`) and let the JavaScript router take over, while still serving real asset files
11 * (`/app.js`, `/style.css`) and letting API calls (`/api/...`) fall through to their handlers. This is
12 * that routing decision: given a request path, return whether to serve the file, serve the shell, or
13 * pass through to the app.
14 *
15 * The rule: a path under a configured API prefix passes through; a path whose last segment has a file
16 * extension (a dot) is a real asset request; anything else is a client route and gets the shell. Pure,
17 * zero heap, no stdlib, host-testable; the caller wires the result into serve_static / the router.
18 *
19 * ### The fallback HMI
20 *
21 * On a machine-control device the SPA is a convenience, not the contract. If the shell asset is
22 * missing (a half-finished upload, a wiped filesystem), the client will not run scripts, or the
23 * device itself is degraded, an operator still has to be able to see state and actuate something. So
24 * a client route can resolve to PROTOCORE_SPA_SERVE_FALLBACK instead: a plain server-rendered control page
25 * needing no JavaScript and no asset files. The API prefix keeps passing through in that mode - a
26 * fallback page whose endpoints have stopped answering is decoration.
27 *
28 * ### Conditional UI streaming
29 *
30 * That page is assembled from fragments, each with a predicate, and streamed in caller-sized chunks:
31 * only the panels whose condition currently holds are emitted, and a page far larger than any single
32 * buffer never has to fit in RAM. The same streamer serves any conditional UI - showing an operator
33 * only the panels their role, or the machine's current state, warrants.
34 */
35
36#ifndef PROTOCORE_SPA_ROUTER_H
37#define PROTOCORE_SPA_ROUTER_H
38
39#include "protocore_config.h" // the entry point: protocore_types.h for the widths
40
41#if PROTOCORE_ENABLE_SPA_ROUTER
42
44
45// This module holds nothing between calls, so it carves no borrow and states none. An entry
46// takes one all the same, and never reads it, so every namespace in the tree is invoked the
47// same way.
48
49/** @brief What to do with a request path. */
50typedef enum PROTO_ENUM_PACKED
51{
52 PROTOCORE_SPA_SERVE_FILE, ///< a real asset (has a file extension): serve it statically.
53 PROTOCORE_SPA_SERVE_SHELL, ///< a client route (extensionless): serve the SPA shell (index.html).
54 PROTOCORE_SPA_PASSTHROUGH, ///< under the API prefix: let the app's handlers run.
55 PROTOCORE_SPA_SERVE_FALLBACK, ///< a client route the SPA cannot serve: serve the no-JS control page.
56} protocore_spa_action;
57
58/** @brief What the server currently knows about its ability to serve the SPA. */
59typedef struct
60{
61 const char *api_prefix; ///< paths under this always pass through; null/empty = none.
62 proto_bool shell_available; ///< is the shell asset actually present and servable?
63 proto_bool client_scripting; ///< will the client run the SPA (false = text browser, curl, no-JS)?
64 proto_bool degraded; ///< force the plain control page (recovery mode, failsafe, low memory).
65} protocore_spa_ctx;
66
67/** @brief Predicate deciding whether a fragment is part of this render. */
68typedef proto_bool (*protocore_ui_when_fn)(void *ctx);
69
70/** @brief One UI panel and the condition under which it is shown. Nothing is copied. */
71typedef struct
72{
73 const char *name; ///< label, for diagnostics; not emitted.
74 const char *html; ///< the fragment body (borrowed).
75 protocore_ui_when_fn when; ///< nullptr = always included.
76} protocore_ui_fragment;
77
78/** @brief Cursor over a fragment set. Resumes mid-fragment, so output is chunk-size independent. */
79typedef struct
80{
81 const protocore_ui_fragment *frags;
82 size_t count;
83 void *ctx; ///< passed to each predicate.
84 size_t idx; ///< next fragment to consider.
85 size_t off; ///< bytes of the current fragment already emitted.
86 proto_bool done;
87} protocore_ui_stream;
88
89/** @brief What has_extension takes: path. */
90typedef struct
91{
92 const char *path;
93} SpaRouterHasExtensionArgs;
94
95/** @brief What route takes: path, api_prefix. */
96typedef struct
97{
98 const char *path; ///< the request path (e.g. "/devices/42", "/app.js", "/api/state")
99 const char *api_prefix; ///< a prefix whose paths pass through to handlers (e.g. "/api/"); null/empty = none
100} SpaRouterRouteArgs;
101
102/** @brief What route_ex takes: path, ctx. */
103typedef struct
104{
105 const char *path;
106 const protocore_spa_ctx *ctx;
107} SpaRouterRouteExArgs;
108
109/** @brief What ui_stream_begin takes: s, frags, count, ctx. */
110typedef struct
111{
112 protocore_ui_stream *s;
113 const protocore_ui_fragment *frags;
114 size_t count;
115 void *ctx;
116} SpaRouterUiStreamBeginArgs;
117
118/** @brief What ui_stream_next takes: s, out, cap. */
119typedef struct
120{
121 protocore_ui_stream *s;
122 char *out;
123 size_t cap;
124} SpaRouterUiStreamNextArgs;
125
126/** @brief What ui_stream_done takes: s. */
127typedef struct
128{
129 const protocore_ui_stream *s;
130} SpaRouterUiStreamDoneArgs;
131
132/**
133 * @brief Single-page-app micro-routing + conditional UI streaming (PROTOCORE_ENABLE_SPA_ROUTER). A single-page web UI
134 * ...
135 *
136 * A caller sets the members a call takes, invokes it through ::SpaRouter with the bytes it runs
137 * out of, and reads the outcome off the same handle.
138 *
139 * SpaRouter.has_extension_args.path = ...;
140 * SpaRouter.has_extension(work);
141 * // SpaRouter.ok is what the call reports
142 *
143 * @var SpaRouterNs::has_extension_args what has_extension takes: path
144 * @var SpaRouterNs::route_args what route takes: path, api_prefix
145 * @var SpaRouterNs::route_ex_args what route_ex takes: path, ctx
146 * @var SpaRouterNs::ui_stream_begin_args what ui_stream_begin takes: s, frags, count, ctx
147 * @var SpaRouterNs::ui_stream_next_args what ui_stream_next takes: s, out, cap
148 * @var SpaRouterNs::ui_stream_done_args what ui_stream_done takes: s
149 * @var SpaRouterNs::ok a call's true/false outcome
150 * @var SpaRouterNs::action the routing action. "/" (or empty) serves the shell. A path ...
151 * @var SpaRouterNs::n bytes written; 0 when the stream is finished (or on bad args)
152 * @var SpaRouterNs::has_extension true if the last path segment has a file extension (a '.' after the ...
153 * @var SpaRouterNs::route decide how to route path for a single-page app
154 * @var SpaRouterNs::route_ex decide how to route path, choosing the fallback HMI when the SPA ...
155 * @var SpaRouterNs::ui_stream_begin start streaming frags, evaluating each predicate against ctx. ...
156 * @var SpaRouterNs::ui_stream_next emit up to cap bytes of the remaining included fragments into out. ...
157 * @var SpaRouterNs::ui_stream_done true once every included fragment has been emitted
158 *
159 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
160 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
161 * a caller drives every namespace the same way.
162 */
163typedef struct
164{
165 SpaRouterHasExtensionArgs has_extension_args;
166 SpaRouterRouteArgs route_args;
167 SpaRouterRouteExArgs route_ex_args;
168 SpaRouterUiStreamBeginArgs ui_stream_begin_args;
169 SpaRouterUiStreamNextArgs ui_stream_next_args;
170 SpaRouterUiStreamDoneArgs ui_stream_done_args;
171 proto_bool ok;
172 protocore_spa_action action;
173 size_t n;
174} SpaRouterVars;
175
176/** @brief The operands and the outcome. */
177extern SpaRouterVars SpaRouterV;
178
179/** @brief The entries. */
180typedef struct
181{
182 void (*const has_extension)(uint8_t *work);
183 void (*const route)(uint8_t *work);
184 void (*const route_ex)(uint8_t *work);
185 void (*const ui_stream_begin)(uint8_t *work);
186 void (*const ui_stream_next)(uint8_t *work);
187 void (*const ui_stream_done)(uint8_t *work);
188} SpaRouterNs;
189
190// What the table binds, defined once in the .c and taking one parameter each: everything
191// else an entry needs is an operand in SpaRouterV or a region of the borrow at a fixed offset.
192void protocore_spa_router_has_extension(uint8_t *work);
193void protocore_spa_router_route(uint8_t *work);
194void protocore_spa_router_route_ex(uint8_t *work);
195void protocore_spa_router_ui_stream_begin(uint8_t *work);
196void protocore_spa_router_ui_stream_next(uint8_t *work);
197void protocore_spa_router_ui_stream_done(uint8_t *work);
198
199// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
200// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
201// `SpaRouter.has_extension(work)` resolves to a named function and becomes a DIRECT call. An extern table
202// leaves the call indirect and the symbol live at every level, -O2 -flto included.
203static const SpaRouterNs SpaRouter __attribute__((unused)) = {
204 .has_extension = protocore_spa_router_has_extension,
205 .route = protocore_spa_router_route,
206 .route_ex = protocore_spa_router_route_ex,
207 .ui_stream_begin = protocore_spa_router_ui_stream_begin,
208 .ui_stream_next = protocore_spa_router_ui_stream_next,
209 .ui_stream_done = protocore_spa_router_ui_stream_done,
210};
211
213
214#endif // PROTOCORE_ENABLE_SPA_ROUTER
215
216#endif // PROTOCORE_SPA_ROUTER_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97