@matter/protocol 0.13.1-alpha.0-20250429-fd3557399 → 0.13.1-alpha.0-20250502-43a54f780

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/cjs/action/response/ReadResult.d.ts +4 -0
  2. package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
  3. package/dist/cjs/action/server/AttributeResponse.d.ts +1 -1
  4. package/dist/cjs/action/server/AttributeResponse.d.ts.map +1 -1
  5. package/dist/cjs/action/server/AttributeResponse.js +37 -17
  6. package/dist/cjs/action/server/AttributeResponse.js.map +2 -2
  7. package/dist/cjs/interaction/AttributeDataEncoder.d.ts +1 -1
  8. package/dist/cjs/interaction/AttributeDataEncoder.d.ts.map +1 -1
  9. package/dist/cjs/interaction/InteractionEndpointStructure.d.ts +37 -1
  10. package/dist/cjs/interaction/InteractionEndpointStructure.d.ts.map +1 -1
  11. package/dist/cjs/interaction/InteractionEndpointStructure.js +34 -15
  12. package/dist/cjs/interaction/InteractionEndpointStructure.js.map +1 -1
  13. package/dist/cjs/interaction/InteractionMessenger.js +1 -1
  14. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  15. package/dist/cjs/interaction/ServerSubscription.d.ts +1 -2
  16. package/dist/cjs/interaction/ServerSubscription.d.ts.map +1 -1
  17. package/dist/cjs/interaction/ServerSubscription.js +20 -20
  18. package/dist/cjs/interaction/ServerSubscription.js.map +1 -1
  19. package/dist/cjs/interaction/SubscriptionClient.d.ts +2 -1
  20. package/dist/cjs/interaction/SubscriptionClient.d.ts.map +1 -1
  21. package/dist/cjs/interaction/SubscriptionClient.js +2 -1
  22. package/dist/cjs/interaction/SubscriptionClient.js.map +1 -1
  23. package/dist/cjs/interaction/index.d.ts +0 -1
  24. package/dist/cjs/interaction/index.d.ts.map +1 -1
  25. package/dist/cjs/interaction/index.js +0 -1
  26. package/dist/cjs/interaction/index.js.map +1 -1
  27. package/dist/cjs/protocol/ExchangeManager.d.ts.map +1 -1
  28. package/dist/cjs/protocol/ExchangeManager.js +17 -2
  29. package/dist/cjs/protocol/ExchangeManager.js.map +1 -1
  30. package/dist/cjs/protocol/MessageExchange.d.ts +2 -1
  31. package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
  32. package/dist/cjs/protocol/MessageExchange.js +6 -3
  33. package/dist/cjs/protocol/MessageExchange.js.map +1 -1
  34. package/dist/cjs/protocol/ProtocolHandler.d.ts +1 -0
  35. package/dist/cjs/protocol/ProtocolHandler.d.ts.map +1 -1
  36. package/dist/cjs/securechannel/SecureChannelProtocol.d.ts +1 -0
  37. package/dist/cjs/securechannel/SecureChannelProtocol.d.ts.map +1 -1
  38. package/dist/cjs/securechannel/SecureChannelProtocol.js +1 -0
  39. package/dist/cjs/securechannel/SecureChannelProtocol.js.map +1 -1
  40. package/dist/cjs/session/case/CaseServer.d.ts +1 -0
  41. package/dist/cjs/session/case/CaseServer.d.ts.map +1 -1
  42. package/dist/cjs/session/case/CaseServer.js +1 -0
  43. package/dist/cjs/session/case/CaseServer.js.map +1 -1
  44. package/dist/cjs/session/pase/PaseServer.d.ts +1 -0
  45. package/dist/cjs/session/pase/PaseServer.d.ts.map +1 -1
  46. package/dist/cjs/session/pase/PaseServer.js +1 -0
  47. package/dist/cjs/session/pase/PaseServer.js.map +1 -1
  48. package/dist/esm/action/response/ReadResult.d.ts +4 -0
  49. package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
  50. package/dist/esm/action/server/AttributeResponse.d.ts +1 -1
  51. package/dist/esm/action/server/AttributeResponse.d.ts.map +1 -1
  52. package/dist/esm/action/server/AttributeResponse.js +37 -17
  53. package/dist/esm/action/server/AttributeResponse.js.map +1 -1
  54. package/dist/esm/interaction/AttributeDataEncoder.d.ts +1 -1
  55. package/dist/esm/interaction/AttributeDataEncoder.d.ts.map +1 -1
  56. package/dist/esm/interaction/InteractionEndpointStructure.d.ts +37 -1
  57. package/dist/esm/interaction/InteractionEndpointStructure.d.ts.map +1 -1
  58. package/dist/esm/interaction/InteractionEndpointStructure.js +21 -7
  59. package/dist/esm/interaction/InteractionEndpointStructure.js.map +1 -1
  60. package/dist/esm/interaction/InteractionMessenger.js +1 -1
  61. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  62. package/dist/esm/interaction/ServerSubscription.d.ts +1 -2
  63. package/dist/esm/interaction/ServerSubscription.d.ts.map +1 -1
  64. package/dist/esm/interaction/ServerSubscription.js +4 -4
  65. package/dist/esm/interaction/ServerSubscription.js.map +1 -1
  66. package/dist/esm/interaction/SubscriptionClient.d.ts +2 -1
  67. package/dist/esm/interaction/SubscriptionClient.d.ts.map +1 -1
  68. package/dist/esm/interaction/SubscriptionClient.js +2 -1
  69. package/dist/esm/interaction/SubscriptionClient.js.map +1 -1
  70. package/dist/esm/interaction/index.d.ts +0 -1
  71. package/dist/esm/interaction/index.d.ts.map +1 -1
  72. package/dist/esm/interaction/index.js +0 -1
  73. package/dist/esm/interaction/index.js.map +1 -1
  74. package/dist/esm/protocol/ExchangeManager.d.ts.map +1 -1
  75. package/dist/esm/protocol/ExchangeManager.js +17 -2
  76. package/dist/esm/protocol/ExchangeManager.js.map +1 -1
  77. package/dist/esm/protocol/MessageExchange.d.ts +2 -1
  78. package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
  79. package/dist/esm/protocol/MessageExchange.js +6 -3
  80. package/dist/esm/protocol/MessageExchange.js.map +1 -1
  81. package/dist/esm/protocol/ProtocolHandler.d.ts +1 -0
  82. package/dist/esm/protocol/ProtocolHandler.d.ts.map +1 -1
  83. package/dist/esm/securechannel/SecureChannelProtocol.d.ts +1 -0
  84. package/dist/esm/securechannel/SecureChannelProtocol.d.ts.map +1 -1
  85. package/dist/esm/securechannel/SecureChannelProtocol.js +1 -0
  86. package/dist/esm/securechannel/SecureChannelProtocol.js.map +1 -1
  87. package/dist/esm/session/case/CaseServer.d.ts +1 -0
  88. package/dist/esm/session/case/CaseServer.d.ts.map +1 -1
  89. package/dist/esm/session/case/CaseServer.js +1 -0
  90. package/dist/esm/session/case/CaseServer.js.map +1 -1
  91. package/dist/esm/session/pase/PaseServer.d.ts +1 -0
  92. package/dist/esm/session/pase/PaseServer.d.ts.map +1 -1
  93. package/dist/esm/session/pase/PaseServer.js +1 -0
  94. package/dist/esm/session/pase/PaseServer.js.map +1 -1
  95. package/package.json +6 -6
  96. package/src/action/response/ReadResult.ts +4 -0
  97. package/src/action/server/AttributeResponse.ts +40 -17
  98. package/src/interaction/AttributeDataEncoder.ts +1 -1
  99. package/src/interaction/InteractionEndpointStructure.ts +62 -12
  100. package/src/interaction/InteractionMessenger.ts +1 -1
  101. package/src/interaction/ServerSubscription.ts +5 -5
  102. package/src/interaction/SubscriptionClient.ts +2 -2
  103. package/src/interaction/index.ts +0 -1
  104. package/src/protocol/ExchangeManager.ts +36 -2
  105. package/src/protocol/MessageExchange.ts +6 -3
  106. package/src/protocol/ProtocolHandler.ts +1 -0
  107. package/src/securechannel/SecureChannelProtocol.ts +1 -0
  108. package/src/session/case/CaseServer.ts +1 -0
  109. package/src/session/pase/PaseServer.ts +1 -0
  110. package/dist/cjs/interaction/InteractionServer.d.ts +0 -132
  111. package/dist/cjs/interaction/InteractionServer.d.ts.map +0 -1
  112. package/dist/cjs/interaction/InteractionServer.js +0 -1166
  113. package/dist/cjs/interaction/InteractionServer.js.map +0 -6
  114. package/dist/esm/interaction/InteractionServer.d.ts +0 -132
  115. package/dist/esm/interaction/InteractionServer.d.ts.map +0 -1
  116. package/dist/esm/interaction/InteractionServer.js +0 -1177
  117. package/dist/esm/interaction/InteractionServer.js.map +0 -6
  118. package/src/interaction/InteractionServer.ts +0 -1649
@@ -1,1649 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2022-2025 Matter.js Authors
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
-
7
- import {
8
- Crypto,
9
- Diagnostic,
10
- ImplementationError,
11
- InternalError,
12
- Logger,
13
- MatterError,
14
- MatterFlowError,
15
- Observable,
16
- ServerAddressIp,
17
- } from "#general";
18
- import { AttributeModel, ClusterModel, CommandModel, GLOBAL_IDS, MatterModel, Specification } from "#model";
19
- import { PeerAddress } from "#peer/PeerAddress.js";
20
- import { SessionManager } from "#session/SessionManager.js";
21
- import {
22
- ArraySchema,
23
- AttributeId,
24
- ClusterId,
25
- CommandId,
26
- DEFAULT_MAX_PATHS_PER_INVOKE,
27
- EndpointNumber,
28
- EventId,
29
- EventNumber,
30
- INTERACTION_PROTOCOL_ID,
31
- NodeId,
32
- ReceivedStatusResponseError,
33
- StatusCode,
34
- StatusResponseError,
35
- TlvAny,
36
- TlvAttributePath,
37
- TlvClusterPath,
38
- TlvCommandPath,
39
- TlvEventFilter,
40
- TlvEventPath,
41
- TlvInvokeResponseData,
42
- TlvInvokeResponseForSend,
43
- TlvNoArguments,
44
- TlvNoResponse,
45
- TlvSubscribeResponse,
46
- TypeFromSchema,
47
- ValidationError,
48
- } from "#types";
49
- import { AnyAttributeServer, AttributeServer, FabricScopedAttributeServer } from "../cluster/server/AttributeServer.js";
50
- import { CommandServer } from "../cluster/server/CommandServer.js";
51
- import { AnyEventServer } from "../cluster/server/EventServer.js";
52
- import { Message, SessionType } from "../codec/MessageCodec.js";
53
- import { EndpointInterface } from "../endpoint/EndpointInterface.js";
54
- import { MessageExchange } from "../protocol/MessageExchange.js";
55
- import { ProtocolHandler } from "../protocol/ProtocolHandler.js";
56
- import { assertSecureSession, NoAssociatedFabricError, SecureSession } from "../session/SecureSession.js";
57
- import {
58
- decodeAttributeValueWithSchema,
59
- decodeListAttributeValueWithSchema,
60
- expandPathsInAttributeData,
61
- } from "./AttributeDataDecoder.js";
62
- import { DataReportPayloadIterator, EventReportPayload } from "./AttributeDataEncoder.js";
63
- import { InteractionEndpointStructure } from "./InteractionEndpointStructure.js";
64
- import {
65
- DataReport,
66
- InteractionRecipient,
67
- InteractionServerMessenger,
68
- InvokeRequest,
69
- MessageType,
70
- ReadRequest,
71
- SubscribeRequest,
72
- TimedRequest,
73
- WriteRequest,
74
- WriteResponse,
75
- } from "./InteractionMessenger.js";
76
- import { ServerSubscription, ServerSubscriptionContext } from "./ServerSubscription.js";
77
- import { ServerSubscriptionConfig } from "./SubscriptionOptions.js";
78
-
79
- const logger = Logger.get("InteractionServer");
80
-
81
- export interface PeerSubscription {
82
- subscriptionId: number;
83
- peerAddress: PeerAddress;
84
- minIntervalFloorSeconds: number;
85
- maxIntervalCeilingSeconds: number;
86
- attributeRequests?: TypeFromSchema<typeof TlvAttributePath>[];
87
- eventRequests?: TypeFromSchema<typeof TlvEventPath>[];
88
- isFabricFiltered: boolean;
89
- maxInterval: number;
90
- sendInterval: number;
91
- operationalAddress?: ServerAddressIp;
92
- }
93
-
94
- export interface CommandPath {
95
- nodeId?: NodeId;
96
- endpointId: EndpointNumber;
97
- clusterId: ClusterId;
98
- commandId: CommandId;
99
- }
100
-
101
- export interface AttributePath {
102
- nodeId?: NodeId;
103
- endpointId: EndpointNumber;
104
- clusterId: ClusterId;
105
- attributeId: AttributeId;
106
- }
107
-
108
- export interface EventPath {
109
- nodeId?: NodeId;
110
- endpointId: EndpointNumber;
111
- clusterId: ClusterId;
112
- eventId: EventId;
113
- isUrgent?: boolean;
114
- }
115
-
116
- export interface AttributeWithPath {
117
- path: AttributePath;
118
- attribute: AnyAttributeServer<any>;
119
- }
120
-
121
- export interface EventWithPath {
122
- path: EventPath;
123
- event: AnyEventServer<any, any>;
124
- }
125
-
126
- export interface CommandWithPath {
127
- path: CommandPath;
128
- command: CommandServer<any, any>;
129
- }
130
-
131
- export function genericElementPathToId(
132
- endpointId: EndpointNumber | undefined,
133
- clusterId: ClusterId | undefined,
134
- elementId: number | undefined,
135
- ) {
136
- return `${endpointId}/${clusterId}/${elementId}`;
137
- }
138
-
139
- export function commandPathToId({ endpointId, clusterId, commandId }: CommandPath) {
140
- return genericElementPathToId(endpointId, clusterId, commandId);
141
- }
142
-
143
- export function attributePathToId({ endpointId, clusterId, attributeId }: TypeFromSchema<typeof TlvAttributePath>) {
144
- return genericElementPathToId(endpointId, clusterId, attributeId);
145
- }
146
-
147
- export function eventPathToId({ endpointId, clusterId, eventId }: TypeFromSchema<typeof TlvEventPath>) {
148
- return genericElementPathToId(endpointId, clusterId, eventId);
149
- }
150
-
151
- export function clusterPathToId({ nodeId, endpointId, clusterId }: TypeFromSchema<typeof TlvClusterPath>) {
152
- return `${nodeId}/${endpointId}/${clusterId}`;
153
- }
154
-
155
- function isConcreteAttributePath(
156
- path: TypeFromSchema<typeof TlvAttributePath>,
157
- ): path is TypeFromSchema<typeof TlvAttributePath> & AttributePath {
158
- const { endpointId, clusterId, attributeId } = path;
159
- return endpointId !== undefined && clusterId !== undefined && attributeId !== undefined;
160
- }
161
-
162
- export function validateReadAttributesPath(path: TypeFromSchema<typeof TlvAttributePath>, isGroupSession = false) {
163
- if (isGroupSession) {
164
- throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
165
- }
166
- const { clusterId, attributeId } = path;
167
- if (clusterId === undefined && attributeId !== undefined) {
168
- if (!GLOBAL_IDS.has(attributeId)) {
169
- throw new StatusResponseError(
170
- `Illegal read request for wildcard cluster and non global attribute ${attributeId}`,
171
- StatusCode.InvalidAction,
172
- );
173
- }
174
- }
175
- }
176
-
177
- function validateWriteAttributesPath(path: TypeFromSchema<typeof TlvAttributePath>, isGroupSession = false) {
178
- const { endpointId, clusterId, attributeId } = path;
179
- if (clusterId === undefined || attributeId === undefined) {
180
- throw new StatusResponseError(
181
- "Illegal write request with wildcard cluster or attribute ID",
182
- StatusCode.InvalidAction,
183
- );
184
- }
185
- if (isGroupSession && endpointId !== undefined) {
186
- throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
187
- }
188
- }
189
-
190
- function isConcreteEventPath(
191
- path: TypeFromSchema<typeof TlvEventPath>,
192
- ): path is TypeFromSchema<typeof TlvEventPath> & EventPath {
193
- const { endpointId, clusterId, eventId } = path;
194
- return endpointId !== undefined && clusterId !== undefined && eventId !== undefined;
195
- }
196
-
197
- export function validateReadEventPath(path: TypeFromSchema<typeof TlvEventPath>, isGroupSession = false) {
198
- const { clusterId, eventId } = path;
199
- if (clusterId === undefined && eventId !== undefined) {
200
- throw new StatusResponseError("Illegal read request with wildcard cluster ID", StatusCode.InvalidAction);
201
- }
202
- if (isGroupSession) {
203
- throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
204
- }
205
- }
206
-
207
- function isConcreteCommandPath(
208
- path: TypeFromSchema<typeof TlvCommandPath>,
209
- ): path is TypeFromSchema<typeof TlvCommandPath> & CommandPath {
210
- const { endpointId, clusterId, commandId } = path;
211
- return endpointId !== undefined && clusterId !== undefined && commandId !== undefined;
212
- }
213
-
214
- function validateCommandPath(path: TypeFromSchema<typeof TlvCommandPath>, isGroupSession = false) {
215
- const { endpointId, clusterId, commandId } = path;
216
- if (clusterId === undefined || commandId === undefined) {
217
- throw new StatusResponseError(
218
- "Illegal write request with wildcard cluster or attribute ID",
219
- StatusCode.InvalidAction,
220
- );
221
- }
222
- if (isGroupSession && endpointId !== undefined) {
223
- throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
224
- }
225
- }
226
-
227
- function getMatterModelCluster(clusterId: ClusterId) {
228
- return MatterModel.standard.get(ClusterModel, clusterId);
229
- }
230
-
231
- function getMatterModelClusterAttribute(clusterId: ClusterId, attributeId: AttributeId) {
232
- return getMatterModelCluster(clusterId)?.get(AttributeModel, attributeId);
233
- }
234
-
235
- function getMatterModelClusterCommand(clusterId: ClusterId, commandId: CommandId) {
236
- return getMatterModelCluster(clusterId)?.get(CommandModel, commandId);
237
- }
238
-
239
- /**
240
- * Interfaces {@link InteractionServer} with other components.
241
- */
242
- export interface InteractionContext {
243
- readonly sessions: SessionManager;
244
- readonly structure: InteractionEndpointStructure;
245
- readonly subscriptionOptions?: Partial<ServerSubscriptionConfig>;
246
- readonly maxPathsPerInvoke?: number;
247
- initiateExchange(address: PeerAddress, protocolId: number): MessageExchange;
248
- }
249
-
250
- /**
251
- * Translates interactions from the Matter protocol to matter.js APIs.
252
- */
253
- export class InteractionServer implements ProtocolHandler, InteractionRecipient {
254
- readonly id = INTERACTION_PROTOCOL_ID;
255
- #context: InteractionContext;
256
- #nextSubscriptionId = Crypto.getRandomUInt32();
257
- #isClosing = false;
258
- #clientHandler?: ProtocolHandler;
259
- readonly #subscriptionConfig: ServerSubscriptionConfig;
260
- readonly #maxPathsPerInvoke;
261
- readonly #subscriptionEstablishmentStarted = Observable<[peerAddress: PeerAddress]>();
262
-
263
- constructor(context: InteractionContext) {
264
- this.#context = context;
265
-
266
- this.#subscriptionConfig = ServerSubscriptionConfig.of(context.subscriptionOptions);
267
- this.#maxPathsPerInvoke = context.maxPathsPerInvoke ?? DEFAULT_MAX_PATHS_PER_INVOKE;
268
-
269
- this.#context.structure.change.on(async () => {
270
- this.#context.sessions.updateAllSubscriptions();
271
- });
272
- }
273
-
274
- protected get isClosing() {
275
- return this.#isClosing;
276
- }
277
-
278
- get maxPathsPerInvoke() {
279
- return this.#maxPathsPerInvoke;
280
- }
281
-
282
- get subscriptionEstablishmentStarted() {
283
- return this.#subscriptionEstablishmentStarted;
284
- }
285
-
286
- async onNewExchange(exchange: MessageExchange, message: Message) {
287
- // Note - changes here must be copied to TransactionalInteractionServer as it does not call super() to avoid
288
- // the stack frame
289
- if (this.#isClosing) return; // We are closing, ignore anything newly incoming
290
-
291
- // An incoming data report as the first message is not a valid server operation. We instead delegate to a
292
- // client implementation if available
293
- if (message.payloadHeader.messageType === MessageType.SubscribeRequest && this.#clientHandler) {
294
- return this.#clientHandler.onNewExchange(exchange, message);
295
- }
296
-
297
- await new InteractionServerMessenger(exchange).handleRequest(this);
298
- }
299
-
300
- get clientHandler(): ProtocolHandler | undefined {
301
- return this.#clientHandler;
302
- }
303
-
304
- set clientHandler(clientHandler: ProtocolHandler) {
305
- this.#clientHandler = clientHandler;
306
- }
307
-
308
- async #collectEventDataForRead(
309
- { eventRequests, eventFilters, isFabricFiltered }: ReadRequest,
310
- exchange: MessageExchange,
311
- message: Message,
312
- ) {
313
- let eventReportsPayload: undefined | EventReportPayload[];
314
- if (eventRequests) {
315
- eventReportsPayload = [];
316
- for (const requestPath of eventRequests) {
317
- validateReadEventPath(requestPath);
318
-
319
- const events = this.#endpointStructure.getEvents([requestPath]);
320
-
321
- // Requested event path not found in any cluster server on any endpoint
322
- if (events.length === 0) {
323
- if (isConcreteEventPath(requestPath)) {
324
- const { endpointId, clusterId, eventId } = requestPath;
325
- try {
326
- this.#endpointStructure.validateConcreteEventPath(endpointId, clusterId, eventId);
327
- throw new InternalError(
328
- "validateConcreteEventPath should throw StatusResponseError but did not.",
329
- );
330
- } catch (e) {
331
- StatusResponseError.accept(e);
332
-
333
- logger.debug(
334
- `Read event from ${
335
- exchange.channel.name
336
- }: ${this.#endpointStructure.resolveEventName(requestPath)}: unsupported path: Status=${
337
- e.code
338
- }`,
339
- );
340
- eventReportsPayload?.push({
341
- hasFabricSensitiveData: false,
342
- eventStatus: { path: requestPath, status: { status: e.code } },
343
- });
344
- }
345
- }
346
- // Wildcard path: Just leave out values
347
- logger.debug(
348
- `Read event from ${exchange.channel.name}: ${this.#endpointStructure.resolveEventName(
349
- requestPath,
350
- )}: ignore non-existing event`,
351
- );
352
- continue;
353
- }
354
-
355
- const reportsForPath = new Array<EventReportPayload>();
356
- for (const { path, event } of events) {
357
- try {
358
- const matchingEvents = await this.readEvent(
359
- path,
360
- eventFilters,
361
- event,
362
- exchange,
363
- isFabricFiltered,
364
- message,
365
- );
366
- logger.debug(
367
- `Read event from ${exchange.channel.name}: ${this.#endpointStructure.resolveEventName(
368
- path,
369
- )}=${Diagnostic.json(matchingEvents)}`,
370
- );
371
- const { schema } = event;
372
- reportsForPath.push(
373
- ...matchingEvents.map(({ number, priority, epochTimestamp, payload }) => ({
374
- hasFabricSensitiveData: event.hasFabricSensitiveData,
375
- eventData: {
376
- path,
377
- eventNumber: number,
378
- priority,
379
- epochTimestamp,
380
- payload,
381
- schema,
382
- },
383
- })),
384
- );
385
- } catch (error) {
386
- logger.error(
387
- `Error while reading event from ${
388
- exchange.channel.name
389
- } to ${this.#endpointStructure.resolveEventName(path)}:`,
390
- error,
391
- );
392
-
393
- StatusResponseError.accept(error);
394
-
395
- // Add StatusResponseErrors, but only when the initial path was concrete, else error are ignored
396
- if (isConcreteEventPath(requestPath)) {
397
- eventReportsPayload?.push({
398
- hasFabricSensitiveData: false,
399
- eventStatus: { path, status: { status: error.code } },
400
- });
401
- }
402
- }
403
- }
404
- eventReportsPayload.push(
405
- ...reportsForPath.sort((a, b) => {
406
- const eventNumberA = a.eventData?.eventNumber ?? EventNumber(0);
407
- const eventNumberB = b.eventData?.eventNumber ?? EventNumber(0);
408
- if (eventNumberA > eventNumberB) {
409
- return 1;
410
- } else if (eventNumberA < eventNumberB) {
411
- return -1;
412
- } else {
413
- return 0;
414
- }
415
- }),
416
- );
417
- }
418
- }
419
- return eventReportsPayload;
420
- }
421
-
422
- /**
423
- * Returns an iterator that yields the data reports and events data for the given read request.
424
- */
425
- *#iterateReadAttributesPaths(
426
- { attributeRequests, dataVersionFilters, isFabricFiltered }: ReadRequest,
427
- eventReportsPayload: EventReportPayload[] | undefined,
428
- exchange: MessageExchange,
429
- message: Message,
430
- ) {
431
- const dataVersionFilterMap = new Map<string, number>(
432
- dataVersionFilters?.map(({ path, dataVersion }) => [clusterPathToId(path), dataVersion]) ?? [],
433
- );
434
- if (dataVersionFilterMap.size > 0) {
435
- logger.debug(
436
- `DataVersionFilters: ${Array.from(dataVersionFilterMap.entries())
437
- .map(([path, version]) => `${path}=${version}`)
438
- .join(", ")}`,
439
- );
440
- }
441
-
442
- for (const requestPath of attributeRequests ?? []) {
443
- validateReadAttributesPath(requestPath);
444
-
445
- const attributes = this.#endpointStructure.getAttributes([requestPath]);
446
-
447
- // Requested attribute path not found in any cluster server on any endpoint
448
- if (attributes.length === 0) {
449
- // TODO Add checks for nodeId -> UnknownNode
450
- if (isConcreteAttributePath(requestPath)) {
451
- const { endpointId, clusterId, attributeId } = requestPath;
452
- // Concrete path, but still unknown for us, so generate the right error status
453
- try {
454
- this.#endpointStructure.validateConcreteAttributePath(endpointId, clusterId, attributeId);
455
- throw new InternalError(
456
- "validateConcreteAttributePath should throw StatusResponseError but did not.",
457
- );
458
- } catch (e) {
459
- StatusResponseError.accept(e);
460
- logger.debug(
461
- `Error reading attribute from ${
462
- exchange.channel.name
463
- }: ${this.#endpointStructure.resolveAttributeName(requestPath)}: unsupported path: Status=${
464
- e.code
465
- }`,
466
- );
467
- yield {
468
- hasFabricSensitiveData: false,
469
- attributeStatus: { path: requestPath, status: { status: e.code } },
470
- };
471
- }
472
- }
473
-
474
- // Wildcard path and we do not know any of the attributes: Ignore the error
475
- logger.debug(
476
- `Read from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
477
- requestPath,
478
- )}: ${this.#endpointStructure.resolveAttributeName(requestPath)}: ignore non-existing attribute`,
479
- );
480
- continue;
481
- }
482
-
483
- // Process all known attributes for the given path
484
- for (const { path, attribute } of attributes) {
485
- const { nodeId, endpointId, clusterId } = path;
486
-
487
- try {
488
- // We accept attributes not in the model ans readable, because existence is checked already
489
- if (getMatterModelClusterAttribute(clusterId, attribute.id)?.readable === false) {
490
- throw new StatusResponseError(
491
- `Attribute ${attribute.id} is not readable.`,
492
- StatusCode.UnsupportedRead,
493
- );
494
- }
495
-
496
- let value, version;
497
- try {
498
- // TODO Optimize read later here too to optimize context usage
499
- ({ value, version } = this.readAttribute(path, attribute, exchange, isFabricFiltered, message));
500
- } catch (e) {
501
- NoAssociatedFabricError.accept(e);
502
-
503
- // TODO: Remove when we remove legacy API
504
- // This is not fully correct but should be sufficient for now
505
- // This is fixed in the new API already, so this error should never throw
506
- // Fabric scoped attributes are access errors, fabric sensitive attributes are just filtered
507
- // Assume for now that in this place we only need to handle fabric sensitive case
508
- if (endpointId === undefined || clusterId === undefined) {
509
- throw new MatterFlowError("Should never happen");
510
- }
511
- const cluster = this.#endpointStructure.getClusterServer(endpointId, clusterId);
512
- if (cluster === undefined || cluster.datasource == undefined) {
513
- throw new MatterFlowError("Should never happen");
514
- }
515
- version = cluster.datasource.version;
516
- value = [];
517
- }
518
-
519
- const versionFilterValue =
520
- endpointId !== undefined && clusterId !== undefined
521
- ? dataVersionFilterMap.get(clusterPathToId({ nodeId, endpointId, clusterId }))
522
- : undefined;
523
- if (versionFilterValue !== undefined && versionFilterValue === version) {
524
- logger.debug(
525
- `Read attribute from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
526
- path,
527
- )}=${Diagnostic.json(value)} (version=${version}) ignored because of dataVersionFilter`,
528
- );
529
- continue;
530
- }
531
-
532
- logger.debug(
533
- `Read attribute from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
534
- path,
535
- )}=${Diagnostic.json(value)} (version=${version})`,
536
- );
537
-
538
- const { schema } = attribute;
539
- yield {
540
- hasFabricSensitiveData: attribute.hasFabricSensitiveData,
541
- attributeData: { path, dataVersion: version, payload: value, schema },
542
- };
543
- } catch (error) {
544
- const what = `reading ${this.#endpointStructure.resolveAttributeName(path)} from ${exchange.channel.name}`;
545
-
546
- if (!(error instanceof StatusResponseError)) {
547
- const wrappedError = new ImplementationError(`Unhandled error ${what}`);
548
- wrappedError.cause = error;
549
- throw wrappedError;
550
- }
551
-
552
- logger.error(`Error ${what}:`, error.message);
553
-
554
- // Add StatusResponseErrors, but only when the initial path was concrete, else error are ignored
555
- if (isConcreteAttributePath(requestPath)) {
556
- yield {
557
- hasFabricSensitiveData: false,
558
- attributeStatus: { path, status: { status: error.code } },
559
- };
560
- }
561
- }
562
- }
563
- }
564
-
565
- if (eventReportsPayload !== undefined) {
566
- for (const eventReport of eventReportsPayload) {
567
- yield eventReport;
568
- }
569
- }
570
- }
571
-
572
- async handleReadRequest(
573
- exchange: MessageExchange,
574
- readRequest: ReadRequest,
575
- message: Message,
576
- ): Promise<{ dataReport: DataReport; payload?: DataReportPayloadIterator }> {
577
- const { attributeRequests, eventRequests, isFabricFiltered, interactionModelRevision } = readRequest;
578
- logger.debug(
579
- `Received read request from ${exchange.channel.name}: attributes:${
580
- attributeRequests?.map(path => this.#endpointStructure.resolveAttributeName(path)).join(", ") ?? "none"
581
- }, events:${
582
- eventRequests?.map(path => this.#endpointStructure.resolveEventName(path)).join(", ") ?? "none"
583
- } isFabricFiltered=${isFabricFiltered}`,
584
- );
585
-
586
- if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
587
- logger.debug(
588
- `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
589
- );
590
- }
591
- if (attributeRequests === undefined && eventRequests === undefined) {
592
- return {
593
- dataReport: {
594
- interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
595
- suppressResponse: true,
596
- },
597
- };
598
- }
599
-
600
- if (message.packetHeader.sessionType !== SessionType.Unicast) {
601
- throw new StatusResponseError(
602
- "Subscriptions are only allowed on unicast sessions",
603
- StatusCode.InvalidAction,
604
- );
605
- }
606
-
607
- return {
608
- dataReport: {
609
- interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
610
- suppressResponse: true,
611
- },
612
- payload: this.#iterateReadAttributesPaths(
613
- readRequest,
614
- await this.#collectEventDataForRead(readRequest, exchange, message),
615
- exchange,
616
- message,
617
- ),
618
- };
619
- }
620
-
621
- protected readAttribute(
622
- _path: AttributePath,
623
- attribute: AnyAttributeServer<any>,
624
- exchange: MessageExchange,
625
- isFabricFiltered: boolean,
626
- message: Message,
627
- offline = false,
628
- ) {
629
- return attribute.getWithVersion(exchange.session, isFabricFiltered, offline ? undefined : message);
630
- }
631
-
632
- /**
633
- * Reads the attributes for the given endpoint.
634
- * This can currently only be used for subscriptions because errors are ignored!
635
- */
636
- protected readEndpointAttributesForSubscription(
637
- attributes: { path: AttributePath; attribute: AnyAttributeServer<any> }[],
638
- exchange: MessageExchange,
639
- isFabricFiltered: boolean,
640
- message: Message,
641
- offline = false,
642
- ) {
643
- const result = new Array<{
644
- path: AttributePath;
645
- attribute: AnyAttributeServer<unknown>;
646
- value: any;
647
- version: number;
648
- }>();
649
- for (const { path, attribute } of attributes) {
650
- try {
651
- const { version, value } = this.readAttribute(
652
- path,
653
- attribute,
654
- exchange,
655
- isFabricFiltered,
656
- message,
657
- offline,
658
- );
659
- result.push({ path, value, version, attribute });
660
- } catch (error) {
661
- if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
662
- logger.warn(
663
- `Permission denied reading attribute ${this.#endpointStructure.resolveAttributeName(path)}`,
664
- );
665
- } else {
666
- logger.warn(
667
- `Error reading attribute ${this.#endpointStructure.resolveAttributeName(path)}:`,
668
- error,
669
- );
670
- }
671
- }
672
- }
673
- return result;
674
- }
675
-
676
- protected async readEvent(
677
- _path: EventPath,
678
- eventFilters: TypeFromSchema<typeof TlvEventFilter>[] | undefined,
679
- event: AnyEventServer<any, any>,
680
- exchange: MessageExchange,
681
- isFabricFiltered: boolean,
682
- message: Message,
683
- ) {
684
- return event.get(exchange.session, isFabricFiltered, message, eventFilters);
685
- }
686
-
687
- async handleWriteRequest(
688
- exchange: MessageExchange,
689
- { suppressResponse, timedRequest, writeRequests, interactionModelRevision, moreChunkedMessages }: WriteRequest,
690
- message: Message,
691
- ): Promise<WriteResponse> {
692
- const sessionType = message.packetHeader.sessionType;
693
- logger.debug(
694
- `Received write request from ${exchange.channel.name}: ${writeRequests
695
- .map(req => this.#endpointStructure.resolveAttributeName(req.path))
696
- .join(", ")}, suppressResponse=${suppressResponse}, moreChunkedMessages=${moreChunkedMessages}`,
697
- );
698
-
699
- if (moreChunkedMessages && suppressResponse) {
700
- throw new StatusResponseError(
701
- "MoreChunkedMessages and SuppressResponse cannot be used together in write messages",
702
- StatusCode.InvalidAction,
703
- );
704
- }
705
-
706
- if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
707
- logger.debug(
708
- `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
709
- );
710
- }
711
-
712
- const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
713
-
714
- if (receivedWithinTimedInteraction && moreChunkedMessages) {
715
- throw new StatusResponseError(
716
- "Write Request action that is part of a Timed Write Interaction SHALL NOT be chunked.",
717
- StatusCode.InvalidAction,
718
- );
719
- }
720
-
721
- if (exchange.hasExpiredTimedInteraction()) {
722
- exchange.clearTimedInteraction(); // ??
723
- throw new StatusResponseError(`Timed request window expired. Decline write request.`, StatusCode.Timeout);
724
- }
725
-
726
- if (timedRequest !== exchange.hasTimedInteraction()) {
727
- throw new StatusResponseError(
728
- `timedRequest flag of write interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
729
- StatusCode.TimedRequestMismatch,
730
- );
731
- }
732
-
733
- if (receivedWithinTimedInteraction) {
734
- logger.debug(
735
- `Write request from ${exchange.channel.name} successfully received while timed interaction is running.`,
736
- );
737
- exchange.clearTimedInteraction();
738
- if (sessionType !== SessionType.Unicast) {
739
- throw new StatusResponseError(
740
- "Write requests are only allowed on unicast sessions when a timed interaction is running.",
741
- StatusCode.InvalidAction,
742
- );
743
- }
744
- }
745
-
746
- if (sessionType === SessionType.Group && !suppressResponse) {
747
- throw new StatusResponseError(
748
- "Write requests are only allowed as group casts when suppressResponse=true.",
749
- StatusCode.InvalidAction,
750
- );
751
- }
752
-
753
- const writeData = expandPathsInAttributeData(writeRequests, true);
754
-
755
- const writeResults = new Array<{
756
- path: TypeFromSchema<typeof TlvAttributePath>;
757
- statusCode: StatusCode;
758
- clusterStatusCode?: number;
759
- }>();
760
- const attributeListWrites = new Set<AttributeServer<any>>();
761
- const clusterDataVersionInfo = new Map<string, number>();
762
- const inaccessiblePaths = new Set<string>();
763
-
764
- // TODO Add handling for moreChunkedMessages here when adopting for Matter 1.4
765
-
766
- for (const writeRequest of writeData) {
767
- const { path: writePath, dataVersion } = writeRequest;
768
-
769
- validateWriteAttributesPath(writePath);
770
-
771
- const attributes = this.#endpointStructure.getAttributes([writePath], true);
772
-
773
- // No existing attribute matches the given path and is writable
774
- if (attributes.length === 0) {
775
- if (isConcreteAttributePath(writePath)) {
776
- const { endpointId, clusterId, attributeId } = writePath;
777
-
778
- // was a concrete path
779
- try {
780
- this.#endpointStructure.validateConcreteAttributePath(endpointId, clusterId, attributeId);
781
-
782
- // Ok it is a valid concrete path, so it is not writable
783
- throw new StatusResponseError(
784
- `Attribute ${attributeId} is not writable.`,
785
- StatusCode.UnsupportedWrite,
786
- );
787
- } catch (e) {
788
- StatusResponseError.accept(e);
789
-
790
- logger.debug(
791
- `Write from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
792
- writePath,
793
- )} not allowed: Status=${e.code}`,
794
- );
795
- writeResults.push({ path: writePath, statusCode: e.code });
796
- }
797
- } else {
798
- // Wildcard path: Just ignore
799
- logger.debug(
800
- `Write from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
801
- writePath,
802
- )}: ignore non-existing (wildcard) attribute`,
803
- );
804
- }
805
- continue;
806
- }
807
-
808
- // Concrete path and found and writable
809
- if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
810
- const { endpointId, clusterId } = writePath;
811
- const { attribute } = attributes[0];
812
-
813
- if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
814
- logger.debug(`This write requires a timed interaction which is not initialized.`);
815
- writeResults.push({ path: writePath, statusCode: StatusCode.NeedsTimedInteraction });
816
- continue;
817
- }
818
-
819
- if (
820
- attribute instanceof FabricScopedAttributeServer &&
821
- (!exchange.session.isSecure || !(exchange.session as SecureSession).fabric)
822
- ) {
823
- logger.debug(`This write requires a secure session with a fabric assigned which is missing.`);
824
- writeResults.push({ path: writePath, statusCode: StatusCode.UnsupportedAccess });
825
- continue;
826
- }
827
-
828
- // Check the provided dataVersion with the dataVersion of the cluster
829
- // And remember this initial dataVersion for all checks inside this write transaction to allow proper
830
- // processing of chunked lists
831
- if (dataVersion !== undefined) {
832
- const datasource = this.#endpointStructure.getClusterServer(endpointId, clusterId)?.datasource;
833
- const { nodeId } = writePath;
834
- const clusterKey = clusterPathToId({ nodeId, endpointId, clusterId });
835
- const currentDataVersion = clusterDataVersionInfo.get(clusterKey) ?? datasource?.version;
836
-
837
- if (currentDataVersion !== undefined) {
838
- if (dataVersion !== currentDataVersion) {
839
- logger.debug(
840
- `This write requires a specific data version (${dataVersion}) which do not match the current cluster data version (${currentDataVersion}).`,
841
- );
842
- writeResults.push({ path: writePath, statusCode: StatusCode.DataVersionMismatch });
843
- continue;
844
- }
845
- clusterDataVersionInfo.set(clusterKey, currentDataVersion);
846
- }
847
- }
848
- }
849
-
850
- for (const { path, attribute } of attributes) {
851
- const { schema, defaultValue } = attribute;
852
- const pathId = attributePathToId(path);
853
-
854
- try {
855
- if (
856
- !(attribute instanceof AttributeServer) &&
857
- !(attribute instanceof FabricScopedAttributeServer)
858
- ) {
859
- throw new StatusResponseError(
860
- "Fixed attributes cannot be written",
861
- StatusCode.UnsupportedWrite,
862
- );
863
- }
864
-
865
- if (inaccessiblePaths.has(pathId)) {
866
- logger.debug(`This write is not allowed due to previous access denied.`);
867
- continue;
868
- }
869
-
870
- const { endpointId } = path;
871
- const { listIndex } = writePath;
872
- const value =
873
- listIndex === undefined
874
- ? decodeAttributeValueWithSchema(schema, [writeRequest], defaultValue)
875
- : decodeListAttributeValueWithSchema(
876
- schema as ArraySchema<any>,
877
- [writeRequest],
878
- this.readAttribute(path, attribute, exchange, true, message).value ?? defaultValue,
879
- );
880
- logger.debug(
881
- `Handle write request from ${
882
- exchange.channel.name
883
- } resolved to: ${this.#endpointStructure.resolveAttributeName(path)}=${Diagnostic.json(
884
- value,
885
- )} (listIndex=${listIndex}, for-version=${dataVersion})`,
886
- );
887
-
888
- if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
889
- logger.debug(`This write requires a timed interaction which is not initialized.`);
890
- throw new StatusResponseError(
891
- "This write requires a timed interaction which is not initialized.",
892
- StatusCode.NeedsTimedInteraction,
893
- );
894
- }
895
-
896
- await this.writeAttribute(
897
- path,
898
- attribute,
899
- value,
900
- exchange,
901
- message,
902
- this.#endpointStructure.getEndpoint(endpointId)!,
903
- receivedWithinTimedInteraction,
904
- schema instanceof ArraySchema,
905
- );
906
- if (schema instanceof ArraySchema && !attributeListWrites.has(attribute)) {
907
- attributeListWrites.add(attribute);
908
- }
909
- } catch (error: any) {
910
- if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
911
- inaccessiblePaths.add(pathId);
912
- }
913
- if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
914
- // For Multi-Attribute-Writes we ignore errors
915
- logger.error(
916
- `Error while handling write request from ${
917
- exchange.channel.name
918
- } to ${this.#endpointStructure.resolveAttributeName(path)}:`,
919
- error instanceof StatusResponseError ? error.message : error,
920
- );
921
- if (error instanceof StatusResponseError) {
922
- writeResults.push({ path, statusCode: error.code, clusterStatusCode: error.clusterCode });
923
- continue;
924
- }
925
- writeResults.push({ path, statusCode: StatusCode.ConstraintError });
926
- continue;
927
- } else {
928
- logger.debug(
929
- `While handling write request from ${
930
- exchange.channel.name
931
- } to ${this.#endpointStructure.resolveAttributeName(path)} ignored: ${error.message}`,
932
- );
933
-
934
- // TODO - This behavior may be wrong.
935
- //
936
- // If a wildcard write fails we should either:
937
- //
938
- // 1. Ignore entirely (add nothing to write results), or
939
- // 2. Add an error response for the concrete attribute that failed.
940
- //
941
- // Spec is a little ambiguous. After request path expansion, in core 1.2 8.7.3.2 it states:
942
- //
943
- // "If the path indicates attribute data that is not writable, then the path SHALL be
944
- // discarded"
945
- //
946
- // So is this "not writable" -- so it should be #1 -- or is this "writable" but with invalid
947
- // data? The latter is error case #2 but spec doesn't make clear what the status code would
948
- // be... We could fall back to CONSTRAINT_ERROR like we do above though
949
- //
950
- // Currently what we do is add a success response for every concrete path that fails.
951
- }
952
- }
953
- writeResults.push({ path, statusCode: StatusCode.Success });
954
- }
955
- //.filter(({ statusCode }) => statusCode !== StatusCode.Success); // see https://github.com/project-chip/connectedhomeip/issues/26198
956
- }
957
-
958
- const errorResults = writeResults.filter(({ statusCode }) => statusCode !== StatusCode.Success);
959
- logger.debug(
960
- `Write request from ${exchange.channel.name} done ${
961
- errorResults.length
962
- ? `with following errors: ${errorResults
963
- .map(
964
- ({ path, statusCode }) =>
965
- `${this.#endpointStructure.resolveAttributeName(path)}=${Diagnostic.json(statusCode)}`,
966
- )
967
- .join(", ")}`
968
- : "without errors"
969
- }`,
970
- );
971
-
972
- const response = {
973
- interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
974
- writeResponses: writeResults.map(({ path, statusCode, clusterStatusCode }) => ({
975
- path,
976
- status: { status: statusCode, clusterStatus: clusterStatusCode },
977
- })),
978
- };
979
-
980
- // Trigger attribute events for delayed list writes
981
- for (const attribute of attributeListWrites.values()) {
982
- try {
983
- attribute.triggerDelayedChangeEvents();
984
- } catch (error) {
985
- logger.error(
986
- `Ignored Error while writing attribute from ${exchange.channel.name} to ${attribute.name}:`,
987
- error,
988
- );
989
- }
990
- }
991
-
992
- return response;
993
- }
994
-
995
- protected async writeAttribute(
996
- _path: AttributePath,
997
- attribute: AttributeServer<any>,
998
- value: any,
999
- exchange: MessageExchange,
1000
- message: Message,
1001
- _endpoint: EndpointInterface,
1002
- _receivedWithinTimedInteraction?: boolean,
1003
- isListWrite = false,
1004
- ) {
1005
- attribute.set(value, exchange.session, message, isListWrite);
1006
- }
1007
-
1008
- async handleSubscribeRequest(
1009
- exchange: MessageExchange,
1010
- request: SubscribeRequest,
1011
- messenger: InteractionServerMessenger,
1012
- message: Message,
1013
- ): Promise<void> {
1014
- const {
1015
- minIntervalFloorSeconds,
1016
- maxIntervalCeilingSeconds,
1017
- attributeRequests,
1018
- dataVersionFilters,
1019
- eventRequests,
1020
- eventFilters,
1021
- keepSubscriptions,
1022
- isFabricFiltered,
1023
- interactionModelRevision,
1024
- } = request;
1025
- logger.debug(
1026
- `Received subscribe request from ${exchange.channel.name} (keepSubscriptions=${keepSubscriptions}, isFabricFiltered=${isFabricFiltered})`,
1027
- );
1028
-
1029
- if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
1030
- logger.debug(
1031
- `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
1032
- );
1033
- }
1034
-
1035
- if (message.packetHeader.sessionType !== SessionType.Unicast) {
1036
- throw new StatusResponseError(
1037
- "Subscriptions are only allowed on unicast sessions",
1038
- StatusCode.InvalidAction,
1039
- );
1040
- }
1041
-
1042
- assertSecureSession(exchange.session, "Subscriptions are only implemented on secure sessions");
1043
- const session = exchange.session;
1044
- const fabric = session.fabric;
1045
- if (fabric === undefined)
1046
- throw new StatusResponseError(
1047
- "Subscriptions are only implemented after a fabric has been assigned",
1048
- StatusCode.InvalidAction,
1049
- );
1050
-
1051
- if (!keepSubscriptions) {
1052
- const clearedCount = await this.#context.sessions.clearSubscriptionsForNode(
1053
- fabric.addressOf(session.peerNodeId),
1054
- true,
1055
- );
1056
- if (clearedCount > 0) {
1057
- logger.debug(
1058
- `Cleared ${clearedCount} subscriptions for Subscriber node ${session.peerNodeId} because keepSubscriptions=false`,
1059
- );
1060
- }
1061
- }
1062
-
1063
- if (
1064
- (!Array.isArray(attributeRequests) || attributeRequests.length === 0) &&
1065
- (!Array.isArray(eventRequests) || eventRequests.length === 0)
1066
- ) {
1067
- throw new StatusResponseError("No attributes or events requested", StatusCode.InvalidAction);
1068
- }
1069
-
1070
- logger.debug(
1071
- `Subscribe to attributes:${
1072
- attributeRequests?.map(path => this.#endpointStructure.resolveAttributeName(path)).join(", ") ?? "none"
1073
- }, events:${
1074
- eventRequests?.map(path => this.#endpointStructure.resolveEventName(path)).join(", ") ?? "none"
1075
- }`,
1076
- );
1077
-
1078
- if (dataVersionFilters !== undefined && dataVersionFilters.length > 0) {
1079
- logger.debug(
1080
- `DataVersionFilters: ${dataVersionFilters
1081
- .map(
1082
- ({ path: { nodeId, endpointId, clusterId }, dataVersion }) =>
1083
- `${clusterPathToId({ nodeId, endpointId, clusterId })}=${dataVersion}`,
1084
- )
1085
- .join(", ")}`,
1086
- );
1087
- }
1088
- if (eventFilters !== undefined && eventFilters.length > 0)
1089
- logger.debug(
1090
- `Event filters: ${eventFilters.map(filter => `${filter.nodeId}/${filter.eventMin}`).join(", ")}`,
1091
- );
1092
-
1093
- // Validate of the paths before proceeding
1094
- attributeRequests?.forEach(path => validateReadAttributesPath(path));
1095
- eventRequests?.forEach(path => validateReadEventPath(path));
1096
-
1097
- if (minIntervalFloorSeconds < 0) {
1098
- throw new StatusResponseError(
1099
- "minIntervalFloorSeconds should be greater or equal to 0",
1100
- StatusCode.InvalidAction,
1101
- );
1102
- }
1103
- if (maxIntervalCeilingSeconds < 0) {
1104
- throw new StatusResponseError(
1105
- "maxIntervalCeilingSeconds should be greater or equal to 1",
1106
- StatusCode.InvalidAction,
1107
- );
1108
- }
1109
- if (maxIntervalCeilingSeconds < minIntervalFloorSeconds) {
1110
- throw new StatusResponseError(
1111
- "maxIntervalCeilingSeconds should be greater or equal to minIntervalFloorSeconds",
1112
- StatusCode.InvalidAction,
1113
- );
1114
- }
1115
-
1116
- if (this.#nextSubscriptionId === 0xffffffff) this.#nextSubscriptionId = 0;
1117
- const subscriptionId = this.#nextSubscriptionId++;
1118
-
1119
- this.#subscriptionEstablishmentStarted.emit(session.peerAddress);
1120
- let subscription: ServerSubscription;
1121
- try {
1122
- subscription = await this.#establishSubscription(
1123
- subscriptionId,
1124
- request,
1125
- messenger,
1126
- session,
1127
- exchange,
1128
- message,
1129
- );
1130
- } catch (error) {
1131
- logger.error(
1132
- `Subscription ${subscriptionId} for Session ${session.id}: Error while sending initial data reports`,
1133
- error instanceof MatterError ? error.message : error,
1134
- );
1135
- if (error instanceof StatusResponseError && !(error instanceof ReceivedStatusResponseError)) {
1136
- logger.info(`Sending status response ${error.code} for interaction error: ${error.message}`);
1137
- await messenger.sendStatus(error.code, {
1138
- logContext: {
1139
- for: "I/SubscriptionSeed-Status",
1140
- },
1141
- });
1142
- }
1143
- await messenger.close();
1144
- return; // Make sure to not bubble up the exception
1145
- }
1146
-
1147
- const maxInterval = subscription.maxInterval;
1148
- // Then send the subscription response
1149
- await messenger.send(
1150
- MessageType.SubscribeResponse,
1151
- TlvSubscribeResponse.encode({
1152
- subscriptionId,
1153
- maxInterval,
1154
- interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
1155
- }),
1156
- {
1157
- logContext: {
1158
- subId: subscriptionId,
1159
- maxInterval,
1160
- },
1161
- },
1162
- );
1163
-
1164
- // When an error occurs while sending the response, the subscription is not yet active and will be cleaned up by GC
1165
- subscription.activate();
1166
- }
1167
-
1168
- async #establishSubscription(
1169
- id: number,
1170
- {
1171
- minIntervalFloorSeconds,
1172
- maxIntervalCeilingSeconds,
1173
- attributeRequests,
1174
- dataVersionFilters,
1175
- eventRequests,
1176
- eventFilters,
1177
- isFabricFiltered,
1178
- }: SubscribeRequest,
1179
- messenger: InteractionServerMessenger,
1180
- session: SecureSession,
1181
- exchange: MessageExchange,
1182
- message: Message,
1183
- ) {
1184
- const context: ServerSubscriptionContext = {
1185
- session,
1186
- structure: this.#endpointStructure,
1187
-
1188
- readAttribute: (path, attribute, offline) =>
1189
- this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
1190
-
1191
- readEndpointAttributesForSubscription: attributes =>
1192
- this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
1193
-
1194
- readEvent: (path, event, eventFilters) =>
1195
- this.readEvent(path, eventFilters, event, exchange, isFabricFiltered, message),
1196
-
1197
- initiateExchange: (address: PeerAddress, protocolId) => this.#context.initiateExchange(address, protocolId),
1198
- };
1199
-
1200
- const subscription = new ServerSubscription({
1201
- id,
1202
- context,
1203
- criteria: {
1204
- attributeRequests,
1205
- dataVersionFilters,
1206
- eventRequests,
1207
- eventFilters,
1208
- isFabricFiltered,
1209
- },
1210
- minIntervalFloorSeconds,
1211
- maxIntervalCeilingSeconds,
1212
- subscriptionOptions: this.#subscriptionConfig,
1213
- });
1214
-
1215
- try {
1216
- // Send initial data report to prime the subscription with initial data
1217
- await subscription.sendInitialReport(messenger);
1218
- } catch (error) {
1219
- await subscription.close(); // Cleanup
1220
- throw error;
1221
- }
1222
-
1223
- logger.info(
1224
- `Successfully created subscription ${id} for Session ${
1225
- session.id
1226
- } to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`,
1227
- );
1228
- return subscription;
1229
- }
1230
-
1231
- async establishFormerSubscription(
1232
- {
1233
- subscriptionId,
1234
- attributeRequests,
1235
- eventRequests,
1236
- isFabricFiltered,
1237
- minIntervalFloorSeconds,
1238
- maxIntervalCeilingSeconds,
1239
- maxInterval,
1240
- sendInterval,
1241
- }: PeerSubscription,
1242
- session: SecureSession,
1243
- ) {
1244
- const exchange = this.#context.initiateExchange(session.peerAddress, INTERACTION_PROTOCOL_ID);
1245
- const message = {} as Message;
1246
- logger.debug(
1247
- `Send DataReports to re-establish subscription ${subscriptionId} to `,
1248
- Diagnostic.dict({ isFabricFiltered, maxInterval, sendInterval }),
1249
- );
1250
- const context: ServerSubscriptionContext = {
1251
- session,
1252
- structure: this.#endpointStructure,
1253
-
1254
- readAttribute: (path, attribute, offline) =>
1255
- this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
1256
-
1257
- readEndpointAttributesForSubscription: attributes =>
1258
- this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
1259
-
1260
- readEvent: (path, event, eventFilters) =>
1261
- this.readEvent(path, eventFilters, event, exchange, isFabricFiltered, message),
1262
-
1263
- initiateExchange: (address: PeerAddress, protocolId) => this.#context.initiateExchange(address, protocolId),
1264
- };
1265
-
1266
- const subscription = new ServerSubscription({
1267
- id: subscriptionId,
1268
- context,
1269
- minIntervalFloorSeconds,
1270
- maxIntervalCeilingSeconds,
1271
- criteria: {
1272
- attributeRequests,
1273
- eventRequests,
1274
- isFabricFiltered,
1275
- },
1276
- subscriptionOptions: this.#subscriptionConfig,
1277
- useAsMaxInterval: maxInterval,
1278
- useAsSendInterval: sendInterval,
1279
- });
1280
-
1281
- try {
1282
- // Send initial data report to prime the subscription with initial data
1283
- await subscription.sendInitialReport(new InteractionServerMessenger(exchange));
1284
- subscription.activate();
1285
- logger.info(
1286
- `Successfully re-established subscription ${subscriptionId} for Session ${
1287
- session.id
1288
- } to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`,
1289
- );
1290
- } catch (error) {
1291
- await subscription.close(); // Cleanup
1292
- throw error;
1293
- }
1294
- return subscription;
1295
- }
1296
-
1297
- async handleInvokeRequest(
1298
- exchange: MessageExchange,
1299
- { invokeRequests, timedRequest, suppressResponse, interactionModelRevision }: InvokeRequest,
1300
- messenger: InteractionServerMessenger,
1301
- message: Message,
1302
- ): Promise<void> {
1303
- logger.debug(
1304
- `Received invoke request from ${exchange.channel.name}${invokeRequests.length > 0 ? ` with ${invokeRequests.length} commands` : ""}: ${invokeRequests
1305
- .map(({ commandPath: { endpointId, clusterId, commandId } }) =>
1306
- this.#endpointStructure.resolveCommandName({ endpointId, clusterId, commandId }),
1307
- )
1308
- .join(", ")}, suppressResponse=${suppressResponse}`,
1309
- );
1310
-
1311
- if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
1312
- logger.debug(
1313
- `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
1314
- );
1315
- }
1316
-
1317
- const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
1318
- if (exchange.hasExpiredTimedInteraction()) {
1319
- exchange.clearTimedInteraction(); // ??
1320
- throw new StatusResponseError(`Timed request window expired. Decline invoke request.`, StatusCode.Timeout);
1321
- }
1322
-
1323
- if (timedRequest !== exchange.hasTimedInteraction()) {
1324
- throw new StatusResponseError(
1325
- `timedRequest flag of invoke interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
1326
- StatusCode.TimedRequestMismatch,
1327
- );
1328
- }
1329
-
1330
- if (receivedWithinTimedInteraction) {
1331
- logger.debug(`Invoke request from ${exchange.channel.name} received while timed interaction is running.`);
1332
- exchange.clearTimedInteraction();
1333
- if (message.packetHeader.sessionType !== SessionType.Unicast) {
1334
- throw new StatusResponseError(
1335
- "Invoke requests are only allowed on unicast sessions when a timed interaction is running.",
1336
- StatusCode.InvalidAction,
1337
- );
1338
- }
1339
- }
1340
-
1341
- if (invokeRequests.length > this.#maxPathsPerInvoke) {
1342
- throw new StatusResponseError(
1343
- `Only ${this.#maxPathsPerInvoke} invoke requests are supported in one message. This message contains ${invokeRequests.length}`,
1344
- StatusCode.InvalidAction,
1345
- );
1346
- }
1347
-
1348
- // Validate all commandPaths before proceeding to make sure not to have executed partial commands
1349
- invokeRequests.forEach(({ commandPath }) => validateCommandPath(commandPath));
1350
-
1351
- // Perform additional cross-command validation required for batch invoke
1352
- if (invokeRequests.length > 1) {
1353
- const pathsUsed = new Set<string>();
1354
- const commandRefsUsed = new Set<number>();
1355
- invokeRequests.forEach(({ commandPath, commandRef }) => {
1356
- if (!isConcreteCommandPath(commandPath)) {
1357
- throw new StatusResponseError("Illegal wildcard path in batch invoke", StatusCode.InvalidAction);
1358
- }
1359
-
1360
- const commandPathId = commandPathToId(commandPath);
1361
- if (pathsUsed.has(commandPathId)) {
1362
- throw new StatusResponseError(
1363
- `Duplicate command path (${commandPathId}) in batch invoke`,
1364
- StatusCode.InvalidAction,
1365
- );
1366
- }
1367
-
1368
- if (commandRef === undefined) {
1369
- throw new StatusResponseError(
1370
- `Command reference missing in batch invoke of ${commandPathId}`,
1371
- StatusCode.InvalidAction,
1372
- );
1373
- }
1374
-
1375
- if (commandRefsUsed.has(commandRef)) {
1376
- throw new StatusResponseError(
1377
- `Duplicate command reference ${commandRef} in invoke of ${commandPathId}`,
1378
- StatusCode.InvalidAction,
1379
- );
1380
- }
1381
-
1382
- pathsUsed.add(commandPathId);
1383
- commandRefsUsed.add(commandRef);
1384
- });
1385
- }
1386
-
1387
- const isGroupSession = message.packetHeader.sessionType === SessionType.Group;
1388
- const invokeResponseMessage: TypeFromSchema<typeof TlvInvokeResponseForSend> = {
1389
- suppressResponse: false, // Deprecated but must be present
1390
- interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
1391
- invokeResponses: [],
1392
- moreChunkedMessages: invokeRequests.length > 1, // Assume for now we have multiple responses when having multiple invokes
1393
- };
1394
- const emptyInvokeResponseBytes = TlvInvokeResponseForSend.encode(invokeResponseMessage);
1395
- let messageSize = emptyInvokeResponseBytes.length;
1396
- let invokeResultsProcessed = 0;
1397
-
1398
- // To lower potential latency when we would process all invoke messages and just send responses at the end we
1399
- // assemble response on the fly locally here and send when message becomes too big
1400
- const processResponseResult = async (
1401
- invokeResponse: TypeFromSchema<typeof TlvInvokeResponseData>,
1402
- ): Promise<void> => {
1403
- invokeResultsProcessed++;
1404
-
1405
- if (isGroupSession) {
1406
- // We send no responses at all for group sessions
1407
- return;
1408
- }
1409
- const encodedInvokeResponse = TlvInvokeResponseData.encodeTlv(invokeResponse);
1410
- const invokeResponseBytes = TlvAny.getEncodedByteLength(encodedInvokeResponse);
1411
-
1412
- if (
1413
- messageSize + invokeResponseBytes > exchange.maxPayloadSize ||
1414
- invokeResultsProcessed === invokeRequests.length
1415
- ) {
1416
- let lastMessageProcessed = false;
1417
- if (messageSize + invokeResponseBytes <= exchange.maxPayloadSize) {
1418
- // last invoke response and matches in the message
1419
- invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
1420
- lastMessageProcessed = true;
1421
- }
1422
- // Send the response when the message is full or when all responses are processed
1423
- if (invokeResponseMessage.invokeResponses.length > 0) {
1424
- if (invokeRequests.length > 1) {
1425
- logger.debug(
1426
- `Send ${lastMessageProcessed ? "final " : ""}invoke response for ${invokeResponseMessage.invokeResponses} commands`,
1427
- );
1428
- }
1429
- const moreChunkedMessages = lastMessageProcessed ? undefined : true;
1430
- await messenger.send(
1431
- MessageType.InvokeResponse,
1432
- TlvInvokeResponseForSend.encode({
1433
- ...invokeResponseMessage,
1434
- moreChunkedMessages,
1435
- }),
1436
- {
1437
- logContext: {
1438
- invokeMsgFlags: Diagnostic.asFlags({
1439
- suppressResponse,
1440
- moreChunkedMessages,
1441
- }),
1442
- },
1443
- },
1444
- );
1445
- invokeResponseMessage.invokeResponses = [];
1446
- messageSize = emptyInvokeResponseBytes.length;
1447
- }
1448
- if (!lastMessageProcessed) {
1449
- invokeResultsProcessed--; // Correct counter again because we recall the method
1450
- return processResponseResult(invokeResponse);
1451
- }
1452
- } else {
1453
- invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
1454
- messageSize += invokeResponseBytes;
1455
- }
1456
- };
1457
-
1458
- // We could do more fancy parallel command processing, but it makes no sense for now, so lets simply process
1459
- // invoked commands one by one sequentially
1460
- for (const { commandPath, commandFields, commandRef } of invokeRequests) {
1461
- const commands = this.#endpointStructure.getCommands([commandPath]);
1462
-
1463
- if (commands.length === 0) {
1464
- if (isConcreteCommandPath(commandPath)) {
1465
- const { endpointId, clusterId, commandId } = commandPath;
1466
-
1467
- let result;
1468
-
1469
- try {
1470
- this.#endpointStructure.validateConcreteCommandPath(endpointId, clusterId, commandId);
1471
- throw new InternalError(
1472
- "validateConcreteCommandPath should throw StatusResponseError but did not.",
1473
- );
1474
- } catch (e) {
1475
- StatusResponseError.accept(e);
1476
-
1477
- logger.debug(
1478
- `Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
1479
- commandPath,
1480
- )} unsupported path: Status=${e.code}`,
1481
- );
1482
- result = { status: { commandPath, status: { status: e.code }, commandRef } };
1483
- }
1484
-
1485
- await processResponseResult(result);
1486
- } else {
1487
- // Wildcard path: Just ignore
1488
- logger.debug(
1489
- `Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
1490
- commandPath,
1491
- )} ignore non-existing command`,
1492
- );
1493
- }
1494
- continue;
1495
- }
1496
-
1497
- const isConcretePath = isConcreteCommandPath(commandPath);
1498
- for (const { command, path } of commands) {
1499
- const { endpointId, clusterId, commandId } = path;
1500
- if (endpointId === undefined) {
1501
- // Should never happen
1502
- logger.error(
1503
- `Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
1504
- path,
1505
- )} invalid path because empty endpoint!`,
1506
- );
1507
- if (isConcretePath) {
1508
- await processResponseResult({
1509
- status: {
1510
- commandPath: path,
1511
- status: { status: StatusCode.UnsupportedEndpoint },
1512
- commandRef,
1513
- },
1514
- });
1515
- }
1516
- continue;
1517
- }
1518
- const endpoint = this.#endpointStructure.getEndpoint(endpointId);
1519
- if (endpoint === undefined) {
1520
- // Should never happen
1521
- logger.error(
1522
- `Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
1523
- path,
1524
- )} invalid path because endpoint not found!`,
1525
- );
1526
- if (isConcretePath) {
1527
- await processResponseResult({
1528
- status: {
1529
- commandPath: path,
1530
- status: { status: StatusCode.UnsupportedEndpoint },
1531
- commandRef,
1532
- },
1533
- });
1534
- }
1535
- continue;
1536
- }
1537
- if (command.requiresTimedInteraction && !receivedWithinTimedInteraction) {
1538
- logger.debug(`This invoke requires a timed interaction which is not initialized.`);
1539
- if (isConcretePath) {
1540
- await processResponseResult({
1541
- status: {
1542
- commandPath: path,
1543
- status: { status: StatusCode.NeedsTimedInteraction },
1544
- commandRef,
1545
- },
1546
- });
1547
- }
1548
- continue;
1549
- }
1550
- if (
1551
- getMatterModelClusterCommand(clusterId, commandId)?.fabricScoped &&
1552
- (!exchange.session.isSecure || !(exchange.session as SecureSession).fabric)
1553
- ) {
1554
- logger.debug(`This invoke requires a secure session with a fabric assigned which is missing.`);
1555
- if (isConcretePath) {
1556
- await processResponseResult({
1557
- status: { commandPath: path, status: { status: StatusCode.UnsupportedAccess }, commandRef },
1558
- });
1559
- }
1560
- continue;
1561
- }
1562
-
1563
- let result;
1564
- try {
1565
- result = await this.invokeCommand(
1566
- path,
1567
- command,
1568
- exchange,
1569
- commandFields ?? TlvNoArguments.encodeTlv(commandFields),
1570
- message,
1571
- endpoint,
1572
- receivedWithinTimedInteraction,
1573
- );
1574
- } catch (e) {
1575
- StatusResponseError.accept(e);
1576
-
1577
- let errorCode = e.code;
1578
- const errorLogText = `Error ${Diagnostic.hex(errorCode)}${
1579
- e.clusterCode !== undefined ? `/${Diagnostic.hex(e.clusterCode)}` : ""
1580
- } while invoking command: ${e.message}`;
1581
-
1582
- if (e instanceof ValidationError) {
1583
- logger.info(
1584
- `Validation-${errorLogText}${e.fieldName !== undefined ? ` in field ${e.fieldName}` : ""}`,
1585
- );
1586
- if (errorCode === StatusCode.InvalidAction) {
1587
- errorCode = StatusCode.InvalidCommand;
1588
- }
1589
- } else {
1590
- logger.info(errorLogText);
1591
- }
1592
-
1593
- result = {
1594
- code: errorCode,
1595
- clusterCode: e.clusterCode,
1596
- responseId: command.responseId,
1597
- response: TlvNoResponse.encodeTlv(),
1598
- };
1599
- }
1600
- const { code, clusterCode, responseId, response } = result;
1601
- if (response.length === 0) {
1602
- await processResponseResult({
1603
- status: { commandPath: path, status: { status: code, clusterStatus: clusterCode }, commandRef },
1604
- });
1605
- } else {
1606
- await processResponseResult({
1607
- command: {
1608
- commandPath: { ...path, commandId: responseId },
1609
- commandFields: response,
1610
- commandRef,
1611
- },
1612
- });
1613
- }
1614
- }
1615
- }
1616
- }
1617
-
1618
- protected async invokeCommand(
1619
- _path: CommandPath,
1620
- command: CommandServer<any, any>,
1621
- exchange: MessageExchange,
1622
- commandFields: any,
1623
- message: Message,
1624
- endpoint: EndpointInterface,
1625
- _receivedWithinTimedInteraction = false,
1626
- ) {
1627
- return command.invoke(exchange.session, commandFields, message, endpoint);
1628
- }
1629
-
1630
- handleTimedRequest(exchange: MessageExchange, { timeout, interactionModelRevision }: TimedRequest) {
1631
- logger.debug(`Received timed request (${timeout}ms) from ${exchange.channel.name}`);
1632
-
1633
- if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
1634
- logger.debug(
1635
- `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
1636
- );
1637
- }
1638
-
1639
- exchange.startTimedInteraction(timeout);
1640
- }
1641
-
1642
- async close() {
1643
- this.#isClosing = true;
1644
- }
1645
-
1646
- get #endpointStructure() {
1647
- return this.#context.structure;
1648
- }
1649
- }