/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { deepCopy, ImplementationError, isObject } from "@matter/general"; import type { Schema } from "@matter/model"; import { DataModelPath, Metatype, StoredDefaultValue, ValueModel } from "@matter/model"; import { SchemaImplementationError, Val, WriteError } from "@matter/protocol"; import { RootSupervisor } from "../../../supervision/RootSupervisor.js"; import { ValueSupervisor } from "../../../supervision/ValueSupervisor.js"; /** * Obtain a {@link ValueSupervisor.Patch} function for the given schema. */ export function ValuePatcher(schema: Schema, supervisor: RootSupervisor) { switch (schema.effectiveMetatype) { // "any" means the schema defines no type. Assume it's an object since ValuePatcher is only invoked where // an object is expected naturally case Metatype.any: case Metatype.object: return StructPatcher(schema as ValueModel, supervisor); case Metatype.array: return ListPatcher(schema as ValueModel, supervisor); default: return PrimitivePatcher(); } } // Defaults depend on the features active in the supervisor's scope, and supervisors with different features share // nested schema identity, so schema alone is not a safe key const defaultsCache = new WeakMap>(); /** * Obtain the values storage holds for a struct absent an explicit value. * * The result is cached and shared, so callers copy before handing it to a mutable value. */ function getDefaults(supervisor: RootSupervisor, schema: Schema): Val.Struct { let schemaDefaults = defaultsCache.get(supervisor); if (schemaDefaults === undefined) { defaultsCache.set(supervisor, (schemaDefaults = new WeakMap())); } const cached = schemaDefaults.get(schema); if (cached !== undefined) { return cached; } const { scope } = supervisor; const defaults = {} as Val.Struct; for (const member of supervisor.membersOf(schema)) { const value = StoredDefaultValue(scope, member); if (value !== undefined) { defaults[member.propertyName] = value; } } schemaDefaults.set(schema, defaults); return defaults; } /** * Create a function that takes a patch object and applies it to a target object. */ function StructPatcher(schema: ValueModel, supervisor: RootSupervisor): ValueSupervisor.Patch { // Membership of these maps decides whether a patch names a member, so they carry no prototype: an inherited key // such as "toString" would otherwise pass for a member the schema never declared // // An object mapping name to a patch function for sub-collections and undefined otherwise const memberPatchers: Record = Object.create(null); // An object mapping name to default value (if any) for sub-structs const memberDefaults: Record = Object.create(null); // An object mapping name to true if member is an array const memberArrays: Record = Object.create(null); // An object mapping alt keys (numeric IDs) to member names, if numeric IDs are defined const memberAltKeys: Record = Object.create(null); for (const member of supervisor.membersOf(schema)) { const metatype = member.effectiveMetatype; let handler: ValueSupervisor.Patch | undefined; if (metatype === Metatype.object || metatype === Metatype.array) { handler = supervisor.get(member).patch; } const key = member.propertyName; if (member.id !== undefined) { memberAltKeys[member.id.toString()] = key; } memberPatchers[key] = handler; if (metatype === Metatype.object) { memberDefaults[key] = getDefaults(supervisor, member); } if (metatype === Metatype.array) { memberArrays[key] = true; } } return (changes, target, path) => { // Validate changes if (typeof changes !== "object" || changes === null || Array.isArray(changes)) { throw new WriteError(path, `patch definition ${changes} is not an object`); } // Validate target if (typeof target !== "object" || target === null || Array.isArray(target)) { throw new WriteError(path, `cannot patch ${target} because it is not an object`); } for (let key in changes as Val.Struct) { let newValue = (changes as Val.Struct)[key]; // Validate the key if (!(key in memberPatchers)) { if (!(key in memberAltKeys)) { throw new WriteError(path, `${key} is not a property of ${schema.name}`); } key = memberAltKeys[key]; } // If this is not a subcollection or the new value is not an object, just do direct set const subpatch = memberPatchers[key]; if (!subpatch || newValue === null || typeof newValue !== "object") { target[key] = newValue; continue; } // If the target is a list but currently empty, create by patching an empty array. This ensures arrays of // structs have defaults initialized if (memberArrays[key]) { if (target[key] === undefined || target[key] === null) { newValue = subpatch(newValue as Val.Collection, [], path.at(key)); target[key] = newValue; continue; } } // If the field is a struct but currently empty, create by patching over defaults if (target[key] === undefined || target[key] === null) { newValue = subpatch(newValue as Val.Collection, deepCopy(memberDefaults[key]) ?? {}, path.at(key)); target[key] = newValue; continue; } // Patch existing container. Casts to collection here may be incorrect but the subpatcher will validate // input and throw for non-collections subpatch(newValue as Val.Collection, target[key] as Val.Collection, path.at(key)); } return target; }; } /** * Creates a function that takes a patch object and applies it to a target array. */ function ListPatcher(schema: ValueModel, supervisor: RootSupervisor): ValueSupervisor.Patch { const entry = schema.listEntry; if (entry === undefined) { throw new SchemaImplementationError(new DataModelPath(schema.path), "List schema has no entry definition"); } const entryMetatype = entry?.effectiveMetatype; let patchEntry: ValueSupervisor.Patch | undefined; if (entryMetatype === Metatype.object || entryMetatype === Metatype.array) { patchEntry = supervisor.get(entry).patch; } let entryDefaults: Val.Struct | undefined; if (entryMetatype === Metatype.object) { entryDefaults = getDefaults(supervisor, entry); } return (changes, target, path) => { // Validate changes if (typeof changes !== "object" || changes === null) { throw new WriteError(path, `patch definition ${changes} is not an object`); } // Validate target if (!Array.isArray(target)) { throw new WriteError(path, `patch definition ${changes} is not an object`); } // eslint reports error here when linting entire project but not individual file. Unsure if this is a bug but // disabling as code is correct as written // oxlint-disable-next-line @typescript-eslint/no-for-in-array for (const indexStr in changes) { const index = Number.parseInt(indexStr); if (index < 0 || !Number.isFinite(index)) { throw new WriteError(path, `key ${index} is not a valid array index`); } // Changes may be an array or object but JS allows string indices either way even though TS doesn't let newValue = (changes as Val.Struct)[indexStr]; if (patchEntry) { const oldValue = index < target.length ? target[index] : undefined; if (newValue === undefined || newValue === null || oldValue === undefined || oldValue === null) { // If creating a new object, apply as a patch to the object's defaults before insertion if (entryDefaults && isObject(newValue)) { newValue = patchEntry(newValue as Val.Collection, deepCopy(entryDefaults), path.at(index)); } target[index] = newValue; } else { patchEntry(newValue as Val.Collection, target[index] as Val.Collection, path.at(index)); } } else { target[index] = newValue; } } if (Array.isArray(changes)) { // We set an Array and no patching object, so set the length as requested target.length = changes.length; } return target; }; } function PrimitivePatcher(): ValueSupervisor.Patch { return (_changes, _target, path) => { throw new ImplementationError(`Cannot generate patch ${path} because it does not define not a collection`); }; }