/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Diagnostic } from "#log/Diagnostic.js"; import { DiagnosticPresentation } from "#log/DiagnosticPresentation.js"; import { DiagnosticSource } from "#log/DiagnosticSource.js"; import { InternalError } from "#MatterError.js"; import { Duration } from "#time/Duration.js"; import "#time/StandardTime.js"; import { Time } from "#time/Time.js"; import { Timestamp } from "#time/Timestamp.js"; import { Boot } from "./Boot.js"; /** * A "lifetime" represents the existence of an entity or ongoing task. * * This serves as a mechanism for tracking granular information about process state for diagnostic purposes. A lifetime * is present in diagnostic reports until disposed. * * Lifetimes are hierarchical. Create sublifetimes using join(). */ export interface Lifetime extends Disposable, Diagnostic, Lifetime.Owner { /** * The name of the lifetime used for diagnostic presentation. * * Any diagnostic (so, any value) may serve as a name. */ name: unknown; /** * The time at which the lifetime began. */ readonly startedAt: Timestamp; /** * A "span" is a sub-lifetime created via {@link join}. */ readonly spans: Set; /** * Arbitrary details presented as a dictionary with {@link name}. */ readonly details: Record; /** * The inverse of {@link isClosing}. */ readonly isOpen: boolean; /** * Set when the lifetime begins closing (via {@link closing}) or {@link isClosed} is true. */ readonly isClosing: boolean; /** * Set when the lifetime has exited. */ readonly isClosed: boolean; /** * The lifetime enclosing this lifetime. * * Only the process lifetime should have no owner. This field is writable so you can move ownership of a lifetime. */ owner?: Lifetime; /** * Mark this lifetime as closing. * * Creates a sublifetime specifically for closing this lifetime. This supports the common pattern of tracking the * close process associated with an active lifetime. * * Calling repeatedly returns the same sublifetime. Disposing the returned lifetime disposes this lifetime. */ closing(): Lifetime; } export function Lifetime(...name: unknown[]) { return Lifetime.process.join(...name); } class LifetimeImplementation implements Lifetime, Lifetime.Owner { #name: unknown; #owner?: Lifetime; #startedAt: Timestamp; #details?: Record; #spans?: Set; #closing?: Lifetime; #isClosed = false; declare [Diagnostic.presentation]?: unknown; constructor(name: unknown[], owner?: Lifetime) { this.#name = name.length > 1 ? name : name[0]; this.#startedAt = Time.nowMs; this.#owner = owner; if (owner) { owner.spans.add(this); } } get spans() { if (!this.#spans) { this.#spans = new Set(); } return this.#spans; } get name() { return this.#name; } set name(name: unknown) { this.#name = name; } get startedAt() { return this.#startedAt; } get details() { return this.#details ?? {}; } get owner() { return this.#owner; } set owner(owner: Lifetime | undefined) { if (!this.#owner) { throw new InternalError("Cannot move ownership of root lifetime"); } if (this.#owner === owner) { return; } removeSpan(this.owner, this); this.#owner = owner; this.#owner?.spans.add(this); } join(...name: unknown[]): Lifetime { return new LifetimeImplementation(name, this); } closing(): Lifetime { if (!this.#closing) { this.#closing = this.join("closing"); const disposeClosing = this.#closing[Symbol.dispose].bind(this.#closing); this.#closing[Symbol.dispose] = () => { disposeClosing(); this[Symbol.dispose](); }; } return this.#closing; } get isOpen() { return this.#closing === undefined && !this.#isClosed; } get isClosing() { return this.#closing !== undefined || this.#isClosed; } get isClosed() { return this.#isClosed; } get [DiagnosticPresentation.value]() { // Special case for process lifetime if (!this.#owner) { return Diagnostic.node("🌱", "Lifetimes", { children: this.spans, }); } const header: unknown[] = [this.#name]; if (this.isClosed) { header.push(Diagnostic.weak("(zombie)")); } const details: Record = { up: Duration.format(Timestamp.delta(this.startedAt, Time.nowMs)), ...this.#details, }; header.push(Diagnostic.dict(details)); const result: unknown[] = [header]; if (this.#spans?.size) { result.push(Diagnostic.list(this.spans)); } return result; } [Symbol.dispose]() { if (!this.#owner) { // Can't dispose of process lifetime return; } this.#isClosed = true; // If we are disposed with active sublifetimess we become a zombie if (this.#spans?.size) { return; } removeSpan(this.#owner, this); } } let process = new LifetimeImplementation(["process"]); DiagnosticSource.add(process); function removeSpan(owner: Lifetime | undefined, span: Lifetime) { if (!owner) { return; } owner.spans.delete(span); if (owner.isClosed && !owner.spans?.size) { owner[Symbol.dispose](); } } export namespace Lifetime { /** * The lifetime of the system process. * * This is effectively a "global" lifetime. It parents all other lifetimes. */ export declare const process: Lifetime.Owner; /** * Obtain a lifetime not attached to {@link process} for testing purposes. */ export declare const mock: Lifetime; /** * An object associated with a lifetime. */ export interface Owner { /** * Create or move a sublifetime. */ join(...name: unknown[]): Lifetime; } /** * A lifetime subject that exists for a portion of a larger timespan. */ export interface Contributor { [owner]: Owner; } /** * Determine the lifetime of the owner of a component. */ export function of(subject?: {}) { return (subject as Partial | undefined)?.[owner] ?? process; } export const owner = Symbol("owner"); } Object.defineProperties(Lifetime, { process: { get() { return process; }, }, mock: { get() { return new LifetimeImplementation(["mock"]); }, }, }); Boot.init(() => { process = new LifetimeImplementation(["process"]); DiagnosticSource.add(process); }, "state");