ServiceContainer
class exported by @mwillbanks/tuil-core.
class
Public class exported by @mwillbanks/tuil-core.
export class ServiceContainer implements Disposable {
readonly #records: Map<string, ServiceRecord>;
readonly #order: string[];
readonly #controller: AbortController;
#disposed: boolean;
constructor() {
this.#records = new Map();
this.#order = [];
this.#controller = new AbortController();
this.#disposed = false;
}
register<TId extends string, TValue>(
definition: ServiceDefinition<TId, TValue>,
): Disposable;
register<TId extends string, TValue>(id: TId, value: TValue): Disposable;
register<TId extends string, TValue>(
definitionOrId: ServiceDefinition<TId, TValue> | TId,
value?: TValue,
): Disposable {
this.#assertActive();
const id =
typeof definitionOrId === "string" ? definitionOrId : definitionOrId.id;
if (this.#records.has(id)) {
throw new Error(`Service "${id}" is already registered`);
}
this.#records.set(
id,
typeof definitionOrId === "string"
? { value, status: "ready" }
: {
definition: {
create: definitionOrId.create,
dispose: definitionOrId.dispose as
| ((created: unknown) => void | Promise<void>)
| undefined,
},
status: "registered",
},
);
this.#order.push(id);
return toDisposable(() => {
const record = this.#records.get(id);
if (record?.status === "initializing") {
throw new Error(
`Cannot unregister service "${id}" while it initializes`,
);
}
this.#records.delete(id);
const index = this.#order.indexOf(id);
if (index >= 0) {
this.#order.splice(index, 1);
}
});
}
has(id: string): boolean {
return this.#records.has(id);
}
get<TValue>(id: string): TValue {
this.#assertActive();
const record = this.#records.get(id);
if (!record) {
throw new Error(`Service "${id}" is not registered`);
}
if (record.status === "failed") {
throw record.error;
}
if (record.status !== "ready") {
throw new Error(`Service "${id}" has not been initialized`);
}
return record.value as TValue;
}
async resolve<TValue>(id: string): Promise<TValue> {
this.#assertActive();
const record = this.#records.get(id);
if (!record) {
throw new Error(`Service "${id}" is not registered`);
}
if (record.status === "ready") {
return record.value as TValue;
}
if (record.status === "failed") {
throw record.error;
}
if (record.status === "initializing") {
throw new Error(
`Circular or concurrent initialization detected for service "${id}"`,
);
}
record.status = "initializing";
try {
record.value = await record.definition?.create({
services: this,
signal: this.#controller.signal,
});
record.status = "ready";
return record.value as TValue;
} catch (error) {
record.status = "failed";
record.error = error;
throw error;
}
}
async initialize(): Promise<void> {
for (const id of this.#order) {
await this.resolve(id);
}
}
entries(): readonly [string, unknown][] {
return this.#order
.filter((id) => this.#records.get(id)?.status === "ready")
.map((id) => [id, this.#records.get(id)?.value] as const);
}
async dispose(): Promise<void> {
if (this.#disposed) {
return;
}
this.#disposed = true;
this.#controller.abort(new Error("Service container disposed"));
const errors: unknown[] = [];
for (const id of [...this.#order].reverse()) {
const record = this.#records.get(id);
if (
record?.status === "ready" &&
record.definition?.dispose &&
record.value !== undefined
) {
try {
await record.definition.dispose(record.value);
} catch (error) {
errors.push(error);
}
}
}
this.#records.clear();
this.#order.length = 0;
if (errors.length > 0) {
throw new AggregateError(errors, "Failed to dispose runtime services");
}
}
#assertActive(): void {
if (this.#disposed) {
throw new Error("Service container is disposed");
}
}
}Members
| Member | Type | Required | Description | Related types |
|---|---|---|---|---|
#records | Map<string, ServiceRecord> | Yes | The #records member uses the Map<string, ServiceRecord> contract. | — |
#order | string[] | Yes | The #order member uses the string[] contract. | — |
#controller | AbortController | Yes | The #controller member uses the AbortController contract. | — |
#disposed | boolean | Yes | The #disposed member uses the boolean contract. | — |
__constructor | any | Yes | The __constructor member uses the any contract. | — |
register | { <TId extends string, TValue>(definition: ServiceDefinition<TId, TValue>): Disposable; <TId extends string, TValue>(id: TId, value: TValue): Disposable; } | Yes | The register member uses the { <TId extends string, TValue>(definition: ServiceDefinition<TId, TValue>): Disposable; <TId extends string, TValue>(id: TId, value: TValue): Disposable; } contract. | Disposable, ServiceDefinition |
has | (id: string) => boolean | Yes | The has member uses the (id: string) => boolean contract. | — |
get | <TValue>(id: string) => TValue | Yes | The get member uses the <TValue>(id: string) => TValue contract. | — |
resolve | <TValue>(id: string) => Promise<TValue> | Yes | The resolve member uses the <TValue>(id: string) => Promise<TValue> contract. | — |
initialize | () => Promise<void> | Yes | The initialize member uses the () => Promise<void> contract. | — |
entries | () => readonly [string, unknown][] | Yes | The entries member uses the () => readonly [string, unknown][] contract. | — |
dispose | () => Promise<void> | Yes | The dispose member uses the () => Promise<void> contract. | — |
#assertActive | () => void | Yes | The #assertActive member uses the () => void contract. | — |
Parameters
This declaration has no public members.
Returns
This declaration does not return a value.
Throws
No thrown errors are documented for this declaration.
Related types
Source
View the secondary source reference