Navigated to /docs/core/scheduling-and-instrumentation

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 / contractType / boundaryDefault / resultBehavior
immediateUpdatePriorityexplicitQueues work that flushSync drains before returning from its callback.
user-blockingUpdatePriorityexplicitHigher-priority interactive work flushed with the normal microtask queue.
normalUpdatePrioritycurrent defaultConnected component updates flush in a microtask.
backgroundUpdatePrioritystartTransitionNon-urgent updates flush from a timer turn.
idleUpdatePriorityexplicitUses 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.

  • runWithUpdatePriority restores the previous priority in a finally block, including after a callback throws.

  • getCurrentUpdatePriority reports the active callback priority; outside an override it reports normal.

TS
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.

TSX
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.

TS
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 / contractType / boundaryDefault / resultBehavior
nameroute.resolve | middleware | loader | action | cacheoperation-specificIdentifies the framework operation being observed.
phasestart | end | error | abort | hit | miss | invalidateoperation-specificIdentifies lifecycle progress, cancellation, or a cache outcome.
timestamp / durationMsnumberDate.now / terminal onlyProvides wall-clock correlation and elapsed time.
requestId / route / layerstringwhen availableCorrelates related work without including a URL query, headers, or data payload.
status / count / cacheTagsbounded result metadatawhen availableReports HTTP and cache outcomes.
errorunknownerror phase onlyPotentially 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 TavoInstrumentation implementation receives the same isolation guarantee as createInstrumentation.

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.

TS
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);
  }
);
TS
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, or tavo.route.cache.

  • Correlation uses request ID, event name, route, and layer. Concurrent matching starts are completed in order.

  • recordErrors defaults 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.

TS
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;
};
TS
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.

BASH
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.