/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { BasicSet, decamelize, Diagnostic, ImmutableSet, MatterAggregateError, MutableSet, ObservableSet, } from "@matter/general"; import { Endpoint } from "../Endpoint.js"; import { IdentityConflictError } from "../errors.js"; /** * Manages parent-child relationships between endpoints. */ export class EndpointContainer implements ImmutableSet, MutableSet, ObservableSet { #children = new BasicSet(); #owner: Endpoint; constructor(endpoint: Endpoint) { this.#owner = endpoint; } get(id: string | number) { if (typeof id === "number") { for (const child of this) { if (child.maybeNumber === id) { return child; } } } else { for (const child of this) { if (child.maybeId === id) { return child; } } } } add(endpoint: T) { // Assigning ownership below reenters via the endpoint's owner setter, so membership is the guard against // installing the destruction listener twice if (this.#children.has(endpoint)) { return; } if (endpoint.lifecycle.hasId) { this.assertIdAvailable(endpoint.id, endpoint); } this.#children.add(endpoint); endpoint.owner = this.#owner; endpoint.lifecycle.destroyed.once(() => { this.remove(endpoint); }); } delete(endpoint: T) { return this.remove(endpoint); } clear() { this.#children.clear(); } /** * Remove an endpoint from the container when it is destroyed or moves to another owner. * * @internal */ remove(endpoint: T) { return this.#children.delete(endpoint); } has(endpoint: T) { return this.#children.has(endpoint); } get added() { return this.#children.added; } get deleted() { return this.#children.deleted; } get empty() { return this.#children.empty; } get size() { return this.#children.size; } map(fn: (part: T) => T2) { return this.#children.map(fn); } filter(predicate: (part: T) => boolean) { return this.#children.filter(predicate); } find(predicate: (part: T) => boolean) { return this.#children.find(predicate); } forEach(fn: (part: T) => boolean) { [...this.#children].forEach(fn); } [Symbol.iterator]() { return this.#children[Symbol.iterator](); } /** * Soft-reset all parts. Invoked automatically by the owner on reset. */ async reset() { for (const endpoint of this) { await endpoint.reset(); } } /** * Destroy all parts. Invoked automatically by the owner on destroy. */ async close() { await MatterAggregateError.settleSeries(this.#closeTasks(), `Error closing endpoints owned by ${this.#owner}`); } // Iterates the live set, so an endpoint added while closing is closed too *#closeTasks() { for (const endpoint of this) { yield () => endpoint.close(); } } /** * Confirm the availability of an ID amongst the endpoint's children. */ assertIdAvailable(id: string, endpoint: Endpoint) { const other = this.get(id); if (other && other !== endpoint) { throw new IdentityConflictError(`${other} is already defined; endpoint IDs must be unique within parent`); } } protected get owner() { return this.#owner; } get [Diagnostic.value]() { return Diagnostic.list([decamelize(this.constructor.name), ...this]); } }