/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Access, FieldModel, Schema, Scope } from "@matter/model"; import type { Val } from "@matter/protocol"; import type { Behavior } from "../Behavior.js"; import type { StateType } from "../state/StateType.js"; import { RootSupervisor } from "./RootSupervisor.js"; /** * Create a {@link RootSupervisor} for a {@link Behavior}. * * {@link Behavior} uses this internally for its {@link Behavior.supervisor}. * * BehaviorSupervisor derives operational schema from a "logical" schema, the {@link BehaviorSupervisor.Options.schema} * it receives or else {@link Schema.empty}. * * This function loads the logical schema and mutates as required. This includes addition of fields for any * programmatic extensions of state. This allows schema-driven logic to process state fields added in pure JS. * * The {@link RootSupervisor} is then constructed from the mutated logical schema. */ export function BehaviorSupervisor(options: BehaviorSupervisor.Options): RootSupervisor { const logical = options.schema ?? Schema.empty; // Add fields for programmatic extensions const schema = addExtensionFields(logical, new options.State()); return RootSupervisor.for(schema); } export namespace BehaviorSupervisor { export interface Options { id: string; schema?: Schema; State: StateType; } } const extendedSchemaCache = new Map>(); /** * Adds a field for every property in state that isn't defined in the schema. * * Note 1: We skip all attributes, not just those that are applicable given the supported features. * * Note 2: Finding fields isn't as simple as just using "for ... in" because ES6 class accessors are non-enumerable. So * we instead search the prototype chain for read/write descriptors. * * Note 3: We can't do anything with types either. This means e.g. writing to subfields won't behave as expected. * Really to do things correctly behavior authors should hand-craft schema. Once we can configure with decorators this * will be more straightforward. * * Note 4: We cache the combination of schema + extension fields. This is a small win but primarily to enable caching * of other tiers that use the schema as a cache key. */ function addExtensionFields(schema: Schema, defaultState: Val.Struct) { const props = new Set(); const scope = Scope(schema, { forceOwner: true }); for (const field of scope.membersOf(schema, { conformance: "deconflicted" })) { props.add(field.propertyName); } let newProperties: Record | undefined; function addProperties(object: null | Val.Struct) { if (!object || object === Object.prototype) { return; } const descriptors = Object.getOwnPropertyDescriptors(object); for (const name in descriptors) { if (props.has(name) || name === "constructor") { continue; } if (!props.has(name)) { props.add(name); const descriptor = descriptors[name]; const fixed = !descriptor.writable && !descriptor.set; if (newProperties === undefined) { newProperties = {}; } newProperties[name] = fixed; } } addProperties(Object.getPrototypeOf(object)); } addProperties(defaultState); if (newProperties === undefined) { return schema; } // The secondary cache key is formed from the fields we are adding const cacheKey = Object.entries(newProperties) .map(([name, fixed]) => `${name}/${fixed}`) .join(","); let cacheBucket = extendedSchemaCache.get(schema); if (cacheBucket === undefined) { extendedSchemaCache.set(schema, (cacheBucket = {})); } else if (cacheKey in cacheBucket) { return cacheBucket[cacheKey]; } // Create the extension schema = schema.extend(); for (const name in newProperties) { const field = new FieldModel({ name, type: "any", access: new Access({ rw: Access.Rw.ReadWrite, readPriv: Access.Privilege.View, writePriv: Access.Privilege.Operate, }), }); if (newProperties[name]) { field.quality = "F"; } schema.children.push(field); } cacheBucket[cacheKey] = schema; return schema; }