Resumable pages: answering before the runtime boots
Everything above boots Foldkit's runtime as soon as the page loads. A resumable
page waits: it answers clicks and typing from the server's markup, and boots
only when it must, so the first interaction does not wait for the application's
code to start.
server each handler ──▶ its Message, encoded ──▶ a marker on the element + an entry in the envelopebrowser event ──▶ the markers name its Messages ──▶ queued ──▶ the runtime boots ──▶ update runs the queue first
It rests on the fact that a Foldkit handler is already a Message value.
Resume.builder(h) is h that writes those values into the server's markup:
ts
constview=(model:Model, h:HtmlBuilder<Message>)=>{const rh = Resume.builder(h)return{title:'Post',body: rh.main( [], [ rh.button([rh.OnClick(Message.Liked({id: model.post.id }))], ['Like']),// A Message with a hole: the event fills `value`. rh.input([rh.Value(model.draft), rh.OnInput(Message.ChangedDraft)]),// Fixed fields beside the hole: the event fills `title`. rh.input([rh.OnChange(Message.Renamed,{id: model.post.id })]),// A key event fills `key` and `modifiers`. rh.div([rh.OnKeyDown(Message.Pressed)], []), ], ),}}
rh differs from h in one place: OnInput, OnChange, OnKeyDown and
OnKeyUp also take a Message's own constructor, and the event fills the field
it leaves open. The types check it where it is written: the member must be one
of the view's Messages, and leave exactly one string field, or exactly key
and modifiers. A closure still works. It is not data, so the page cannot
answer that event itself and boots on it instead, letting the live page answer.
SSR.render names each such element and event in its result's unnamed.
So does a hole form whose field has checks the empty placeholder fails, such
as Schema.isMinLength(1): it cannot be written into the page as data, so it
is treated as a closure.
A Surface renderer gets the same builder through Resume.view, and its rh
makes only that Surface's Messages:
const Post = SSR.plan(App,{id:'post',state: Projection.pick(App.model.post),surfaces: [PostActionsAt],start:'on-interaction',// or 'idle'; 'now', the default, boots on load})
The event that boots the page is not lost and does not count twice. The
runtime starts from the resumed Model, so its first render is the served
markup and every node is adopted, the input typed into included. The Messages
answered before boot then go through the same update, in order, ahead of
anything the live page answers, even an event dispatched in the task that
boots it, so the Model ends where an eager boot would have taken it.
Its Surfaces. A Surface's messages list is what the page may dispatch
before boot. A binding whose Message no active Surface lists is refused on
the server, naming the element and the Message, and the browser refuses a
page that carries one.
What may start late. A Subscription or Managed Resource that starts at
boot starts later on a deferred page, which can change what the application
does. SSR.render refuses a deferred plan while one would be active,
naming each, until the plan declares it:
ts
const Post = SSR.plan(App,{id:'post',state: Projection.pick(App.model.post),parts: [Remote.resume(Data)],// Remote's entries may start late; its part says sostart:'idle',deferrable: ['tick'],// this application's own clock may start late})
For the check to see them, pass the application's subscriptions and
managedResources in the config given to SSR.render, as you would to
Foldkit. boot Commands run at boot, so a deferred page's Mirror.kv
restore waits for the first interaction.
The server loads the code before it renders, so the page carries the real
view. The browser loads it before it boots and answers from the markers
meanwhile, whatever start says. A binding inside a placement dispatches the
parent's Message, as Foldkit's own handlers do, so the plan's Surface lists the
wrapper variant, Message.GotUploadMessage.