Services
A service is an @Injectable class bound to a container. Put business logic there. Put UI rendering somewhere else.
Services can hold reactive state, plain state, or no state. Wirestate does not care.
Declare A Service
import { Injectable } from "@wirestate/core";
@Injectable()
export class UserService {
public currentUser: User | null = null;
public setUser(user: User): void {
this.currentUser = user;
}
}Bind A Service
bindService registers the class as a singleton and wires lifecycle and messaging decorators.
import { Container, bindService, createContainer } from "@wirestate/core";
import { UserService } from "./UserService";
const container: Container = createContainer();
bindService(container, UserService);
const users = container.get(UserService);Most React and Lit code uses provider entries instead of calling bindService by hand.
<ContainerProvider config={{ entries: [UserService] }}>
<App />
</ContainerProvider>Constructor Injection
Use @Inject(Token) for constructor dependencies. A token can be a class, string, or symbol.
import { Inject, Injectable } from "@wirestate/core";
@Injectable()
export class OrderService {
public constructor(
@Inject(UserService)
private readonly users: UserService,
@Inject(LoggerService)
private readonly logger: LoggerService
) {}
}WireScope
Inject WireScope when a service needs the container edge: events, commands, queries, seeds, or lazy resolution.
import { Inject, Injectable, WireScope } from "@wirestate/core";
@Injectable()
export class CartService {
public constructor(@Inject(WireScope) private readonly scope: WireScope) {}
public checkout(): void {
this.scope.emitEvent("CHECKOUT_STARTED");
}
}WireScope is transient. Each service gets its own handle.
scope.isDisposedbecomestrueafter service deactivation.scope.isDeprovisionedtracks provider ownership:null, thenfalse, thentrue.scope.isInactiveis the normal guard for async work that may finish late.
@OnActivated()
public async onActivated(): Promise<void> {
const data = await fetch("/api/data").then((r) => r.json());
if (!this.scope.isInactive) {
this.data.value = data;
}
}Lifecycle
Service lifecycle follows the container.
@OnActivatedruns when the service is first resolved.@OnDeactivationruns when the service is unbound or the container is disposed.
import { Injectable, OnActivated, OnDeactivation } from "@wirestate/core";
@Injectable()
export class PollingService {
private timerId: ReturnType<typeof setInterval> | null = null;
@OnActivated()
public onActivated(): void {
this.timerId = setInterval(() => this.poll(), 5_000);
}
@OnDeactivation()
public onDeactivation(): void {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
}
}
}Provider lifecycle follows React or Lit provider ownership.
@OnProvisionruns when a provider exposes the container to a subtree.@OnDeprovisionruns before that provider removes or replaces it.
import { Injectable, OnDeprovision, OnProvision } from "@wirestate/core";
import { connectPanelChannel } from "./panel-channel";
@Injectable()
export class PanelService {
private unsubscribe: (() => void) | null = null;
@OnProvision()
public onProvision(): void {
this.unsubscribe = connectPanelChannel();
}
@OnDeprovision()
public onDeprovision(): void {
this.unsubscribe?.();
this.unsubscribe = null;
}
}Provider hooks only run for entries registered through Wirestate helpers: createContainer({ entries }), React ContainerProvider config, React SubContainerProvider, Lit providers, or direct bindService / bindEntry.
Circular Dependencies
Avoid cycles when you can. If two services need each other immediately, the design is usually too tight.
Use forwardRef when a constructor cycle is unavoidable.
import { Inject, Injectable, forwardRef } from "@wirestate/core";
@Injectable()
export class ServiceA {
public constructor(@Inject(forwardRef(() => ServiceB)) private readonly b: ServiceB) {}
}
@Injectable()
export class ServiceB {
public constructor(@Inject(forwardRef(() => ServiceA)) private readonly a: ServiceA) {}
}Use scope.resolve(Token) when the dependency is only needed later. That turns the startup handshake into a lazy lookup.
import { Inject, Injectable, WireScope } from "@wirestate/core";
@Injectable()
export class NotificationService {
public constructor(@Inject(WireScope) private readonly scope: WireScope) {}
public notify(message: string): void {
const logger = this.scope.resolve(LoggerService);
logger.log(message);
}
}scope.resolveOptional(Token) returns null when the token is not bound.
Constants And Factories
Use descriptors for non-class entries.
import { BindingType, bindConstant, bindDynamicValue, createContainer } from "@wirestate/core";
const API_URL = Symbol("API_URL");
const DATE_NOW = Symbol("DATE_NOW");
const container = createContainer();
bindConstant(container, { id: API_URL, value: "https://api.example.com" });
bindDynamicValue(container, {
id: DATE_NOW,
bindingType: BindingType.DynamicValue,
factory: () => new Date(),
});Inject descriptor tokens with @Inject(API_URL) or resolve them with container.get(API_URL).