/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Events } from "#behavior/Events.js"; import type { Agent } from "#endpoint/Agent.js"; import { ImplementationError, MaybePromise } from "@matter/general"; import { ClusterModifier, ElementTag, Schema } from "@matter/model"; import { ClusterType, type ClusterTyping } from "@matter/types"; import { Behavior } from "../Behavior.js"; import type { BehaviorBacking } from "../internal/BehaviorBacking.js"; import type { RootSupervisor } from "../supervision/RootSupervisor.js"; import { NetworkBehavior } from "../system/network/NetworkBehavior.js"; import { ExtensionInterfaceOf, isClientBehavior } from "./cluster-behavior-utils.js"; import { ClusterBehaviorType } from "./ClusterBehaviorType.js"; import type { ClusterEvents } from "./ClusterEvents.js"; import { ClusterInterface } from "./ClusterInterface.js"; import type { ClusterState } from "./ClusterState.js"; /** * A {@link Behavior} with specialization for a specific cluster. * * To implement cluster functionality you should use a subclass provided by {@link ClusterBehavior.for} with the * cluster namespace you are implementing. Most commonly you would use one of the predefined implementations * matter.js includes. * * Subclasses can be modified using the static builder methods or extended like a normal class. * * Behaviors should store all mutable state in their {@link Behavior.state} property. Other fields will be transient * and may not be retained across invocations. * * ClusterBehaviors may be instantiated with unsupported mandatory commands and attributes. This is currently results * in a runtime error, but it will not cause a type error during development. */ export class ClusterBehavior extends Behavior { declare static readonly schema: Schema.Cluster; /** * The ID of ClusterBehavior implementations is the uncapitalized cluster name. */ static override id: Uncapitalize; /** * The cluster namespace associated with this behavior. */ get cluster() { return (this.constructor as typeof ClusterBehavior).cluster; } /** * Supported features as a flag object. */ get features() { return (this.constructor as ClusterBehavior.Type).features; } override get type() { return this.constructor as ClusterBehavior.Type; } /** * Every cluster behavior has an associated cluster namespace defined statically. */ static readonly cluster: ClusterType.Concrete = ClusterType.Unknown; /** * Supported features as a flag object. */ static readonly features: Record = {}; /** * Method definitions. */ static readonly Interface = ClusterInterface.Empty; /** * All ClusterBehavior initialization currently runs as part of {@link Endpoint} initialization. */ static override readonly early = true; /** * Keep networking alive until I'm destroyed. */ static override readonly dependencies = [NetworkBehavior]; /** * Automatically lock state on command invoke. */ static readonly lockOnInvoke = true; constructor(agent: Agent, backing: BehaviorBacking) { super(agent, backing); const cluster = (this.constructor as typeof ClusterBehavior).cluster; if (!cluster) { throw new ImplementationError("ClusterBehavior class has no cluster defined"); } } /** * Create a new behavior for a specific cluster namespace. * * If you invoke directly on {@link ClusterBehavior} you will receive a new implementation that reports all commands * as unimplemented. * * If you invoke on an existing subclass, you will receive a new implementation with the cluster in the subclass * replaced. You should generally only do this with a namespace with the same cluster ID. * * Feature selection comes from {@link ns} if it designates features, otherwise it is unchanged. Use * {@link withFeatures} to select features. */ static for( this: This, ns: NS, schema?: Schema.Cluster, name?: string, ): ClusterBehavior.Type & { readonly id: Uncapitalize; }; static for(this: ClusterBehavior.Type, ns: ClusterType, schema?: Schema.Cluster, name?: string) { return ClusterBehaviorType({ namespace: ns, base: this, schema, name, }); } /** * Create a new behavior with different cluster features. */ static withFeatures< This extends ClusterBehavior.Type, const FeaturesT extends ClusterType.FeatureSelection>, >(this: This, ...features: FeaturesT) { return ClusterBehaviorType({ namespace: this.cluster, base: this, features, }) as unknown as ClusterBehavior.Type< This, ClusterType.WithSupportedFeatures< ClusterInterface.InterfaceOf, ClusterType.FeaturesAsFlags, FeaturesT> > >; } /** * Alias for {@link withFeatures}. */ static with< This extends ClusterBehavior.Type, const FeaturesT extends ClusterType.FeatureSelection>, >(this: This, ...features: FeaturesT) { return this.withFeatures(...features); } /** * Create a new behavior with modified cluster elements. */ static alter< This extends ClusterBehavior.Type, const AlterationsT extends ClusterType.Alterations>, >(this: This, alterations: AlterationsT) { const schema = ClusterModifier.applyRequirements(clusterSchemaOf(this, "alter"), alterations); return this.for(this.cluster, schema) as unknown as ClusterBehavior.Type< This, ClusterType.WithEnabledAttributes< ClusterType.WithEnabledEvents< ClusterInterface.InterfaceOf, ClusterType.AlteredMandatoryEventKeysOf >, ClusterType.AlteredMandatoryAttributeKeysOf > >; } /** * Create a new behavior with additional cluster features marked "mandatory". * * This informs matter.js that an application supports these elements. */ static enable< This extends ClusterBehavior.Type, const FlagsT extends ClusterType.ElementFlags>, >(this: This, flags: FlagsT) { const schema = ClusterModifier.applyPresence(clusterSchemaOf(this, "enable"), flags); return this.for(this.cluster, schema) as unknown as ClusterBehavior.Type< This, ClusterType.WithEnabledAttributes< ClusterType.WithEnabledEvents< ClusterInterface.InterfaceOf, ClusterType.EnabledEventKeysOf >, ClusterType.EnabledAttributeKeysOf > >; } /** * Returns this behavior type with all cluster elements present regardless of feature selection. * * This is type-only; at runtime the deconflicted conformance mode already includes all elements. */ static get complete(): ClusterBehavior.Type { return this as any; } static isType(type: Behavior.Type): type is ClusterBehavior.Type { return typeof (type as ClusterBehavior.Type)?.withFeatures === "function"; } static override supports(other: Behavior.Type) { if (!ClusterBehavior.isType(other)) { return false; } // Special case for "client" behaviors. We implement these with the "complete" cluster so the interface offers // type-safe access to the entire cluster. The only exception to this is for non-nullable mandatory state // values. If the peer does not implement such a value then it will be undefined even though our type says it // won't be. This however is not an issue specific to the complete cluster so we do not need to worry about it // here // // Further, we know the "Client" classes can have no extension methods or properties, so we don't need to do an // exact class match for type safety if (isClientBehavior(other) && other.schema.id === this.schema.id) { return true; } if (!Behavior.supports.call(this, other)) { return false; } const otherFeatures = other.schema.supportedFeatures; for (const name of otherFeatures) { if (!this.schema.supportedFeatures.has(name)) { return false; } } return true; } requireAttributeEnabled( this: This, attributeName: K, ): Exclude { if ((this.state as any)[attributeName] === undefined) { throw new ImplementationError( `To use this feature, please enable attribute ${String(attributeName)} by setting the value during initialization`, ); } return (this.state as any)[attributeName]; } assertAttributeEnabled(this: This, attributeName: K): void { if ((this.state as any)[attributeName] === undefined) { throw new ImplementationError( `To use this feature, please enable attribute ${String(attributeName)} by setting the value during initialization`, ); } } static override Events = Events; } /** * The resolved cluster schema of a behavior type. * * Resolves explicitly because `schema` of a subclass whose parent's static override has not resolved yet returns that * override, without what the subclass adds. */ function clusterSchemaOf(type: Behavior.Type, operation: string) { const schema = Schema(type); if (schema?.tag !== ElementTag.Cluster) { throw new ImplementationError(`Cannot ${operation} behavior ${type.name} because it has no cluster schema`); } return schema; } export namespace ClusterBehavior { /** * A ClusterBehavior specialized for a specific cluster namespace. */ export interface Type< B extends Behavior.Type = Behavior.Type, N extends ClusterTyping = ClusterInterface.InterfaceOf, NS extends ClusterType = ClusterType.Concrete, // I and ID are redundant — they duplicate information already derivable from B. They exist because tsc < 6 // deeply expands B's structure when it encounters indexed access (B["Internal"]) or infer...extends // (B extends { id: infer S extends ... }) during declaration emit. This transitively triggers contravariant // checking on N, breaking `.with().alter()` chains with TS2684. Pre-computing as separate parameters avoids // the expansion. tsgo handles this correctly; tsc 6 has not been tested. Once tsc < 6 is no longer // supported, these parameters can be inlined back into the property types. I = B["Internal"], // inline: B["Internal"] ID extends Uncapitalize = B extends { readonly id: infer S extends Uncapitalize } // inline: B extends { readonly id: infer S extends Uncapitalize } ? S : Uncapitalize ? S : Uncapitalize, > { new (agent: Agent, backing: BehaviorBacking): Instance; readonly name: string; /** * The behavior ID for ClusterBehaviors is the name of the cluster. */ readonly id: ID; /** * The cluster namespace for this behavior. */ readonly cluster: NS; /** * Supported features as a flag object. */ readonly features: ClusterType.FeaturesOf; readonly Events: ClusterEvents.Type; readonly State: new () => ClusterState.Type; readonly Internal: I; readonly Interface: N; readonly schema: Schema.Cluster; readonly early: boolean; readonly defaults: ClusterState.Type; readonly supervisor: RootSupervisor; readonly dependencies?: Iterable; readonly migrate: typeof Behavior.migrate; supports: typeof ClusterBehavior.supports; readonly ExtensionInterface: ExtensionInterfaceOf; readonly lockOnInvoke: boolean; /** * Create a new behavior for a specific cluster namespace. */ for( this: This, ns: NS, schema?: Schema, name?: string, ): ClusterBehavior.Type & { readonly id: Uncapitalize; }; /** * Create a new behavior with different cluster features. */ withFeatures< This extends ClusterBehavior.Type, const FeaturesT extends ClusterType.FeatureSelection>, >( this: This, ...features: FeaturesT ): ClusterBehavior.Type< This, ClusterType.WithSupportedFeatures< ClusterInterface.InterfaceOf, ClusterType.FeaturesAsFlags, FeaturesT> > >; /** * Alias for {@link withFeatures}. */ with< This extends ClusterBehavior.Type, const FeaturesT extends ClusterType.FeatureSelection>, >( this: This, ...features: FeaturesT ): ClusterBehavior.Type< This, ClusterType.WithSupportedFeatures< ClusterInterface.InterfaceOf, ClusterType.FeaturesAsFlags, FeaturesT> > >; /** * Create a new behavior with modified cluster elements. */ alter< This extends ClusterBehavior.Type, const AlterationsT extends ClusterType.Alterations>, >( this: This, alterations: AlterationsT, ): ClusterBehavior.Type< This, ClusterType.WithEnabledAttributes< ClusterType.WithEnabledEvents< ClusterInterface.InterfaceOf, ClusterType.AlteredMandatoryEventKeysOf >, ClusterType.AlteredMandatoryAttributeKeysOf > >; set(this: This, defaults: Behavior.InputStateOf): This; enable< This extends ClusterBehavior.Type, const FlagsT extends ClusterType.ElementFlags>, >( this: This, flags: FlagsT, ): ClusterBehavior.Type< This, ClusterType.WithEnabledAttributes< ClusterType.WithEnabledEvents< ClusterInterface.InterfaceOf, ClusterType.EnabledEventKeysOf >, ClusterType.EnabledAttributeKeysOf > >; } /** * A fully-typed ClusterBehavior. This type is derived by combining properties of the base type with properties * contributed by the cluster. */ export type Instance = // Base class ClusterBehavior & // Bring extensions of old class forward Omit< InstanceType, | "cluster" | "state" | "events" | "initialize" // Typescript 5.3 gets confused and thinks this is an instance property if we don't omit and then add // (as we do below) | typeof Symbol.asyncDispose // Omit command methods of old cluster | keyof ClusterInterface.MethodsOf> // Omit methods defined in ExtensionInterface | keyof ExtensionInterfaceOf > & // Add command methods ClusterInterface.MethodsOf & // Add methods defined manually in ExtensionInterface ExtensionInterfaceOf & { // Cluster-specific members /** * The cluster namespace. */ cluster: ClusterType.Concrete; /** * State values for the behavior. */ readonly state: ClusterState; /** * Observables for cluster events and attribute changes. */ readonly events: ClusterEvents; /** * Supported features as a flag object. */ readonly features: ClusterType.FeaturesOf; [Symbol.asyncDispose](): MaybePromise; }; /** * A behavior type with all cluster elements exposed regardless of feature selection. * * Used by client behaviors to provide access to all commands, attributes, and events. */ export interface Complete< B extends Behavior.Type = Behavior.Type, NS extends ClusterType = ClusterType.Concrete, > extends Omit, "new" | "Events" | "State"> { new (agent: Agent, backing: BehaviorBacking): CompleteInstance; readonly Events: ClusterEvents.CompleteType; readonly State: new () => ClusterState.Complete; } /** * A fully-typed instance with all cluster elements present regardless of feature selection. */ export type CompleteInstance = ClusterBehavior & Omit< InstanceType, | "cluster" | "state" | "events" | "initialize" | typeof Symbol.asyncDispose | keyof ClusterInterface.MethodsOf> | keyof ExtensionInterfaceOf > & ClusterInterface.AllMethodsOf & ExtensionInterfaceOf & { cluster: ClusterType.Concrete; readonly state: ClusterState.Complete; readonly events: ClusterEvents.Complete; readonly features: ClusterType.FeaturesOf; [Symbol.asyncDispose](): MaybePromise; }; /** * This is an unfortunate kludge required to work around https://github.com/microsoft/TypeScript/issues/27965. It * allows you to designate extension methods available on behavior instances. * * Methods designated in this way make it so you can override methods using the syntax: * * override foo() {} * * rather than: * * override foo: () => {} * * This is also required for protected properties because TypeScript doesn't include them in the mapped types our * typing system uses to define modified behaviors. * * See {@link ClusterInterface} for more details. */ export declare const ExtensionInterface: {}; export function is(type: Behavior.Type): type is ClusterBehavior.Type { return "cluster" in type; } /** * The cluster behaviors among {@link types}, in order. */ export function typesOf(types: Iterable) { const clusters = new Array(); for (const type of types) { if (ClusterBehavior.isType(type)) { clusters.push(type); } } return clusters; } }