@furystack/inject
Version:
Dependency Injection framework for FuryStack
136 lines (120 loc) • 5.47 kB
text/typescript
import type { defineService, defineServiceAsync } from './define-service.js'
import type { Injector } from './injector.js'
/**
* The lifetime of an injectable service. Determines how and where instances
* are cached inside an {@link Injector}.
*
* - `transient` — a new instance is produced each time the service is requested. Not cached.
* - `singleton` — a single instance per injector tree. Cached at the root injector.
* - `scoped` — a single instance per injector scope. Cached at the injector that first requested it.
*/
export type Lifetime = 'transient' | 'singleton' | 'scoped'
/**
* Opaque, type-carrying handle that identifies a service in the DI container.
*
* Created by {@link defineService} or {@link defineServiceAsync}. The `id` symbol
* is used as the registry key; the phantom `__type` preserves the resolved type
* for inference at call sites.
*
* @typeParam TService - The type returned by the factory when the service is resolved
* @typeParam TLifetime - The lifetime category of the service
* @typeParam TIsAsync - `true` for tokens produced by {@link defineServiceAsync},
* `false` for tokens produced by {@link defineService}. Encoded as a literal so
* {@link Injector.get} can reject async tokens at compile time.
*/
export type Token<TService, TLifetime extends Lifetime = Lifetime, TIsAsync extends boolean = false> = {
readonly id: symbol
readonly name: string
readonly lifetime: TLifetime
readonly isAsync: TIsAsync
readonly factory: ServiceFactory<TService> | AsyncServiceFactory<TService>
readonly __type?: TService
}
/**
* Sync-token specialisation of {@link Token}. Resolvable via both
* {@link Injector.get} and {@link Injector.getAsync}.
*/
export type SyncToken<TService, TLifetime extends Lifetime = Lifetime> = Token<TService, TLifetime, false>
/**
* Async-token specialisation of {@link Token}. Only resolvable via
* {@link Injector.getAsync} — {@link Injector.get} rejects async tokens at
* compile time.
*/
export type AsyncToken<TService, TLifetime extends Lifetime = Lifetime> = Token<TService, TLifetime, true>
/**
* Either {@link SyncToken} or {@link AsyncToken}. Use when a consumer needs
* to accept both varieties generically.
*/
export type AnyToken<TService, TLifetime extends Lifetime = Lifetime> =
| SyncToken<TService, TLifetime>
| AsyncToken<TService, TLifetime>
/** Extracts the resolved service type from a {@link Token}. */
export type ServiceOf<TToken> = TToken extends Token<infer T, Lifetime, boolean> ? T : never
/**
* Callback registered via {@link ServiceContext.onDispose}, invoked when the
* owning scope is disposed. May return a promise; async callbacks are awaited
* as part of scope teardown.
*/
export type DisposeCallback = () => void | Promise<void>
/**
* Synchronous resolver passed to sync factories. Accepts sync tokens only —
* async dependencies must be resolved via {@link InjectAsyncFn}.
*
* The signature is refined per-factory lifetime so that singleton factories
* can only depend on singleton services.
*/
export type InjectFn<TParentLifetime extends Lifetime = Lifetime> = TParentLifetime extends 'singleton'
? <TService>(token: SyncToken<TService, 'singleton'>) => TService
: <TService>(token: SyncToken<TService>) => TService
/**
* Asynchronous resolver. Accepts both sync and async tokens; sync tokens are
* wrapped in a resolved promise.
*/
export type InjectAsyncFn<TParentLifetime extends Lifetime = Lifetime> = TParentLifetime extends 'singleton'
? <TService>(token: AnyToken<TService, 'singleton'>) => Promise<TService>
: <TService>(token: AnyToken<TService>) => Promise<TService>
/** Context object passed to a service factory. */
export type ServiceContext<TLifetime extends Lifetime = Lifetime> = {
inject: InjectFn<TLifetime>
injectAsync: InjectAsyncFn<TLifetime>
/** The injector that owns this service's cached instance. */
injector: Injector
/**
* The token currently being instantiated. Useful for services that want to
* reflect on their own definition — e.g. scoping loggers by the defining
* service's {@link Token.name}.
*/
token: AnyToken<unknown>
/** Registers a disposal callback to run (LIFO) when the owning scope is disposed. */
onDispose: (cb: DisposeCallback) => void
}
export type ServiceFactory<TService> = (ctx: ServiceContext) => TService
/**
* Factory for an async service. Resolved values are cached after first
* resolution; concurrent callers share the same pending promise.
*/
export type AsyncServiceFactory<TService> = (ctx: ServiceContext) => Promise<TService>
/**
* Options accepted by {@link defineService}. `lifetime` is required — there
* is no default; pick explicitly.
*/
export type DefineServiceOptions<TService, TLifetime extends Lifetime> = {
/**
* Human-readable identifier for debug output only. Token identity is
* established by the returned {@link Token} object reference, not this
* string.
*/
name: string
lifetime: TLifetime
factory: (ctx: ServiceContext<TLifetime>) => TService
}
/** See {@link DefineServiceOptions} — same shape, async factory return type. */
export type DefineServiceAsyncOptions<TService, TLifetime extends Lifetime> = {
name: string
lifetime: TLifetime
factory: (ctx: ServiceContext<TLifetime>) => Promise<TService>
}
export type CreateScopeOptions = {
/** Opaque owner reference (e.g. a request, session, element). */
owner?: unknown
}