Scheduling and instrumentation
Prioritize browser updates and observe server route lifecycles through the public experimental runtime tooling boundary.
Understand the priority queue
Scheduling APIs are public experimental exports from @tavojs/core/dev. They change when a connected component update renders; they do not make synchronous business logic asynchronous and they do not prioritize network requests.
| API / contract | Type / boundary | Default / result | Behavior |
|---|---|---|---|
immediate | UpdatePriority | explicit | Queues work that flushSync drains before returning from its callback. |
user-blocking | UpdatePriority | explicit | Higher-priority interactive work flushed with the normal microtask queue. |
normal | UpdatePriority | current default | Connected component updates flush in a microtask. |
background | UpdatePriority | startTransition | Non-urgent updates flush from a timer turn. |
idle | UpdatePriority | explicit | Uses requestIdleCallback when available and a short timer fallback otherwise. |
A component queued more than once keeps its highest requested priority and renders once for the accumulated state.
runWithUpdatePriorityrestores the previous priority in a finally block, including after a callback throws.getCurrentUpdatePriorityreports the active callback priority; outside an override it reports normal.
tsimport {
getCurrentUpdatePriority,
runWithUpdatePriority,
} from "@tavojs/core/dev";
runWithUpdatePriority("user-blocking", () => {
console.debug(getCurrentUpdatePriority()); // "user-blocking"
updateKeyboardSelection();
});Defer a non-urgent result update
Keep the controlled input update urgent and defer only the replaceable result rendering.
Cancellation and stale-result handling still belong to the controller or resource that owns asynchronous work.
Do not use a transition for accessibility state that must be announced immediately.
tsximport { TavoController, createTavo } from "@tavojs/core";
import { startTransition } from "@tavojs/core/dev";
import { Input, Stack, Text } from "@tavojs/ui";
type SearchState = {
query: string;
visibleProjects: string[];
};
class ProjectSearchController extends TavoController {
updateQuery(query: string) {
this.model.patch({ query });
startTransition(() => {
this.model.patch({
visibleProjects: filterProjects(query)
});
});
}
}
export const ProjectSearch = createTavo<{}, SearchState>({
model: function createSearchState() {
return {
query: "",
visibleProjects: []
};
},
controller: ProjectSearchController,
view: function ProjectSearchView({ state, controller }) {
return (
<Stack gap="sm">
<Input
value={state.query}
onInput={(event) => {
controller?.updateQuery(event.currentTarget.value);
}}
/>
<Text>{state.visibleProjects.length} projects</Text>
</Stack>
);
}
});Flush only when browser ordering requires it
flushSync runs its callback at immediate priority and drains immediate connected component work before returning. It is appropriate when the next statement must observe the updated DOM, such as measurement or focus handoff.
tsimport { flushSync } from "@tavojs/core/dev";
export function openAndFocus(
open: () => void,
focusPanel: () => void
): void {
flushSync(() => {
open();
});
focusPanel();
}Verify the user-visible effect
Use runtime devtools to confirm that pending updates return to zero after the interaction.
Test that urgent input remains responsive while transition work is pending.
Test focus or measurement code in a real DOM environment; a server string render cannot verify browser ordering.
Avoid tests that depend on exact timer milliseconds. Assert final state and ordering instead.
Read the lifecycle event contract
| API / contract | Type / boundary | Default / result | Behavior |
|---|---|---|---|
name | route.resolve | middleware | loader | action | cache | operation-specific | Identifies the framework operation being observed. |
phase | start | end | error | abort | hit | miss | invalidate | operation-specific | Identifies lifecycle progress, cancellation, or a cache outcome. |
timestamp / durationMs | number | Date.now / terminal only | Provides wall-clock correlation and elapsed time. |
requestId / route / layer | string | when available | Correlates related work without including a URL query, headers, or data payload. |
status / count / cacheTags | bounded result metadata | when available | Reports HTTP and cache outcomes. |
error | unknown | error phase only | Potentially sensitive application object; adapters do not record it unless explicitly enabled. |
Framework events do not include request bodies, headers, cookies, tokens, loader results, or store state.
Listener failures are caught and never alter route behavior.
A custom
TavoInstrumentationimplementation receives the same isolation guarantee ascreateInstrumentation.
Create an isolated custom observer
Keep the observer synchronous and inexpensive. Buffer or enqueue slow exporter work outside the request path.
Use route patterns rather than raw pathnames as metric labels to avoid unbounded cardinality.
Do not throw from a listener to signal exporter failure; monitor the exporter separately.
tsimport "@tavojs/core/server-only";
import {
createInstrumentation,
type TavoInstrumentationEvent
} from "@tavojs/core/dev";
function recordRouteMetric(event: TavoInstrumentationEvent): void {
if (event.phase !== "end" || event.durationMs === undefined) {
return;
}
metrics.histogram("tavo.route.duration", event.durationMs, {
operation: event.name,
route: event.route ?? "unknown"
});
}
export const instrumentation = createInstrumentation(
function observeTavoEvent(event) {
recordRouteMetric(event);
}
);tsimport { defineConfig } from "@tavojs/core/config";
import { instrumentation } from "./instrumentation";
export default defineConfig({
ssr: {
instrumentation
}
});Adapt events to an OpenTelemetry tracer
accepts the stable subset shared by OpenTelemetry tracer implementations. Supply an adapter; its spans implement . Start events create spans. End, error, abort, hit, miss, and invalidate events finish the matching span or create a bounded terminal span when no start is pending.
Span names use
tavo.route.resolve,tavo.route.middleware,tavo.route.loader,tavo.route.action, ortavo.route.cache.Correlation uses request ID, event name, route, and layer. Concurrent matching starts are completed in order.
recordErrorsdefaults to false. Enable it only after application-level exception redaction is configured.Error and abort phases set an error status; normal and cache terminal phases set success.
tsexport type OpenTelemetrySpanLike = {
setAttribute?(
name: string,
value: string | number | boolean
): unknown;
recordException?(error: unknown): unknown;
setStatus?(status: {
code: number;
message?: string;
}): unknown;
end?(endTime?: number): unknown;
};
export type OpenTelemetryTracerLike = {
startSpan(
name: string,
options?: {
attributes?: Record<string, string | number | boolean>;
startTime?: number;
}
): OpenTelemetrySpanLike;
};tsimport "@tavojs/core/server-only";
import {
createOpenTelemetryInstrumentation
} from "@tavojs/core/dev";
import { tracer } from "./telemetry";
export const instrumentation = createOpenTelemetryInstrumentation(
tracer,
{
recordErrors: false
}
);Verify lifecycle and failure isolation
Assert start and terminal events share request, route, and layer identity.
Abort a navigation or disconnect a request and verify an abort event rather than a false success.
Make a test listener throw and verify the route still completes.
Keep telemetry setup behind a server-only boundary and never import a private exporter into browser code.
bashnpm run typecheck
npx tavo build
npx tavo preview --ssr
# Exercise one loader, one action, and one cache hit.
# Confirm the exporter receives terminal events and the responses are unchanged.Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.