/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { ActionContext } from "#behavior/context/ActionContext.js"; import { CommissioningServer } from "#behavior/system/commissioning/CommissioningServer.js"; import { EventsBehavior } from "#behavior/system/events/EventsBehavior.js"; import { NetworkServer } from "#behavior/system/network/NetworkServer.js"; import { ServerNetworkRuntime } from "#behavior/system/network/ServerNetworkRuntime.js"; import { ProductDescriptionServer } from "#behavior/system/product-description/ProductDescriptionServer.js"; import { SessionsBehavior } from "#behavior/system/sessions/SessionsBehavior.js"; import { SubscriptionsServer } from "#behavior/system/subscriptions/SubscriptionsServer.js"; import { Endpoint } from "#endpoint/Endpoint.js"; import { ServerNodeStore } from "#storage/server/ServerNodeStore.js"; import type { Environment } from "@matter/general"; import { asyncNew, Construction, DiagnosticSource, errorOf, Identity, MatterError } from "@matter/general"; import { FabricManager, Interactable, OccurrenceManager, PeerSet, ServerInteraction, SessionManager, } from "@matter/protocol"; import { RootEndpoint as BaseRootEndpoint } from "../endpoints/root.js"; import { Node } from "./Node.js"; import { Plugins } from "./Plugins.js"; import { Peers } from "./client/Peers.js"; import { ServerEnvironment } from "./server/ServerEnvironment.js"; /** * Thrown when there is an error during factory reset. */ class FactoryResetError extends MatterError { constructor(message: string, cause: any) { super(message); this.cause = errorOf(cause); } } /** * A server-side Matter {@link Node}. * * The Matter specification often refers to server-side nodes as "devices". */ export class ServerNode extends Node { #peers?: Peers; #interaction?: Interactable; #plugins: Plugins; /** * Construct a new ServerNode. * * You can use {@link create} to construct the node and wait for it to initialize fully. * * @param type the variation of {@link RootEndpoint} that defines the root endpoint's behavior * @param options root endpoint options and the node's environment */ constructor(type?: T, options?: Node.Options); /** * Construct a new ServerNode. * * You can use {@link create} to construct the node and wait for it to initialize fully. * * @param config a {@link Endpoint.Configuration} for the root endpoint */ constructor(config: Partial>); constructor(definition?: T | Node.Configuration, options?: Node.Options) { const config = Node.nodeConfigFor(ServerNode.RootEndpoint as T, definition, options ?? ({} as Node.Options)); super(config); this.#plugins = new Plugins(this); this.env.set(Node, this); this.env.set(ServerNode, this); DiagnosticSource.add(this); this.construction.start(); } /** * Create a new ServerNode. * * @param type the variation of {@link RootEndpoint} that defines the root endpoint's behavior * @param options root endpoint configuration and, optionally, the node's environment */ static async create< This extends typeof ServerNode, T extends ServerNode.RootEndpoint = ServerNode.RootEndpoint, >(this: This, type?: T, options?: Node.Options): Promise>; /** * Create a new ServerNode. * * @param config root endpoint configuration and, optionally, the node's {@link Environment} */ static async create< This extends typeof ServerNode, T extends ServerNode.RootEndpoint = ServerNode.RootEndpoint, >(this: This, config: Partial>): Promise>; static async create< This extends typeof ServerNode, T extends ServerNode.RootEndpoint = ServerNode.RootEndpoint, >(this: This, definition?: T | Node.Configuration, options?: Node.Options) { return await asyncNew(this, definition, options); } protected createRuntime(): ServerNetworkRuntime { return new ServerNetworkRuntime(this); } override async [Construction.destruct]() { if (this.#peers) { await this.#peers.close(); } await super[Construction.destruct](); await ServerEnvironment.close(this); } override async prepareRuntimeShutdown() { await this.env.maybeGet(PeerSet)?.disconnect(); await this.env.get(SessionManager).closeAllSessions(); } /** * Perform a factory reset of the node. */ override async erase() { using _erasing = this.construction.join("erasing"); await this.lifecycle.mutex.produce(this.eraseWithMutex.bind(this)); } protected async eraseWithMutex() { try { await this.construction; // Go offline before performing reset const isOnline = this.lifecycle.isOnline; if (isOnline) { await this.cancelWithMutex(); } // Inform user this.statusUpdate("resetting to factory defaults"); // Reset in-memory state await this.resetWithMutex(); // Reset persistent state await this.resetStorage(); // Reset reverts node to inactive state; now reinitialize this.construction.start(); // Go back online if we were online at time of reset and should still be online, otherwise just await reinitialization if (isOnline && this.lifecycle.shouldBeOnline) { await this.startWithMutex(); } else { await this.construction.ready; } } catch (e) { this.construction.crash(); throw new FactoryResetError(`Error during factory reset of ${this}`, e); } } /** * Access other nodes on the fabric. */ get peers() { if (!this.#peers) { try { this.#peers = new Peers(this); this.#peers.initialize(); } catch (e) { this.#peers = undefined; throw e; } } return this.#peers; } get interaction() { if (this.#interaction === undefined) { this.#interaction = new ServerInteraction(this.protocol); } return this.#interaction; } override readonly nodeType = "server" as const; protected override async initialize() { await ServerEnvironment.initialize(this); await this.#plugins.load(); await super.initialize(); } /** * By default, on factory reset we erase all stored data. * * If this is inappropriate for your application, you may override to alter the behavior. Matter requires that all * "security- and privacy-related data and key material" is removed on factory reset. * * @see {@link MatterSpecification.v16.Core} ยง 13.4 */ protected async resetStorage() { await this.env.get(SessionManager).clear(); await this.env.get(FabricManager).clear(); await this.env.get(OccurrenceManager).clear(); await this.env.get(ServerNodeStore).erase(); } /** * Normal endpoints must have an owner to complete construction but server nodes have no such precondition for * construction. */ protected override assertConstructable() {} } export namespace ServerNode { export const RootEndpoint = BaseRootEndpoint.with( CommissioningServer, NetworkServer, ProductDescriptionServer, SubscriptionsServer, SessionsBehavior, EventsBehavior, ); export interface RootEndpoint extends Identity {} } Object.freeze(ServerNode.RootEndpoint);