Services and dependency lifetimes
Register typed application dependencies, resolve them through controllers, and keep process-wide services free of request identity.
Choose the dependency owner first
Use a
createTavomodel for reactive state owned by one mounted component.Use a global
Storefor observable browser state shared across multiple component or route owners.Use an application service for a stable dependency such as a client SDK, stateless formatter, telemetry sink, or feature client.
Use a plugin capability for behavior owned and declared by a Plugin API v1 installation.
Use loader, middleware, action, endpoint, or session context for the current request, user, tenant, token, or permissions.
tsimport {
createServiceKey,
registerService,
} from "@tavojs/core";
export type Clock = {
now(): Date;
};
export const clockKey = createServiceKey<Clock>("app.clock");
registerService(clockKey, {
now: () => new Date(),
});Register and resolve a typed service
createServiceKey carries the service type while retaining a stable string name. Register once in the runtime that owns the dependency with registerService, then resolve by the same key.
getServiceand controller services.get throw when the identifier is missing.tryGetServiceand controller services.tryGet return undefined for an optional dependency.hasServicechecks one identifier;listServicesreturns every registered name.String names are supported, but a typed key keeps registration and lookup aligned without repeated generic arguments.
tsimport {
createServiceKey,
getService,
hasService,
registerService,
} from "@tavojs/core";
export type MetricsService = {
increment(name: string): void;
};
export const metricsKey =
createServiceKey<MetricsService>("app:metrics");
function createMetricsService(): MetricsService {
return {
increment(name) {
navigator.sendBeacon(
"/api/metrics",
JSON.stringify({ name }),
);
},
};
}
export function installMetricsService(): MetricsService {
if (!hasService(metricsKey)) {
registerService(metricsKey, createMetricsService());
}
return getService(metricsKey);
}tsximport { bootTavo } from "@tavojs/core";
import { installMetricsService } from "./services/metrics";
installMetricsService();
void bootTavo().catch((error: unknown) => {
console.error("Tavo.js failed to start.", error);
});tsximport {
createTavo,
TavoController,
} from "@tavojs/core";
import { metricsKey } from "../services/metrics";
class TrackedButtonController extends TavoController {
track() {
this.services.get(metricsKey).increment("project_created");
}
}
export const TrackedButton = createTavo({
controller: TrackedButtonController,
view({ controller }) {
return (
<button
type="button"
onClick={() => {
controller?.track();
}}
>
Create project
</button>
);
},
});Make replacement intentional
registerServicereturns the same service instance it registers.A duplicate name still replaces the previous value. Core warns once unless { override: true } declares that replacement is intentional.
The default i18n service uses the reserved
tavo:i18n name and participates in framework discovery.unregisterServiceandclearServicesare development/testing helpers from@tavojs/core/dev; use them to isolate tests rather than as normal application lifecycle.
tsimport {
clearServices,
unregisterService,
} from "@tavojs/core/dev";
import {
metricsKey,
installMetricsService,
} from "../src/services/metrics";
export function beforeEachServiceTest() {
clearServices();
installMetricsService();
}
export function afterEachServiceTest() {
unregisterService(metricsKey);
}Treat the registry as process-wide during SSR
The service registry lives on globalThis. A Node process can serve concurrent requests through the same service instance, so registration during SSR emits a warning that asks you to audit the lifetime.
A shared service may be immutable or internally concurrency-safe, but it must not store the current request identity.
Never attach a user, tenant, session, token, cart, permissions, or request headers to a registered service.
Pass request values into stateless service methods, or create the request-bound client inside the loader/action/middleware that owns it.
Do not silence a duplicate or SSR warning with override until the replacement and lifetime are intentional.
Distinguish services from capabilities
Controllers expose both this.services and this.capabilities. Services are registered directly by the application. Capabilities are contracts contributed by installed plugins and resolved through the active Pages runtime.
Use
capabilities.resolvefor a required plugin capability andtryResolvefor an optional one.Required capability resolution without an active Pages runtime fails with
TAVO_PLUGIN_004.Keep plugin ownership, permissions, and replacement rules in the Plugin API v1 manifest instead of recreating them in the service registry.
Verify registration and isolation
Resolve the typed key after installation and assert the same instance is returned.
Verify get throws and
tryGetreturns undefined before installation.Register a duplicate in a focused test and confirm intentional override behavior.
Clear services between tests that share a process.
Run concurrent SSR requests and confirm no request-specific value is retained by a service.
Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.