/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import type { Behavior } from "#behavior/Behavior.js"; import type { Events } from "#behavior/Events.js"; import type { BehaviorBacking } from "#behavior/internal/BehaviorBacking.js"; import type { Endpoint } from "#endpoint/Endpoint.js"; import { EndpointLifecycle } from "#endpoint/properties/EndpointLifecycle.js"; import type { Node } from "#node/Node.js"; import type { ServerNode } from "#node/ServerNode.js"; import { InternalError, Observable, ObserverGroup, Timestamp } from "@matter/general"; import { EventModel } from "@matter/model"; import { Val } from "@matter/protocol"; import { EventNumber, Priority } from "@matter/types"; /** * High-level change notification service. * * This service provides an optimized path to detecting property changes for all endpoints associated with a node. This * includes endpoints on peers. */ export class ChangeNotificationService { #change = new Observable<[changes: ChangeNotificationService.Change]>(); #observers = new Map(); constructor(node: ServerNode) { this.#beginNodeObservation(node); if (node.lifecycle.isReady) { this.#beginPeerObservation(node); } else { node.lifecycle.ready.once(() => this.#beginPeerObservation(node)); } } /** * Change event source. */ get change() { return this.#change; } /** * Invoked by the {@link BehaviorBacking} when state changes. */ broadcastUpdate(backing: BehaviorBacking, properties: string[]) { const { endpoint, type: behavior } = backing; this.#change.emit({ kind: "update", endpoint, behavior, version: backing.datasource.version, properties, }); } /** * Invoked by {@link Events} or {@link ClientEventEmitter} as events occur. */ broadcastEvent( endpoint: Endpoint, behavior: Behavior.Type, event: EventModel, occurrence: ChangeNotificationService.OccurrenceProperties, ) { this.#change.emit({ kind: "event", endpoint, behavior, event, ...occurrence, }); } close() { for (const observers of this.#observers.values()) { observers.close(); } this.#observers.clear(); } #beginNodeObservation(node: Node) { const observers = new ObserverGroup(); this.#observers.set(node, observers); observers.on(node.lifecycle.changed, (type, endpoint) => { switch (type) { case EndpointLifecycle.Change.Destroyed: this.#change.emit({ kind: "delete", endpoint, }); if (endpoint === node) { observers.close(); this.#observers.delete(node); } break; } }); } #beginPeerObservation(node: ServerNode) { const observers = this.#observers.get(node); if (observers === undefined) { throw new InternalError("Change notification initialization order is broken"); } observers.on(node.peers.added, this.#beginNodeObservation.bind(this)); } } export namespace ChangeNotificationService { export type Key = string | number; /** * Emits when state changes. * * If present, {@link properties} indicates the specific updated properties. Otherwise the recipient should * consider all properties. */ export interface PropertyUpdate { kind: "update"; endpoint: Endpoint; behavior: Behavior.Type; version: number; properties?: string[]; } export interface OccurrenceProperties { number: EventNumber; timestamp: Timestamp; priority: Priority; payload?: Val.Struct; } /** * Emits when a Matter event occurs. */ export interface EventOccurrence extends OccurrenceProperties { kind: "event"; endpoint: Endpoint; behavior: Behavior.Type; event: EventModel; } /** * Emits when endpoints/nodes are deleted. * * This indicates to the recipient to drop the associated data subtree. */ export interface EndpointDelete { kind: "delete"; endpoint: Endpoint; } export type Change = PropertyUpdate | EventOccurrence | EndpointDelete; }