/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Environment } from "#environment/Environment.js"; import { Environmental } from "#environment/Environmental.js"; import { ServiceProvider } from "#environment/ServiceProvider.js"; import { SharedServicesManager } from "#environment/SharedServicesManager.js"; import { ImplementationError } from "#MatterError.js"; import { MaybePromise } from "#util/Promises.js"; /** * Provides reference-counted access to shared environmental services. * * Tracks which services are in use and ensures services are only closed when all * consumers have released them. This enables safe sharing of services across multiple * consumers without premature cleanup. * * Created via {@link Environment.asDependent} and automatically registered at the root * environment for centralized lifecycle management. All shared services are accessed * at the root level regardless of which environment creates the instance. */ export class SharedEnvironmentServices implements ServiceProvider { #env: Environment; #serviceTypes = new Set(); #closed = false; constructor(env: Environment) { this.#env = env; } #assertClosed() { if (this.#closed) { throw new ImplementationError("Dependent environment is closed"); } } /** Check if this instance is currently tracking a specific service type. */ has(type: Environmental.ServiceType) { return this.#serviceTypes.has(type); } /** * Access an environmental service and register this instance as a consumer. * * The service will be tracked and protected from closure until this instance * explicitly releases it via {@link delete} or {@link close}. */ get(type: Environmental.ServiceType): T { this.#assertClosed(); const instance = this.#env.get(type); this.#serviceTypes.add(type); return instance; } /** * Access an optional environmental service and register this instance as a consumer. * * Returns undefined if the service is not available, otherwise behaves like {@link get}. */ maybeGet(type: Environmental.ServiceType): T | undefined { this.#assertClosed(); if (this.#env.has(type)) { return this.get(type); } } /** * Unregister this instance from a service and remove the service from the environment. * * If other consumers are still using the service, it remains available for them. * Only affects services this instance has accessed via {@link get} or {@link load}. */ delete(type: Environmental.ServiceType, instance?: any) { this.#assertClosed(); if (!this.#serviceTypes.has(type)) { return; // Don't affect services this dependent didn't use } this.#serviceTypes.delete(type); this.#env.delete(type, instance); } /** * Load an environmental service asynchronously and register this instance as a consumer. * * Waits for the service's construction promise to resolve if present, then tracks the service for lifecycle * management. */ async load(type: Environmental.Factory): Promise { this.#assertClosed(); const instance = await this.#env.load(type); this.#serviceTypes.add(type); return instance; } /** * Unregister from and close one or all services used by this instance. * * When called with a specific type: * - Unregisters this instance from that service * - Calls service.close() and removes the service if no other consumers are using it * * When called without arguments: * - Closes all services this instance has accessed * - Marks this instance as permanently closed * - Subsequent operations will throw ImplementationError * * Only affects services this instance has accessed via {@link get} or {@link load}. */ close( type: Environmental.ServiceType, ): T extends { close: () => MaybePromise } ? MaybePromise : void; close(): MaybePromise; close(type?: Environmental.ServiceType): MaybePromise { this.#assertClosed(); if (type === undefined) { this.#closed = true; const types = Array.from(this.#serviceTypes); this.#serviceTypes.clear(); const closePromises = new Array>(); for (const type of types) { const closer = this.#env.close(type); if (MaybePromise.is(closer)) { closePromises.push(closer); } } this.#env.root.get(SharedServicesManager).delete(this); if (closePromises.length > 0) { return Promise.all(closePromises).then(() => undefined); } } else { if (!this.#serviceTypes.has(type)) { return; // Don't affect services this dependent didn't use } this.#serviceTypes.delete(type); return this.#env.close(type); } } async [Symbol.asyncDispose]() { await this.close(); } }