/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Construction, Environment, InternalError, Logger, ObserverGroup, UdpTransport } from "@matter/general"; import { Fabric, FabricManager } from "@matter/protocol"; import { FabricIndex, GroupId } from "@matter/types"; const logger = Logger.get("ServerGroupNetworking"); export class ServerGroupNetworking { #construction: Construction; #udpInterface: UdpTransport; #activeGroupMemberships = new Map>(); #fabricObservers = new Map(); #observers = new ObserverGroup(this); get construction() { return this.#construction; } /** * The server group networking is not implemented in the Node.js environment. * This class is a placeholder to maintain compatibility with the Matter.js architecture. */ constructor(env: Environment, udpInterface: UdpTransport) { this.#udpInterface = udpInterface; this.#construction = Construction(this); this.#construction.start(env); } async [Construction.construct](env: Environment) { const fabrics = env.get(FabricManager); for (const fabric of fabrics) { if (this.#activeGroupMemberships.has(fabric.fabricIndex)) { throw new InternalError("Group transport interfaces already initialized for this fabric."); } for (const groupId of fabric.groups.groupKeyIdMap.keys()) { await this.#addGroupMembership(groupId, fabric); } this.#registerFabricGroupObserver(fabric); } // When new fabric is added we register for group changes - new fabrics cannot have groups already configured this.#observers.on(fabrics.events.added, async fabric => this.#registerFabricGroupObserver(fabric)); // When fabric is deleted, we remove the group memberships this.#observers.on(fabrics.events.deleting, async fabric => { const fabricIndex = fabric.fabricIndex; this.#observersForFabric(fabricIndex).close(); this.#fabricObservers.delete(fabricIndex); const memberships = this.#activeGroupMemberships.get(fabricIndex); if (memberships === undefined || memberships.size === 0) { this.#activeGroupMemberships.delete(fabricIndex); return; } for (const groupId of memberships.keys()) { await this.#dropGroupMembership(groupId, fabric); } this.#activeGroupMemberships.delete(fabricIndex); }); this.#observers.on(fabrics.events.replaced, async fabric => { const fabricIndex = fabric.fabricIndex; this.#observersForFabric(fabricIndex).close(); this.#fabricObservers.delete(fabricIndex); this.#registerFabricGroupObserver(fabric); // Sync (add or remove as needed) by new group configuration const { groupKeyIdMap } = fabric.groups; for (const groupId of groupKeyIdMap.keys()) { await this.#addGroupMembership(groupId, fabric); } const memberships = this.#activeGroupMemberships.get(fabricIndex) ?? new Map(); if (memberships.size !== 0) { for (const groupId of memberships.keys()) { if (!groupKeyIdMap.has(groupId)) { await this.#dropGroupMembership(groupId, fabric); } } } }); } async #addGroupMembership(groupId: GroupId, fabric: Fabric) { const fabricIndex = fabric.fabricIndex; const memberships = this.#activeGroupMemberships.get(fabricIndex) ?? new Map(); if (memberships.has(groupId)) { return; } const address = fabric.groups.multicastAddressFor(groupId); // Only join the multicast group if no other group in this fabric already uses the same address // (multiple IanaAddr groups all share ff05::fa) if (!Array.from(memberships.values()).includes(address)) { logger.debug( `Adding membership for group ${groupId} on fabric ${fabric.fabricId} (index ${fabricIndex}) with address ${address}`, ); await this.#udpInterface.addMembership(address); } memberships.set(groupId, address); this.#activeGroupMemberships.set(fabricIndex, memberships); } async #dropGroupMembership(groupId: GroupId, fabric: Fabric) { const fabricIndex = fabric.fabricIndex; const memberships = this.#activeGroupMemberships.get(fabricIndex); if (memberships === undefined || memberships.size === 0) { return; } // Use the stored address (safer than re-deriving, policy may have changed) const address = memberships.get(groupId) ?? fabric.groups.multicastAddressFor(groupId); memberships.delete(groupId); // Only leave the multicast group if no other group in this fabric still uses the same address // (multiple IanaAddr groups all share ff05::fa) if (!Array.from(memberships.values()).includes(address)) { logger.debug( `Dropping membership for group ${groupId} on fabric ${fabric.fabricId} (index ${fabricIndex}) with address ${address}`, ); await this.#udpInterface.dropMembership(address); } if (!memberships.size) { this.#activeGroupMemberships.delete(fabricIndex); } } #observersForFabric(fabricIndex: FabricIndex) { let observers = this.#fabricObservers.get(fabricIndex); if (observers === undefined) { observers = new ObserverGroup(this); this.#fabricObservers.set(fabricIndex, observers); } return observers; } #registerFabricGroupObserver(fabric: Fabric) { const fabricIndex = fabric.fabricIndex; const observers = this.#observersForFabric(fabricIndex); observers.on( fabric.groups.groupKeyIdMap.added, async groupId => await this.#addGroupMembership(groupId, fabric), ); observers.on(fabric.groups.groupKeyIdMap.deleted, async groupId => this.#dropGroupMembership(groupId, fabric)); } close() { this.#construction.close(); this.#observers.close(); this.#fabricObservers.forEach(observer => observer.close()); this.#activeGroupMemberships.clear(); this.#fabricObservers.clear(); } }