/** * @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, type Duration, EventEmitter, GeneratedClass, ImplementationError, Lifetime, MaybePromise, NotImplementedError, Observable, Transaction, } from "@matter/general"; import { ClassSemantics, Schema, Semantics } from "@matter/model"; import type { Val } from "@matter/protocol"; import type { BehaviorBacking } from "./internal/BehaviorBacking.js"; import { declaredSchemaOf, setDeclaredSchema } from "./internal/DeclaredSchema.js"; import { Reactor } from "./Reactor.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. Override it with a static value, not an accessor. The override applies to this class and its * subclasses only. Once a class's schema resolves, this returns the class's effective schema, including what a * subclass adds to an inherited override. Use {@link Schema} to resolve it explicitly. */ 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); }, }; } /** * Report a change to an attribute this behavior computes on read. * * A behavior may serve an attribute from an accessor rather than a stored value (see `Val.properties`). Change * detection diffs stored values, so no `$Changed` event fires for such an attribute and nothing reports it on its * own. Call this where the behavior knows the value changed; a subscriber reads the value through the accessor * when the report is generated, so none is passed here. * * Call this once the change is durable - from a reaction to the stored value behind the attribute, say. Neither * the report nor the version it advances is part of the calling transaction, so both stand even if that * transaction goes on to fail. The version advances whether or not the framework already saw a change, so naming * a stored attribute here advances it a second time. */ protected markChanged(...attributes: string[]) { (this as unknown as Internal)[BACKING].markChanged(attributes); } /** * 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! * * Each call installs a reactor that remains until the behavior closes. Create the callback once rather than each * time it is passed on; for a timer use {@link reactorTimer} or {@link periodicReactorTimer}. */ 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 a timer that runs {@link reactor} like a reactor of this behavior when it expires. * * The timer is created stopped. Each start installs the reactor for that run only, so restarting the timer or * replacing it with a new one does not accumulate reactors. Once {@link Timer.stop} returns, the reactor does not * start, even if the timer expired and the reaction waits for a lock or for an earlier reaction. The behavior stops * the timer when it closes, and the timer does not start again after that. * * As with {@link reactTo}, the reactor MUST be a real JS function so "this" binds to an active Behavior instance. */ protected reactorTimer( name: string, duration: Duration, reactor: Reactor<[], unknown>, options?: Behavior.ReactorTimerOptions, ) { return (this as unknown as Internal)[BACKING].createTimer(name, duration, false, reactor, options); } /** * Create a periodic timer that runs {@link reactor} like a reactor of this behavior at each interval. * * @see {@link reactorTimer} */ protected periodicReactorTimer( name: string, interval: Duration, reactor: Reactor<[], unknown>, options?: Behavior.ReactorTimerOptions, ) { return (this as unknown as Internal)[BACKING].createTimer(name, interval, true, reactor, options); } /** * 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; } /** * Adjust persisted values whose stored format is incompatible with the current state. * * Runs on a copy of the stored values before the behavior's state initializes, so keys need not exist in the * schema. The behavior's next persisted write stores what changed, so until then a migration also runs on values it * already migrated and must leave those unchanged. If it throws, the stored values apply unchanged. * * Does not run for state that mirrors a peer, such as the attributes of a cluster client behavior. * * Subclasses inherit the migration. An override should invoke `super.migrate(values)` first. */ static migrate(_values: Val.Struct): void {} /** * 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, }, }); /** * Code compiled without `useDefineForClassFields` assigns a static schema override instead of defining it. The setter * turns that assignment into the declaration a class field makes, also once the hook has removed a parent's own value. */ Object.defineProperty(Behavior, "schema", { ...Object.getOwnPropertyDescriptor(Behavior, "schema"), set(this: Behavior.Type, schema: Schema) { if (ClassSemantics.hasOwnSemantics(this) && Semantics.classOf(this).isFinal) { throw new ImplementationError(`Cannot set schema of behavior ${this.name} because its schema is resolved`); } Object.defineProperty(this, "schema", { value: schema, writable: true, enumerable: true, configurable: true }); }, }); /** * Install {@link ClassSemantics} extension logic to integrate schema metadata. */ Object.defineProperties(Behavior, { [ClassSemantics.extend]: { value(this: Behavior.Type, decoration: ClassSemantics) { // Behavior's own accessor derives schema from these semantics, so Behavior declares no model if (decoration.new === Behavior) { return; } const type = decoration.new as Behavior.Type; const own = Object.getOwnPropertyDescriptor(type, "schema"); if (own !== undefined) { if (!("value" in own)) { throw new ImplementationError( `Behavior ${type.name} declares schema as an accessor; declare it as a static value instead`, ); } if (own.value !== undefined) { setDeclaredSchema(type, own.value); } // A static value shadows Behavior's getter for every subclass, so schema would not reflect what a // subclass adds Reflect.deleteProperty(type, "schema"); } // The parent resolves first, whatever this class declares, because the prototype walk skips ancestors // nothing resolved yet and resolving this class's State may otherwise freeze the parent's. This class's // model is its declared schema or else its parent's, extended below by what the class itself declares const parent: unknown = Object.getPrototypeOf(type); const parentType = isBehaviorType(parent) ? parent : undefined; const base = parentType === undefined ? undefined : Schema(parentType); const declared = declaredSchemaOf(type) ?? base; if (declared !== undefined) { decoration.mutableModel = declared; } const { State, Events, defaults } = type; if (!State || !defaults) { return; } // A State or Events class the parent already uses belongs to the parent's model if (State !== parentType?.State && ClassSemantics.hasOwnSemantics(State)) { const stateSemantics = Semantics.classOf(State); stateSemantics.mutableModel = decoration.mutableModel; } if (Events !== parentType?.Events && ClassSemantics.hasOwnSemantics(Events)) { const eventSemantics = Semantics.classOf(Events); eventSemantics.mutableModel = decoration.mutableModel; } // Augment with fields for untyped state members decoration.defineUnknownMembers(defaults); }, }, }); function isBehaviorType(value: unknown): value is Behavior.Type { return typeof value === "function" && (value === Behavior || value.prototype instanceof Behavior); } export namespace Behavior { /** * Reactor options of a timer created by {@link Behavior.reactorTimer} or {@link Behavior.periodicReactorTimer}. */ export type ReactorTimerOptions = Omit; /** * 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 migrate: typeof Behavior.migrate; 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; }