SSR / foldkit-ssr
Forms that work without scripts
A form whose OnSubmit names a Message can be answered by the server, before
any script has run or with scripts blocked, because update is pure and the
server can run it. The plan opts in:
const Todos = SSR.plan(App, {
id: 'todos',
state: Projection.pick(App.model.draft, App.model.todos),
surfaces: [Surface.at(TodoList, undefined)],
fallback: 'server',
})rh.form(
[rh.OnSubmit(Message.Added({ title: model.draft }))],
[rh.input([rh.Name('title'), rh.Value(model.draft), rh.OnInput(Message.Typed)])],
)The Message carries the title because a post without scripts fills it: the
server starts from init, whose draft is empty. With scripts on, it is the
Message as drawn, so a submit within a frame of the typing sends the text
drawn before it (docs/state-model.md).
A form that need not work without scripts sends Added() and reads
model.draft in update instead.
With scripts on, nothing changes: the page answers the submit and the form
never posts. Without them, the server wrote the form to post the Message to the
page's own URL, and SSR.entry hands that post to SSR.handle:
POST ──▶ decode the Message ──▶ set the posted fields into it (the typed `title`)
──▶ init, then boot and its Commands ──▶ update, and every Command after it
──▶ the page, renderedThe Message must be one the page's active Surfaces list. Commands run under the
config's resources Layer, as Foldkit's runtime would provide them, and under
the post's own, which SSR.entry's resources: request => Layer builds once
for it: who is asking, a client bound to them. Where both provide a service,
the post's is the one its Commands get, so a client bound to the request
replaces one bound to nobody. A render runs no Command; it reads the request
through flags. A post the
server cannot use is answered 400 with the reason (FallbackRefused); a
Command that fails is answered 500. The browser's loop runs for the life of
the page, so a Command that reschedules itself, a poll or a tick, is ordinary
there; on the server a post that has not settled within 100 Messages and
Commands is answered 500, naming the last Command. SSR.handle(request,
config, plan, { buildId, flags?, resources? }) can also be called from an
entry of your own.
A form a cookie session authorizes is a cross-site request forgery target,
and a form cannot carry a CSRF header. SSR.entry's origins names the
origins a form may be posted from; a post whose Origin (every browser sends
one on a POST) is not among them is answered 403 before its body is read
or flags and resources run. A page served with Referrer-Policy:
no-referrer posts Origin: null even to its own site; such a post is taken
when its Sec-Fetch-Site, which no page or form can set, says
same-origin. A plan with a server fallback requires origins, since a post
runs flags, the config's resources and its own as whoever the cookie says
posted: an entry made without them throws. An entry of your own that calls
SSR.handle checks Origin itself.