tuil
ReferencePackages@mwillbanks/tuil-coreAPI

ServiceContainer

class exported by @mwillbanks/tuil-core.

View rawEdit

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

MemberTypeRequiredDescriptionRelated types
#recordsMap<string, ServiceRecord>YesThe #records member uses the Map<string, ServiceRecord> contract.
#orderstring[]YesThe #order member uses the string[] contract.
#controllerAbortControllerYesThe #controller member uses the AbortController contract.
#disposedbooleanYesThe #disposed member uses the boolean contract.
__constructoranyYesThe __constructor member uses the any contract.
register&#123; <TId extends string, TValue>(definition: ServiceDefinition<TId, TValue>): Disposable; <TId extends string, TValue>(id: TId, value: TValue): Disposable; &#125;YesThe register member uses the &#123; <TId extends string, TValue>(definition: ServiceDefinition<TId, TValue>): Disposable; <TId extends string, TValue>(id: TId, value: TValue): Disposable; &#125; contract.Disposable, ServiceDefinition
has(id: string) => booleanYesThe has member uses the (id: string) => boolean contract.
get<TValue>(id: string) => TValueYesThe get member uses the <TValue>(id: string) => TValue contract.
resolve<TValue>(id: string) => Promise<TValue>YesThe resolve member uses the <TValue>(id: string) => Promise<TValue> contract.
initialize() => Promise<void>YesThe initialize member uses the () => Promise<void> contract.
entries() => readonly [string, unknown][]YesThe entries member uses the () => readonly [string, unknown][] contract.
dispose() => Promise<void>YesThe dispose member uses the () => Promise<void> contract.
#assertActive() => voidYesThe #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.

Source

View the secondary source reference

Package

@mwillbanks/tuil-core

On this page