|
ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
|
Layer: L7 Application ยท Build flags: PC_ENABLE_SPA_ROUTER
A device UI that is only a JavaScript bundle has a failure mode nobody plans for: the shell asset is missing after a half-finished upload, the filesystem got wiped, the browser will not run scripts, or the device came up degraded. On a machine you can actuate, "the page did not load" is not an acceptable state - an operator still has to see what is happening and be able to stop it.
pc_spa_route_ex() routes for that:
| Path | Action | When |
|---|---|---|
/api/... | PASSTHROUGH | always - including in fallback mode |
/app.js | SERVE_FILE | always - a missing asset is a real 404, not a rewrite |
/dashboard | SERVE_SHELL | the SPA can serve it |
/dashboard | SERVE_FALLBACK | shell missing, client will not script, or device degraded |
Two of those rows are the whole design:
/api/stop. A fallback whose endpoints have stopped answering is decoration./style.css would hand the browser a document where it asked for a stylesheet.The fallback page is a fragment table, each entry with a predicate:
pc_ui_stream_next() emits only the fragments whose predicate holds, into a buffer of any size - it resumes mid-fragment, so a page far larger than the buffer streams out in pieces. Predicates run as the stream reaches each fragment, not all up front, so a long render reflects the state that holds when it gets there.
This example deliberately streams through a 48-byte window to prove that.
HW-verified (2026-07-19) on an ESP32-S3:
Step 5 is the one to read closely. Actuating through the API in fallback mode flipped the alarm, and on the next render the ALARM ACTIVE panel appeared while Status: normal disappeared - two predicates changing in opposite directions. That is conditional streaming doing its job, and it is only observable because the API kept working when the SPA did not.
There is no reliable header for "will this client run my JavaScript", so it is the application's call. This example uses an explicit ?nojs=1 because that is testable; a real build might also weigh the Accept header, a stored user preference, or a probe endpoint the shell hits on load. What the router guarantees is only that whatever you decide is applied consistently.
| Route | What it does |
|---|---|
/ | shell, or the fallback page |
/dashboard | same (a client-side route) |
/?nojs=1 | force the fallback |
/api/state | passes through, in either mode |
/api/stop | toggles the alarm - actuation from the fallback page |
/degrade | toggle the degraded flag |
/shell | toggle whether the shell asset is "present" |
| Board | Flash |
|---|---|
| ESP32 | 754,565 B (57%) |
| ESP32-S3 | 908,447 B (69%) |
The flags must reach the library build, so pass them as build flags: