Services and lifetimes
Scoped vs singleton registration, onStart and onStop, CAPTIVE_DEPENDENCY, and Workers constraints.
Flare has two service lifetimes: scoped (per HTTP request, per WebSocket connection) and singleton (per process on Node). Pick the lifetime that matches how long the instance should live, register it on the host, and list the token in every consumer’s static deps or inline inject map.
Scoped services
Section titled “Scoped services”import { FlareHost, FlareService, FlareResponse } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
class RequestContext extends FlareService { public static override deps = []; readonly requestId = Math.random().toString(36).slice(2);}
const host = new FlareHost(node);host.scoped(RequestContext);
host.http.get( "/id", { inject: { ctx: RequestContext } }, (_ctx, scope) => new FlareResponse(200, { id: scope.ctx.requestId }),);
const app = host.build();app.run();| Scoped | |
|---|---|
| Created | First inject() / scope.name in a request or connection |
| Disposed | When the request ends, or the connection closes (dispose()) |
| Shared across requests | No |
| Workers | Yes |
Use scoped for anything tied to one request: request IDs, per-user context, unit-of-work handles.
On WebSocket routes
Section titled “On WebSocket routes”The scope of a WebSocket route is the connection, not a single message. The container is created at upgrade, a scoped service is built on its first resolution and then shared by every handler call on that connection (open, each message, close), and disposal runs once at terminal close. That span makes a scoped service a legitimate per-connection session object: accumulate state across messages, release resources in dispose().
Two boundaries to know:
- A scoped service shared between HTTP and WebSocket routes has two time horizons. The same token lives milliseconds on an HTTP route and potentially hours on a WebSocket route. A service designed request-length, such as a pooled connection checked out per request, becomes a long hold when a WebSocket route injects it.
- A hibernating Durable Object route scopes per wake, not per connection. The engine’s memory is per event, so each delivered message gets a fresh container and a scoped service lives for that one event. Data that must survive across messages there belongs in
ws.state, which round-trips through the socket attachment.
An upgrade hook resolves from the same container the connection uses, so a service the hook touches is the same instance the handlers later see, disposed with the rest at close.
Singleton services (Node)
Section titled “Singleton services (Node)”import { FlareHost, FlareService, FlareResponse } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
class Cache extends FlareService { public static override deps = [];
readonly #store = new Map<string, number>(); readonly id = Math.random().toString(36).slice(2);
hit(key: string): number { const next = (this.#store.get(key) ?? 0) + 1; this.#store.set(key, next); return next; }}
const host = new FlareHost(node);host.singleton(Cache);
host.http.get( "/hits", { inject: { cache: Cache } }, (_ctx, scope) => new FlareResponse(200, { instance: scope.cache.id, count: scope.cache.hit("hits"), }),);
const app = host.build();app.run();The instance is built at host.build(). Hit /hits twice and instance stays the same while count climbs. A scoped Cache would reset every request.
| Singleton | |
|---|---|
| Created | host.build() (or app.test() in test mode) |
| Disposed | Graceful shutdown (onStop()) |
| Lifecycle hooks | onStart() at app start, onStop() on shutdown |
| Workers | No. Use host.scoped() instead |
Lifecycle hooks on singletons
Section titled “Lifecycle hooks on singletons”import { FlareHost, FlareService } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
class Pool extends FlareService { public static override deps = [];
#open = false;
async onStart(): Promise<void> { this.#open = true; }
async onStop(): Promise<void> { this.#open = false; }
get ready(): boolean { return this.#open; }}
const host = new FlareHost(node);host.singleton(Pool);onStart() and onStop() are singleton-only. Scoped services use dispose() per request. See Lifecycle.
Constructors assign; hooks acquire. Keep singleton constructors to dependency assignment and in-memory setup. Acquire files, sockets, timers, and connections in onStart() and release them in onStop(): a failed startup only stops services whose onStart() ran, so a resource acquired in a constructor has no release path when a later service fails to start.
CAPTIVE_DEPENDENCY
Section titled “CAPTIVE_DEPENDENCY”A singleton cannot list a scoped service in static deps. That would capture one request’s scoped instance for the process lifetime. Flare throws CAPTIVE_DEPENDENCY at build().
When a singleton needs request-specific data, inject the scoped service in the handler and pass values into singleton methods:
import { FlareHost, FlareResponse, FlareService } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
class RequestId extends FlareService { public static override deps = []; readonly value = Math.random().toString(36).slice(2);}
class Metrics extends FlareService { public static override deps = []; record(requestId: string): string { return `recorded ${requestId}`; }}
const host = new FlareHost(node);host.scoped(RequestId);host.singleton(Metrics);
host.http.get( "/track", { inject: { id: RequestId, metrics: Metrics } }, (_ctx, scope) => { const result = scope.metrics.record(scope.id.value); return new FlareResponse(200, { result }); },);On Cloudflare Workers
Section titled “On Cloudflare Workers”host.singleton() is not supported on Workers. The Cloudflare adapter doesn’t stamp the singleton extension onto the host, so host.singleton doesn’t exist on a Cloudflare host and calling it is a compile error. Register per-request services with host.scoped() and put shared state in platform bindings (KV, D1, Durable Objects). See Runtimes > Cloudflare.
Related
Section titled “Related”- Using DI in apps: validator codes
- Host:
scopedandsingletonregistration - Replace services in tests: swap implementations in test mode