Skip to content

Runtime protocol ​

This is an internal contract between the Crystal dispatcher and the browser runtime, not an application-level event or plugin API. Change both sides and their contract tests together. The native adapter supplies the transport; the dispatcher is independent of native webview handles.

Registration and transport ​

The handler emits a root with data-crykit-view. The browser parses its ordinary HTML. Elements that need managed bindings, callbacks, or HTML-ID lookup carry data-crykit-node; dynamic text uses paired crykit-text:ID comment markers. The interpreter registers these sparse targets in a lookup table per mount, then removes the text markers without merging adjacent text nodes.

Structural regions retain paired crykit-region:ID comments. Each compiled instance also emits a crykit-targets: comment containing a JSON map from IDs to element, text, or region. Registration checks that every declared target survived browser parsing with the expected type and remains inside its owning instance's range. Duplicate identities, missing targets, and malformed or crossing region boundaries reject registration before text markers change. Element, text, and region IDs are positive and share one namespace within a mount. The mount root is exposed through the interpreter's root property; it has no managed node ID. Reserved identities and anchors must not be changed by application scripts.

Mounted roots with stylesheet dependencies also carry data-crykit-stylesheets, an escaped JSON array of asset URL paths. Initial link[rel="stylesheet"][data-crykit-stylesheet] elements precede the root's compiled region and carry the asset URL in that attribute. Registration adopts these seed links into the document head, keeping them outside template ranges and target identities. Shared URLs use one managed head link with references from each consuming mount.

View module dependencies follow the same initial metadata pattern: data-crykit-scripts contains an escaped JSON array of asset URL paths, and direct-child script[type="module"][data-crykit-script] seed tags precede the compiled region. Registration validates and adopts the seeds into the document head. Module tags remain for the document lifetime; removing a dependency does not undo module evaluation. The browser module cache ensures one evaluation per URL even when a layout includes an ordinary module tag for the same asset.

Elements with callbacks also carry data-crykit-events with a sorted, space-separated list of their subscriptions: click, input, submit, keydown, and/or keyup. Input subscriptions require inputs or textareas, and submit subscriptions require forms. The interpreter validates this metadata and retains it privately. Ordinary attribute mutations cannot change this reserved marker; direct DOM marker changes do not alter the interpreter's subscriptions.

The on_keyboard template helper registers keydown and keyup subscriptions for one callback. There is no separate keyboard wire event.

Only events with a possible subscribed callback enter the browser queue. Clicks target the nearest enabled subscribed element, including when the clicked descendant is static HTML or SVG. Input events target the eligible input or textarea itself; disabled and readonly controls are excluded. Subscription checks happen when an event is captured. Already queued events retain their order if a later batch removes the callback; target identity and containment are still rechecked before sending. Crystal continues to authenticate requests and resolve current callbacks.

Keyboard events target the nearest enabled subscribed element within the same mount, including when the focused descendant has no managed identity. Readonly controls remain eligible. The runtime captures key metadata at event time and leaves browser default actions and propagation unchanged. Repeated keydowns and composition events are delivered without filtering.

The browser retains at most 1024 queued events, navigations, and update notifications (including startup notifications), plus one in-flight callback. Consecutive queued edits to the same input coalesce to their latest value; clicks, keyboard events, submits, navigations, and updates preserve ordering boundaries. Each payload is limited to 512 Ki UTF-16 code units and queued strings total at most 4 Mi code units. Exceeding these limits disposes the connection and reports event_payload_too_large or event_queue_full with reloadRequired: true, releasing the backlog instead of silently dropping callbacks. Mount update notifications remain deduplicated.

Owned document responses contain one script[type="application/json"] with data-crykit-document. Its JSON string is the document token. Native bootstrap connects only on the application origin and supplies a request function backed by the bound window.__crykit_request function.

CryKitRuntime.connect permits one connection attempt per document. Registration must succeed before readiness. The bootstrap emits crykit:ready on success; errors are logged and emitted through crykit:error with structured detail.

Document readiness ​

The ready request authenticates the document token and its mounted identities against the owning window/document. Successful registration creates an active session. The native bridge records readiness only after the adapter successfully sends that session's ready reply, then emits the public window :document_ready event outside the dispatcher on the UI callback context. The browser's crykit:ready follows processing of the reply; native delivery does not prove that browser notification has occurred.

The bridge rechecks session ownership, document generation and closure before delivery. Duplicate, invalid or foreign ready requests, including reuse of a consumed or expired document token, cannot create another notification or clear an otherwise healthy ready session. A failed reply does not publish readiness. window.document_ready? observes the bridge's recorded session and its continued presence in the dispatcher without expiring state or running cleanup. Document/session failure and expiry make it false; mount-scoped failure keeps the active document session ready.

Replacement navigation and reload clear readiness through the existing document lifecycle. Darwin invalidates at provisional navigation start, even if the new load fails. A replacement document must authenticate again and emits a new ready event in the same native window. Closure clears readiness. Raw HTML, external pages and unmounted local pages have no authenticated session. CSS stylesheet refresh retains the document and its readiness. No reload request, retry, automatic recovery, or asset/paint completion guarantee is added by this event.

Requests and responses ​

RequestMain fieldsOutcome
readydocument, mountsready with session and per-mount revisions
eventsession, sequence, view, revision, name, target; value for input; data for submit or keyboard eventsbatch, ignored, or error
updatesession, sequence, view, revisionDrain queued UI updates; batch, ignored, or error
acksession, sequence, view, revisionacknowledged, or error
failedsession, reasonInvalidate the session and release its document
navigatesession, urlBridge approval; navigate with session and boolean allowed, or error

Supported event names are click, input, submit, keydown, and keyup. Submit data is a JSON object of named string values or arrays of strings; other shapes are rejected before callback execution. Submit capture prevents native form navigation synchronously and snapshots the form's successful controls, including the submitter, before queueing. The response and mutation acknowledgement stay asynchronous, using the same ordering and revision rules as other events.

Keyboard data is an object containing string key and code, integer location (0–3), and boolean altKey, ctrlKey, metaKey, shiftKey, repeat, and isComposing. All fields are required, including false flags; key and code may be empty strings. Unknown fields are ignored. Malformed snapshots are rejected before callback execution or sequence consumption, including requests constructed through the typed protocol API.

Sequences, revisions, and node IDs cross the boundary as canonical unsigned decimal strings so that JavaScript number precision cannot corrupt Crystal UInt64 values. Sequences and event targets are positive; revisions start at zero. Document, session, and mount tokens are distinct identities and are checked against registry ownership.

The browser and dispatcher allow one pending batch acknowledgement per session. Sequences start at one and advance for each accepted event/update request; revisions advance per mount after handled work. A batch carries baseRevision, the new revision, and ordered mutations, plus the session, sequence, and view. It may also carry an ordered effects array. The browser applies all mutations, then the effects, before acknowledging the new revision. Even an empty handled batch must be acknowledged. ignored leaves the revision unchanged.

Requests must not be retried: callbacks may have already changed application state. Malformed or stale requests cannot be treated as fresh events.

Application navigation ​

For an ordinary same-window application link, bootstrap prevents the browser's default navigation and queues {type: "navigate", session, url} through the connection. Navigation waits behind earlier view callbacks and their complete batch/ACK exchanges, so approval sees the resulting application state. It does not consume an event sequence number or change a mount revision. Repeated link attempts while approval is pending are coalesced.

The native bridge authenticates its recorded ready session and validates the URL against the installed application scheme, host, and port, excluding credentials. Requests arriving while a batch still awaits acknowledgement are rejected before any navigation listener runs. The bridge removes the reserved __crykit_navigation parameter before emitting the window's :navigating event, whose cancel method vetoes its navigation request. Those synchronous callbacks run outside the dispatcher and bridge lock. The bridge rechecks document/session ownership and closure after approval, then replies with {type: "navigate", session, allowed}. A cancelled or stale request cannot replace the current document.

Only a matching approved reply permits bootstrap to add the current navigation credential and assign the destination. Cancellation retains the document and resumes queued work. Disposal invalidates pending approval. Fragment-only links, modified clicks, downloads, other browsing targets, and external destinations are not mediated by this request; programmatic navigation, reload, and history also remain outside this boundary. Unmounted documents have no active view guard.

Mutation vocabulary ​

MutationFields beyond typeEffect
SetTexttarget, valueSet a managed text node's content
SetAttributetarget, name, value, optional trustedURLSet an element attribute, with input and textarea value handling; URL schemes are validated unless the value explicitly carries trustedURL: true
RemoveAttributetarget, nameRemove an element attribute
InsertRangeparent, before, htmlParse and insert one child region before a direct sibling or at the end
RemoveRangetargetRemove a region, its anchors, and its owned targets
MoveRangeparent, before, targetMove an existing direct child region, retaining its DOM identities
SetStylesheetsstylesheetsReplace this mount's complete stylesheet URL dependency list
SetScriptsscriptsPublish this mount's complete module URL dependency list and load new modules

SetStylesheets.stylesheets is an array of application asset URL paths. This mutation has no node target: it reconciles the issuing mount's references to managed head links. Snapshots follow committed view render order and declaration order, with duplicate URLs included once. A stylesheet remains active while any mount references it. Surviving links retain their cascade position and newly needed links append after existing styles; releasing the final reference removes the managed link. Document disposal releases the remaining references. These links participate in CSS asset hotreload; their selectors remain document-wide.

SetScripts.scripts is an array of application asset URL paths collected in committed view render order and declaration order, without duplicate URLs. The runtime loads new modules asynchronously and retains a document-wide record of every requested URL, including failed loads. An empty snapshot does not unload code. Imports express module evaluation dependencies; declaration order alone does not order dynamically loaded modules. Module readiness is outside the batch/ACK contract. Load failures emit crykit:script-error with {src} and do not fail an otherwise valid mount.

Region parent and target fields are positive IDs encoded as strings. before is always present: a positive sibling-region ID encoded as a string, or JSON null to append immediately before the parent's closing anchor. A sibling anchor must belong directly to the same region and DOM parent. Moves remain within the same parent region. Stable anchors let the interpreter place ranges without searching or numbering all of the parent's siblings.

InsertRange.html contains exactly one outer child region whose anchors are the payload's first and last nodes. The interpreter parses it in its browser parent's context, including HTML tables and SVG. It stages and validates target IDs, manifests, anchors, and event metadata before inserting the fragment. Inserted regions cannot introduce a nested mount. Removing a region retires its element, text, nested-region, and callback registrations. Moving it retains the original nodes and preserves focused control and editable-text selections.

Scalar text holes are rejected in raw-text/RCDATA elements and structural table containers. Static markup remains browser-owned; target manifests catch managed nodes that parsing discards or moves outside their owning region. Callback replacement with unchanged subscription presence updates Crystal's retained callback definition without a browser mutation. Changed presence uses a new template shape and replaces the affected range.

String bindings render input values as escaped attributes and textarea values as escaped native text. Textarea serialization preserves the browser's leading newline behavior; its native text is not a managed text target. Value updates set the live control property, preserve focused selection and its direction, and protect newer queued edits until their callbacks complete. An input response acknowledges the server's canonical value even when it equals the previous server value.

DOM effects ​

The optional batch effects field contains one-time operations applied in order after the complete mutation list and before ACK:

EffectFields beyond typeBehavior
Focustarget, preventScrollFocus the managed element; preventScroll is a JSON boolean
SelectTexttargetSelect all text in a managed selectable text input or textarea without independently requesting focus
Reanimatetarget, subtreeRestart currently associated CSS animations; subtree is a required JSON boolean

Effect target values use internal managed node IDs, encoded as positive canonical UInt64 decimal strings. Application HTML IDs are resolved within the mount before serialization; CSS selectors and document-global lookup are not part of the protocol. Missing or removed targets are ignored. Effects do not change retained view state and are not repeated on later batches. Unsupported controls are ignored.

Reanimate replays retained CSS animation objects from their beginning, including pseudo-element animations. When subtree is true, it includes light DOM descendants in the same mount and document. CSS transitions, imperative Web Animations API effects, shadow roots, and nested mounts are excluded. Completed animations need animation-fill-mode: forwards or both to remain available for replay. The effect does not override animation: none or reduced-motion styles.

Background notifications ​

The bridge calls CryKitRuntime.requestUpdate(session, view) to signal pending work. The connection queues an update request alongside events and deduplicates queued notifications for a mount. The dispatcher suppresses repeat notifications until the update request is accepted, then can announce any remaining backlog. Notifications arriving before readiness are retained for the matching session.

Failures and timeouts ​

error responses contain a code and reloadRequired, plus validated identity fields where applicable. Runtime error callbacks additionally distinguish mount and document scope. mount_failed stops work for that mount while allowing other mounts to continue; invalid session state terminates the session.

Pending document readiness expires after 60 seconds on the dispatcher. Pending batch acknowledgements expire after 10 seconds. Document completion registers directly on the owning UI thread. Window closure invalidates owned documents and releases their mounts. Publication and lifecycle hooks run outside the bridge lock. The expiry scheduler posts work to the UI even when no browser request arrives.

The browser request timeout defaults to 10 seconds. Native bootstrap passes callbackTimeoutMs: null for event, queued update, and navigation approval requests, allowing UI callbacks to await a synchronous modal dialog. This includes job.notify, stream, and channel callbacks. Readiness, acknowledgements, and other control requests remain bounded. Document disposal cancels outstanding work. Custom adapters can set callbackTimeoutMs to a deadline in milliseconds.

The connection validates mount roots during operation and disposes on page hide. DOM application failures can leave a partially applied batch; the runtime stops and reports failure rather than attempting rollback. reloadRequired is a reported recovery requirement, not an automatic page reload.

Bundle and source contracts ​

Events::Protocol defines Crystal envelopes; Events::Dispatcher enforces their state machine. Browser events implement the connection, and the interpreter validates scalar mutations and effects. TemplateDOM registers compiled targets and applies range operations. Template sessions publish the Crystal binding and region changes that produce these batches. Transport::Bridge owns native injection and bootstrap.

The shipped runtime is one committed JavaScript bundle embedded through ECR. Application builds do not compile TypeScript. See Contributing for regeneration, freshness checks, browser tests, and byte-for-byte consumer verification.

Built with Crystal. Documented with VitePress.