Reactivity (PulsePoint)
PulsePoint is the default reactive frontend layer for Caspian. In the
current runtime, you author plain HTML plus a plain
<script>
inside a single template root, and Caspian injects the runtime-facing
attributes before the browser mounts the page.
Authored vs Runtime
Authored templates
- Prefer one native root element — the only shape that can receive props.
- Use a plain
<script>inside the template when needed. - Use PulsePoint helpers like
pp.state,pp.effect, andpp.rpc. - Do not handwrite
pp-componentortype="text/pp".
Runtime output
- The browser sees mounted roots with
pp-component. - Owned scripts are rewritten to
script[type="text/pp"]. - The global
ppruntime auto-mounts on DOM ready. - Those runtime details explain behavior, but they are not the default authored form.
Where the script goes
Keep the owned <script>
inside the template it belongs to. Sibling top-level nodes are allowed —
they make a component a fragment,
and a .py page or
layout gets a display: contents
host — and the boundary still covers every root, so one script is enough.
A fragment has no root element, so it cannot receive props.
Core APIs
pp.state(initial)
pp.effect(callback, deps?)
pp.layoutEffect(callback, deps?)
pp.ref(initialValue?)
pp.memo(factory, deps?)
pp.callback(fn, deps?)
pp.reducer(reducer, initial)
pp.context(token)
pp.portal(ref, target?)
pp.id()
pp.errorBoundary()
pp.syncExternalStore(...)
pp.imperativeHandle(...)
pp.transition()
pp.deferredValue(value)
pp.optimistic(state, reducer?)
pp.props
pp-for on <template>
pp-ref
pp-spread / pp-style
pp.createContext,
pp.mount,
pp.redirect,
pp.rpc,
pp.socket, and the
perf helpers
pp.enablePerf,
pp.disablePerf,
pp.getPerfStats,
and
pp.resetPerfStats.
<section> <h2>title</h2> <p>Count: count</p> <button onclick="setCount(count + 1)"> Increment </button> <script> const title = "Counter" = pp.props; const [count, setCount] = pp.state(0); pp.effect(() => console.log(count), [count]); </script> </section>
Browser To Server
Use pp.rpc()
as the default bridge from interactive UI to route or backend
@rpc()
actions. Route interactions use this typed request/response bridge directly.
async function saveProfile() const user = await pp.rpc("update_user", name: "Jeff" ); const file = fileInput.files[0]; await pp.rpc("upload_avatar", avatar: file ); await pp.rpc("search", query , true);
- Redirect headers are honored through
pp.redirect(). - Streaming and upload progress use the options form of
pp.rpc():onStream,onStreamComplete,onStreamError, andonUploadProgress. -
The third argument also accepts a bare boolean as shorthand. The
searchcall above is{ abortPrevious: true }, which cancels the previous in-flight call — the usual choice for type-ahead. - For CRUD views, keep the authoritative list in
pp.state(...)and render it withpp-for.
Named WebSockets
Use @socket() and
pp.socket() when the browser and server must
both send values over a persistent channel. This is the right shape for
presence, collaboration, live chat, and multiplayer state. Enable it with
"websocket": true in
caspian.config.json and run the Caspian project
update workflow before adding socket code.
from src.lib.websocket.sockets import Socket, socket @socket(require_auth=True) async def team_feed(team_id: str, socket: Socket): while (message := await socket.recv()) is not None: sent = await socket.send( "teamId": team_id, "message": message, ) if not sent: break
const feed = pp.ref(null); const [events, setEvents] = pp.state([]); pp.effect(() => feed.current = pp.socket( "team_feed", team_id: "alpha" , onMessage: (value) => setEvents((current) => [...current, value]), onError: (error) => console.error(error.message), ); return () => feed.current.close(); , []); function sendUpdate() feed.current.send( type: "refresh" );
Lifecycle contract
- Open the named socket once inside
pp.effect(..., []). - Keep the returned handle in
pp.ref(...). - Close it in the effect cleanup when the component is disposed.
- Use
send(value),close(), andreadyStateon the handle.
Connection policy
require_auth=Trueprotects authenticated channels.allowed_roles=[...]adds socket-level RBAC.- Origin, capacity, message-size, rate, and idle-timeout checks run at the shared endpoint.
- Client arguments are filtered against the socket function signature.
| Interaction | Caspian transport |
|---|---|
| Forms, CRUD, uploads, button actions | pp.rpc() |
| One request with progressive output | RPC streaming |
| Persistent two-way channel | pp.socket() |
URL And Navigation
Use native URLSearchParams
when you need to read the current query string. For navigation, use
pp.redirect()
for programmatic moves and keep standard <a>
links for normal route navigation.
const params = new URLSearchParams(window.location.search); const tab = params.get("tab") || "overview";
function goHome() pp.redirect("/dashboard"); function goExternal() pp.redirect("https://google.com");
pp-reset-scroll="true"
on the main content pane when that pane should reset on child-route
navigation while sidebars or rails keep their scroll position.
Context And Refs
The current context API follows a React-style provider pattern:
pp.createContext(...),
a lowercase provider tag such as
<themecontext.provider value="{theme}">
carrying a value expression, and
pp.context(token).
Tag matching is case-insensitive, so a lowercase tag still resolves a
script binding named ThemeContext. There is no
pp-context
attribute, and a component never reads the value it provides in the same
render. pp.createContext(defaultValue)
creates the token and pp.context(token)
reads it from ancestors — that pair is the complete context API for
providers and consumers.
<div> <input pp-ref="nameInput" /> <button onclick="nameInput.current?.focus()">Focus</button> <script> const nameInput = pp.ref(null); </script> </div>
For Caspian-specific authoring rules, prefer the installed runtime and the packaged Caspian docs over older examples or third-party snippets.