/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { type Agent, INSTALL_BEHAVIOR } from "#endpoint/Agent.js"; import { AsyncObservable, type DeepPartial, EventEmitter, GeneratedClass, Lifetime, MaybePromise, NotImplementedError, Observable, Transaction, } from "@matter/general"; import { ClassSemantics, Schema, Semantics } from "@matter/model"; import { Reactor } from "./Reactor.js"; import type { BehaviorBacking } from "./internal/BehaviorBacking.js"; import { DerivedState, EmptyState } from "./state/StateType.js"; import { RootSupervisor } from "./supervision/RootSupervisor.js"; // Internal fields const BACKING = Symbol("endpoint-owner"); const STATE = Symbol("state"); const INTERNAL = Symbol("internal"); const EVENTS = Symbol("events"); interface Internal extends Behavior { [BACKING]: BehaviorBacking; [STATE]: {}; [INTERNAL]: {}; [EVENTS]: EventEmitter; } const SUPERVISOR = Symbol("supervisor"); interface StaticInternal { [SUPERVISOR]?: RootSupervisor; } /** * Behavior implements functionality for an Endpoint. Endpoint agents are implemented as a composition of behaviors. * * Most behaviors associated 1:1 with a Matter cluster type as implemented by ClusterBehavior. But you can also extend * Behavior directly to add other types of composable logic to an endpoint. * * You probably want to build your behavior using one of the standard implementations offered by Matter.js. */ export abstract class Behavior { #agent: Agent; /** * Each behavior implementation has an ID that uniquely identifies the type of behavior. An Endpoint may only have * one behavior with the specified ID. * * Endpoint instances store each behavior in a property with the same name as the behavior's ID. * * EndpointBuilder also uses the ID when replacing behaviors using the with() builder method. */ static readonly id: string; /** * A behavior's schema controls access to data, commands and events. * * Schema is inferred from the methods and properties of the behavior but you can specify explicitly for additional * control. */ static get schema() { return Schema(this) ?? Schema.empty; } /** * By default behaviors load lazily as they are accessed. You can set this flag to true to force behaviors to load * immediately when the endpoint initializes. */ static readonly early: boolean = false; /** * The agent that owns the behavior. */ get agent() { return this.#agent; } /** * The endpoint that owns behavior's agent. */ get endpoint() { return this.#agent.endpoint; } /** * The context in which the behavior operates. */ get context() { return this.#agent.context; } /** * The endpoint's environment. */ get env() { return this.endpoint.env; } /** * The Behavior's type. */ get type() { return this.constructor as Behavior.Type; } /** * Access the behavior's state. */ declare readonly state: {}; /** * Access the behavior's events. */ declare readonly events: EventEmitter; constructor(agent: Agent, backing: BehaviorBacking) { this.#agent = agent; (this as unknown as Internal)[BACKING] = backing; // Note - for introspection instance agent and backing will actually be undefined. Not represented in types // because it would require numerous assertions just to handle internal edge case if (agent !== undefined) { (agent as unknown as Agent.Internal)[INSTALL_BEHAVIOR](this); } } /** * The data supervisor for the behavior. The supervisor controls validation and access to behavior data. */ static get supervisor(): RootSupervisor { if (Object.hasOwn(this, SUPERVISOR)) { return (this as StaticInternal)[SUPERVISOR] as RootSupervisor; } return ((this as StaticInternal)[SUPERVISOR] = RootSupervisor.for(Schema(this) ?? Schema.empty)); } /** * Implementation of endpoint-scoped state. Subclasses may override to extend. */ static State = EmptyState; /** * Implementation of internal state. Subclasses may override to extend. */ static Internal = EmptyState; /** * Implementation of the events property. Subclasses may override to extend. */ static Events = EventEmitter; /** * Behaviors are ephemeral and should not perform initialization in their constructor. They can override this * method instead. * * This method may be synchronous or asyncronous. If asynchronous, the behavior will not be available for external * use until initialization completes. */ initialize(_options?: {}): MaybePromise {} /** * Release resources. This is the public API for releasing application resources held by behaviors in internal * state. */ [Symbol.asyncDispose](): MaybePromise {} /** * Description used in diagnostic messages. */ toString() { return `${this.endpoint}.${this.type.id}`; } /** * Lifetime associated with this behavior type for {@link endpoint}. */ get lifetime(): Lifetime.Owner { return { join: (...name: unknown[]) => { return (this as unknown as Internal)[BACKING].construction.join(...name); }, }; } /** * Install a {@link Reactor}. * * Important: The reactor MUST be a real JS function - arrow functions will not work! */ protected reactTo>( observable: O, reactor: Reactor, ReturnType>, options?: Reactor.Options, ) { (this as unknown as Internal)[BACKING].reactTo(observable, reactor, options); } protected maybeReactTo>( observable: O | undefined, reactor: Reactor, ReturnType>, options?: Reactor.Options, ) { if (observable) { this.reactTo(observable, reactor, options); } } /** * Stop reacting to specified conditions. * * @param selector the observable and/or reactor to disable; if omitted terminates all reaction */ protected stopReacting(selector?: Reactor.Selector) { return (this as unknown as Internal)[BACKING].stopReacting(selector); } /** * Create a generic callback function that has the same properties as a {@link Reactor}. * * Like a reactor, the callback's "this" will be bound to an active Behavior instance. * Because of this: The reactor MUST be a real JS function - arrow functions will not work! */ protected callback(reactor: Reactor, options?: Reactor.Options) { const observable = Observable(); this.reactTo(observable, reactor, options); return observable.emit.bind(observable) as (...args: A) => R; } /** * Create an async callback. * * @see {@link callback} */ protected asyncCallback(reactor: Reactor, options?: Reactor.Options) { const observable = AsyncObservable(); this.reactTo(observable, reactor, options); return observable.emit.bind(observable); } /** * Does this behavior support functionality of a specific implementation? */ static supports(other: Behavior.Type) { return (this as any) === other || this.prototype instanceof other; } /** * Default state values. */ static get defaults(): Record { return new this.State(); } /** * Create a new behavior with different default state values. */ static set(this: This, defaults: Behavior.InputStateOf) { return GeneratedClass({ name: this.name, base: this, staticProperties: { State: DerivedState({ name: `${this.name}$State`, base: this.State, values: defaults, }), }, }) as unknown as This; } /** * Explicitly reference other {@link Behavior}s as dependencies, ensuring this behavior is destroyed first. * * This probably won't be commonly necessary. If it is we can instrument Agent to collect dependencies * automatically. */ static dependencies?: Iterable; get [Transaction.Resource.reference]() { return (this as unknown as Internal)[BACKING].datasource; } } // TS prevents us from declaring an override type if the base field is a getter in the class. So we just declare in the // base class and manually install the getters here. Object.defineProperties(Behavior.prototype, { state: { get(this: Internal) { if (!this[STATE]) { this[STATE] = this[BACKING].datasource.reference(this.context); } return this[STATE]; }, enumerable: true, }, internal: { get(this: Internal) { if (!this[INTERNAL]) { this[INTERNAL] = this[BACKING].getInternal(); } return this[INTERNAL]; }, enumerable: false, }, events: { get(this: Internal) { if (!this[EVENTS]) { this[EVENTS] = this[BACKING].events; } return this[EVENTS]; }, enumerable: true, }, }); /** * Install {@link ClassSemantics} extension logic to integrate schema metadata. */ Object.defineProperties(Behavior, { [ClassSemantics.extend]: { value(this: Behavior.Type, decoration: ClassSemantics) { const type = decoration.new as Behavior.Type; // Support static override of schema if (Object.hasOwn(type, "schema") && type.schema !== undefined) { decoration.mutableModel = type.schema; } // Obtain state and base schema const { State, Events, defaults } = type; if (!State || !defaults) { return; } // Merge state properties into my schema if (ClassSemantics.hasOwnSemantics(State)) { const stateSemantics = Semantics.classOf(State); stateSemantics.mutableModel = decoration.mutableModel; } // Merge events into my schema if (ClassSemantics.hasOwnSemantics(Events)) { const eventSemantics = Semantics.classOf(Events); eventSemantics.mutableModel = decoration.mutableModel; } // Augment with fields for untyped state members decoration.defineUnknownMembers(defaults); }, }, }); export namespace Behavior { /** * Static properties supported by all behaviors. */ export interface Type { new (agent: Agent, backing: BehaviorBacking): Behavior; readonly name: string; readonly id: typeof Behavior.id; readonly set: typeof Behavior.set; readonly supports: typeof Behavior.supports; readonly defaults: Record; readonly schema: Schema.Struct; readonly early: boolean; readonly supervisor: RootSupervisor; readonly dependencies?: Iterable; readonly State: new () => {}; readonly Internal: new () => {}; readonly Events: typeof EventEmitter; } /** * This function simply throws NotImplementedError. More importantly, its presence in any command implementation * method informs the endpoint that the command is not implemented. */ export function unimplemented(..._args: any[]): Promise { throw new NotImplementedError(); } /** * The state type of a behavior {@link Type}. */ export type StateOf = InstanceType; /** * Input variant of StateOf. */ export type InputStateOf = Partial>; /** * Patch variant of StateOf. */ export type PatchStateOf = DeepPartial>; /** * The events type of a behavior {@link Type}. */ export type EventsOf = InstanceType; /** * Initialization options. */ export type InitializationOptionsOf = Parameters["initialize"]> extends [infer P extends object] ? P : {}; /** * Configuration options you may set when adding a {@link Behavior} to an endpoint. */ export type Options = Behavior.InputStateOf & Behavior.InitializationOptionsOf; }