Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The main work (all changes without a GitHub username in brackets in the below li
## __WORK IN PROGRESS__

- @matter/node
- Enhancement: Added automatic Command batching for non-root-endpoint commands when a node supports it and commands come in within the same macro-tick
- Fix: Prevent error when writing Thermostat systemMode attribute

- @matter/protocol
Expand Down
4 changes: 3 additions & 1 deletion packages/node/src/node/ClientNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ export class ClientNode extends Node<ClientNode.RootEndpoint> {
return new ClientNetworkRuntime(this);
}

async prepareRuntimeShutdown() {}
async prepareRuntimeShutdown() {
await this.#interaction?.close();
}

protected override get container() {
return this.owner?.peers;
Expand Down
56 changes: 0 additions & 56 deletions packages/node/src/node/client/ClientCommandMethod.ts

This file was deleted.

10 changes: 10 additions & 0 deletions packages/node/src/node/client/ClientGroupInteraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@ import {
WriteResult,
} from "#protocol";
import { ClientNodeInteraction } from "./ClientNodeInteraction.js";
import { CommandInvoker } from "./commands/CommandInvoker.js";

export class InvalidGroupOperationError extends ImplementationError {}

export class ClientGroupInteraction extends ClientNodeInteraction {
/**
* Groups use a plain {@link CommandInvoker} without batching.
* Group invokes always use suppressResponse and don't have device details
* like maxPathsPerInvoke, so batching is not applicable.
*/
protected override createInvoker() {
return new CommandInvoker(this.node);
}

/** Groups do not support reading or subscribing to attributes */
override read(_request: Read, _context?: ActionContext): ReadResult {
throw new InvalidGroupOperationError("Groups do not support reading attributes");
Expand Down
43 changes: 42 additions & 1 deletion packages/node/src/node/client/ClientNodeInteraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import type { ActionContext } from "#behavior/context/ActionContext.js";
import { EndpointInitializer } from "#endpoint/properties/EndpointInitializer.js";
import type { CommandInvoker } from "#node/client/commands/CommandInvoker.js";
import type { ClientNode } from "#node/ClientNode.js";
import { NodePhysicalProperties } from "#node/NodePhysicalProperties.js";
import {
Expand All @@ -27,18 +28,58 @@ import {
} from "#protocol";
import { EndpointNumber } from "#types";
import { ClientEndpointInitializer } from "./ClientEndpointInitializer.js";
import { CommandBatcher } from "./commands/CommandBatcher.js";

/**
* A {@link ClientInteraction} that brings the node online before attempting interaction.
*/
export class ClientNodeInteraction implements Interactable<ActionContext> {
#node: ClientNode;
readonly #node: ClientNode;
#physicalProps?: PhysicalDeviceProperties;
#invoker?: CommandInvoker;

constructor(node: ClientNode) {
this.#node = node;
}

/**
* The node this interaction is associated with.
*/
protected get node(): ClientNode {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little leary of exposing internal implementation details like this as if you need to ask an Interactable what it's node is then you're limiting the flexibility of your logic.

And we already have a way to convey "this logic requires a ClientNodeInteraction + ClientNode", which is just to take the ClientNode as an argument instead of ClientNodeInteraction

return this.#node;
}

/**
* Command invoker for this interaction.
*
* For regular client nodes, returns a {@link CommandBatcher} that collects commands
* invoked within the same timer tick and sends them as a single batched invoke-request.
*
* Override in subclasses to provide different invoker behavior (e.g., groups use plain
* {@link CommandInvoker} without batching).
*/
get invoker() {
if (this.#invoker === undefined) {
this.#invoker = this.createInvoker();
}
return this.#invoker;
}

/**
* Create the command invoker for this interaction.
* Override in subclasses to provide different invoker types.
*/
protected createInvoker(): CommandInvoker {
return new CommandBatcher(this.#node);
}

/**
* Close the interaction and release resources.
*/
async close() {
await this.#invoker?.close();
}

/**
* The current session used for interaction with the node if any session is established, otherwise undefined.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/node/src/node/client/PeerBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
UnknownAttribute,
UnknownCommand,
} from "#types";
import { ClientCommandMethod } from "./ClientCommandMethod.js";
import { ClientCommandMethod } from "./commands/ClientCommandMethod.js";

const BIT_BLOCK_SIZE = Math.log2(Number.MAX_SAFE_INTEGER);

Expand Down
35 changes: 35 additions & 0 deletions packages/node/src/node/client/commands/ClientCommandMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2022-2026 Matter.js Authors
* SPDX-License-Identifier: Apache-2.0
*/

import { ClusterBehavior } from "#behavior/cluster/ClusterBehavior.js";
import type { ClientNode } from "#node/ClientNode.js";
import { Node } from "#node/Node.js";
import type { ClientNodeInteraction } from "../ClientNodeInteraction.js";

/**
* Create the command method for a client behavior.
*
* Commands are batched automatically - multiple commands invoked within the same timer tick
* are sent as a single batched invoke request for efficiency.
*/
export function ClientCommandMethod(name: string) {
// This is our usual hack to give a function a proper name in stack traces
const temp = {
// The actual implementation
async [name](this: ClusterBehavior, fields?: {}) {
const node = this.env.get(Node) as ClientNode;

return (node.interaction as ClientNodeInteraction).invoker.invoke({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of implementing this outside of ClientNodeInteraction vs. making it an internal implementation detail of invoke?

endpoint: this.endpoint,
cluster: this.cluster,
command: name,
fields,
});
},
};

return temp[name];
}
Loading
Loading