@matter/node 0.16.8-alpha.0-20260123-dff2cae52 → 0.16.8-alpha.0-20260127-65e1b40e2

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 (72) hide show
  1. package/dist/cjs/behavior/system/commissioning/CommissioningClient.d.ts +1 -0
  2. package/dist/cjs/behavior/system/commissioning/CommissioningClient.d.ts.map +1 -1
  3. package/dist/cjs/behavior/system/commissioning/CommissioningClient.js +29 -0
  4. package/dist/cjs/behavior/system/commissioning/CommissioningClient.js.map +1 -1
  5. package/dist/cjs/behavior/system/network/ClientNetworkRuntime.js +3 -3
  6. package/dist/cjs/behavior/system/network/ClientNetworkRuntime.js.map +1 -1
  7. package/dist/cjs/behavior/system/software-update/OtaAnnouncements.d.ts.map +1 -1
  8. package/dist/cjs/behavior/system/software-update/OtaAnnouncements.js +10 -8
  9. package/dist/cjs/behavior/system/software-update/OtaAnnouncements.js.map +1 -1
  10. package/dist/cjs/behavior/system/subscriptions/SubscriptionsServer.d.ts.map +1 -1
  11. package/dist/cjs/behavior/system/subscriptions/SubscriptionsServer.js +1 -6
  12. package/dist/cjs/behavior/system/subscriptions/SubscriptionsServer.js.map +1 -1
  13. package/dist/cjs/node/client/ClientNodeInteraction.d.ts +4 -1
  14. package/dist/cjs/node/client/ClientNodeInteraction.d.ts.map +1 -1
  15. package/dist/cjs/node/client/ClientNodeInteraction.js +4 -1
  16. package/dist/cjs/node/client/ClientNodeInteraction.js.map +1 -1
  17. package/dist/cjs/node/client/NodePeerAddressStore.d.ts +0 -1
  18. package/dist/cjs/node/client/NodePeerAddressStore.d.ts.map +1 -1
  19. package/dist/cjs/node/client/NodePeerAddressStore.js +0 -2
  20. package/dist/cjs/node/client/NodePeerAddressStore.js.map +1 -1
  21. package/dist/cjs/node/server/InteractionServer.d.ts +2 -2
  22. package/dist/cjs/node/server/InteractionServer.d.ts.map +1 -1
  23. package/dist/cjs/node/server/InteractionServer.js +130 -78
  24. package/dist/cjs/node/server/InteractionServer.js.map +1 -1
  25. package/dist/cjs/node/server/OnlineServerInteraction.d.ts +10 -2
  26. package/dist/cjs/node/server/OnlineServerInteraction.d.ts.map +1 -1
  27. package/dist/cjs/node/server/OnlineServerInteraction.js +9 -1
  28. package/dist/cjs/node/server/OnlineServerInteraction.js.map +1 -1
  29. package/dist/cjs/storage/client/RemoteWriter.d.ts.map +1 -1
  30. package/dist/cjs/storage/client/RemoteWriter.js +1 -2
  31. package/dist/cjs/storage/client/RemoteWriter.js.map +1 -1
  32. package/dist/esm/behavior/system/commissioning/CommissioningClient.d.ts +1 -0
  33. package/dist/esm/behavior/system/commissioning/CommissioningClient.d.ts.map +1 -1
  34. package/dist/esm/behavior/system/commissioning/CommissioningClient.js +29 -0
  35. package/dist/esm/behavior/system/commissioning/CommissioningClient.js.map +1 -1
  36. package/dist/esm/behavior/system/network/ClientNetworkRuntime.js +3 -3
  37. package/dist/esm/behavior/system/network/ClientNetworkRuntime.js.map +1 -1
  38. package/dist/esm/behavior/system/software-update/OtaAnnouncements.d.ts.map +1 -1
  39. package/dist/esm/behavior/system/software-update/OtaAnnouncements.js +11 -9
  40. package/dist/esm/behavior/system/software-update/OtaAnnouncements.js.map +1 -1
  41. package/dist/esm/behavior/system/subscriptions/SubscriptionsServer.d.ts.map +1 -1
  42. package/dist/esm/behavior/system/subscriptions/SubscriptionsServer.js +2 -15
  43. package/dist/esm/behavior/system/subscriptions/SubscriptionsServer.js.map +1 -1
  44. package/dist/esm/node/client/ClientNodeInteraction.d.ts +4 -1
  45. package/dist/esm/node/client/ClientNodeInteraction.d.ts.map +1 -1
  46. package/dist/esm/node/client/ClientNodeInteraction.js +4 -1
  47. package/dist/esm/node/client/ClientNodeInteraction.js.map +1 -1
  48. package/dist/esm/node/client/NodePeerAddressStore.d.ts +0 -1
  49. package/dist/esm/node/client/NodePeerAddressStore.d.ts.map +1 -1
  50. package/dist/esm/node/client/NodePeerAddressStore.js +0 -2
  51. package/dist/esm/node/client/NodePeerAddressStore.js.map +1 -1
  52. package/dist/esm/node/server/InteractionServer.d.ts +2 -2
  53. package/dist/esm/node/server/InteractionServer.d.ts.map +1 -1
  54. package/dist/esm/node/server/InteractionServer.js +130 -78
  55. package/dist/esm/node/server/InteractionServer.js.map +1 -1
  56. package/dist/esm/node/server/OnlineServerInteraction.d.ts +10 -2
  57. package/dist/esm/node/server/OnlineServerInteraction.d.ts.map +1 -1
  58. package/dist/esm/node/server/OnlineServerInteraction.js +9 -1
  59. package/dist/esm/node/server/OnlineServerInteraction.js.map +1 -1
  60. package/dist/esm/storage/client/RemoteWriter.d.ts.map +1 -1
  61. package/dist/esm/storage/client/RemoteWriter.js +1 -2
  62. package/dist/esm/storage/client/RemoteWriter.js.map +1 -1
  63. package/package.json +7 -7
  64. package/src/behavior/system/commissioning/CommissioningClient.ts +35 -0
  65. package/src/behavior/system/network/ClientNetworkRuntime.ts +3 -3
  66. package/src/behavior/system/software-update/OtaAnnouncements.ts +12 -9
  67. package/src/behavior/system/subscriptions/SubscriptionsServer.ts +4 -17
  68. package/src/node/client/ClientNodeInteraction.ts +4 -1
  69. package/src/node/client/NodePeerAddressStore.ts +0 -2
  70. package/src/node/server/InteractionServer.ts +197 -92
  71. package/src/node/server/OnlineServerInteraction.ts +13 -2
  72. package/src/storage/client/RemoteWriter.ts +1 -2
@@ -29,6 +29,7 @@ import {
29
29
  InteractionRecipient,
30
30
  InteractionServerMessenger,
31
31
  InvokeRequest,
32
+ InvokeResponseForSend,
32
33
  Mark,
33
34
  Message,
34
35
  MessageExchange,
@@ -45,10 +46,13 @@ import {
45
46
  TimedRequest,
46
47
  WriteRequest,
47
48
  WriteResponse,
49
+ WriteResult,
48
50
  } from "#protocol";
49
51
  import {
52
+ AttributeData,
50
53
  DEFAULT_MAX_PATHS_PER_INVOKE,
51
54
  INTERACTION_PROTOCOL_ID,
55
+ InvokeResponseData,
52
56
  ReceivedStatusResponseError,
53
57
  Status,
54
58
  StatusCode,
@@ -313,10 +317,11 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
313
317
  async handleWriteRequest(
314
318
  exchange: MessageExchange,
315
319
  writeRequest: WriteRequest,
320
+ messenger: InteractionServerMessenger,
316
321
  message: Message,
317
- ): Promise<WriteResponse> {
318
- const { suppressResponse, timedRequest, writeRequests, interactionModelRevision, moreChunkedMessages } =
319
- writeRequest;
322
+ ): Promise<void> {
323
+ let { suppressResponse, writeRequests, moreChunkedMessages } = writeRequest;
324
+ const { timedRequest, interactionModelRevision } = writeRequest;
320
325
  const sessionType = message.packetHeader.sessionType;
321
326
 
322
327
  logger.info(() => [
@@ -350,7 +355,7 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
350
355
  }
351
356
 
352
357
  if (exchange.hasExpiredTimedInteraction()) {
353
- exchange.clearTimedInteraction(); // ??
358
+ exchange.clearTimedInteraction();
354
359
  throw new StatusResponseError(`Timed request window expired. Decline write request.`, StatusCode.Timeout);
355
360
  }
356
361
 
@@ -379,25 +384,120 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
379
384
  );
380
385
  }
381
386
 
382
- // TODO: We still need to add multi message writes!
387
+ // Track the previous processed attribute path for list operations across chunks.
388
+ // A list ADD (listIndex === null) is only valid if the previous write was to the same attribute.
389
+ let previousProcessedAttributePath: WriteResult.ConcreteAttributePath | undefined;
383
390
 
384
- const result = await this.#serverInteraction.write(
385
- writeRequest,
386
- this.#prepareOnlineContext(
387
- exchange,
388
- message,
389
- true, // always fabric filtered
390
- receivedWithinTimedInteraction,
391
- ),
392
- );
391
+ // Process chunks until moreChunkedMessages is false
392
+ while (true) {
393
+ const allResponses = new Array<WriteResult.AttributeStatus>();
393
394
 
394
- return {
395
- writeResponses: result?.map(({ path, status, clusterStatus }) => ({
396
- path,
397
- status: { status, clusterStatus },
398
- })),
399
- interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
400
- };
395
+ // Separate write requests into batches based on list validity
396
+ // A list ADD without a prior REPLACE_ALL to the same attribute gets a BUSY response
397
+ let currentBatch = new Array<AttributeData>();
398
+
399
+ const processBatch = async () => {
400
+ if (currentBatch.length === 0) {
401
+ return;
402
+ }
403
+
404
+ const context = this.#prepareOnlineContext(
405
+ exchange,
406
+ message,
407
+ true, // always fabric filtered
408
+ receivedWithinTimedInteraction,
409
+ );
410
+
411
+ // Send batch to OnlineServerInteraction
412
+ const batchRequest = { ...writeRequest, writeRequests: currentBatch, suppressResponse: false };
413
+ const batchResults = await this.#serverInteraction.write(batchRequest, context);
414
+ if (batchResults) {
415
+ allResponses.push(...batchResults);
416
+ }
417
+
418
+ currentBatch = [];
419
+ };
420
+
421
+ for (const request of writeRequests) {
422
+ const { path } = request;
423
+ const listIndex = path.listIndex;
424
+
425
+ if (listIndex === null) {
426
+ // This is a list ADD - check if a previous path matches
427
+ if (
428
+ previousProcessedAttributePath?.endpointId !== path.endpointId ||
429
+ previousProcessedAttributePath?.clusterId !== path.clusterId ||
430
+ previousProcessedAttributePath?.attributeId !== path.attributeId
431
+ ) {
432
+ // Invalid ADD - process any pending batch first
433
+ await processBatch();
434
+
435
+ // According to Specification, ADDs are only allowed with a REPLACE before them
436
+ // Chip SDK returns "BUSY" in cases where this rule is not followed, so we do too
437
+ allResponses.push({
438
+ kind: "attr-status",
439
+ path: path as WriteResult.ConcreteAttributePath,
440
+ status: Status.Busy,
441
+ });
442
+
443
+ // Don't update previousProcessedAttributePath for BUSY responses
444
+ continue;
445
+ }
446
+ }
447
+
448
+ // Valid write - add to batch and update tracking
449
+ currentBatch.push(request);
450
+ if (path.endpointId !== undefined && path.clusterId !== undefined && path.attributeId !== undefined) {
451
+ previousProcessedAttributePath = path as WriteResult.ConcreteAttributePath;
452
+ }
453
+ }
454
+
455
+ // Process any remaining batch
456
+ await processBatch();
457
+
458
+ if (suppressResponse) {
459
+ // No response to send, we are done
460
+ break;
461
+ }
462
+
463
+ // Send WriteResponse for this chunk
464
+ const chunkResponse: WriteResponse = {
465
+ writeResponses: allResponses.map(({ path, status, clusterStatus }) => ({
466
+ path,
467
+ status: { status, clusterStatus },
468
+ })),
469
+ interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
470
+ };
471
+
472
+ await messenger.sendWriteResponse(chunkResponse, {
473
+ logContext: moreChunkedMessages ? "WriteResponse-chunk" : undefined,
474
+ });
475
+
476
+ if (!moreChunkedMessages) {
477
+ // Was the last message, so we are done
478
+ break;
479
+ }
480
+
481
+ // Wait for the next chunk
482
+ const nextChunk = await messenger.readNextWriteRequest();
483
+ const nextRequest = nextChunk.writeRequest;
484
+ ({ writeRequests, moreChunkedMessages, suppressResponse } = nextRequest);
485
+
486
+ logger.info(() => [
487
+ "Write",
488
+ Mark.INBOUND,
489
+ exchange.via,
490
+ Diagnostic.asFlags({ suppressResponse, moreChunkedMessages }),
491
+ Diagnostic.weak(writeRequests.map(req => this.#node.protocol.inspectPath(req.path)).join(", ")),
492
+ ]);
493
+
494
+ if (suppressResponse) {
495
+ throw new StatusResponseError(
496
+ "Multiple chunked messages and SuppressResponse cannot be used together in write messages",
497
+ StatusCode.InvalidAction,
498
+ );
499
+ }
500
+ }
401
501
  }
402
502
 
403
503
  async handleSubscribeRequest(
@@ -738,7 +838,7 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
738
838
 
739
839
  const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
740
840
  if (exchange.hasExpiredTimedInteraction()) {
741
- exchange.clearTimedInteraction(); // ??
841
+ exchange.clearTimedInteraction();
742
842
  throw new StatusResponseError(`Timed request window expired. Decline invoke request.`, StatusCode.Timeout);
743
843
  }
744
844
 
@@ -767,92 +867,78 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
767
867
  );
768
868
  }
769
869
 
870
+ const context = this.#prepareOnlineContext(exchange, message, undefined, receivedWithinTimedInteraction);
871
+
770
872
  const isGroupSession = message.packetHeader.sessionType === SessionType.Group;
771
- const invokeResponseMessage: TypeFromSchema<typeof TlvInvokeResponseForSend> = {
873
+
874
+ // Get the invoke-results from the server interaction
875
+ const results = this.#serverInteraction.invoke(request, context);
876
+
877
+ // For suppressResponse or group sessions, just consume the iterator without sending responses
878
+ if (suppressResponse || isGroupSession) {
879
+ for await (const _chunk of results);
880
+ return;
881
+ }
882
+
883
+ // Track accumulated responses for the current message
884
+ const currentChunkResponses = new Array<InvokeResponseData>();
885
+ const emptyInvokeResponse: InvokeResponseForSend = {
772
886
  suppressResponse: false, // Deprecated but must be present
773
887
  interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
774
888
  invokeResponses: [],
775
- moreChunkedMessages: invokeRequests.length > 1, // Assume for now we have multiple responses when having multiple invokes
776
889
  };
777
- const emptyInvokeResponseBytes = TlvInvokeResponseForSend.encode(invokeResponseMessage);
778
- let messageSize = emptyInvokeResponseBytes.byteLength;
779
- let invokeResultsProcessed = 0;
780
-
781
- // To lower potential latency when we would process all invoke messages and just send responses at the end we
782
- // assemble response on the fly locally here and send when message becomes too big
783
- // TODO generalize as streaming like DataReports
784
- const processResponseResult = async (
785
- invokeResponse: TypeFromSchema<typeof TlvInvokeResponseData>,
786
- ): Promise<void> => {
787
- invokeResultsProcessed++;
788
-
789
- if (isGroupSession) {
790
- // We send no responses at all for group sessions
791
- return;
792
- }
890
+ const emptyInvokeResponseLength = TlvInvokeResponseForSend.encode(emptyInvokeResponse).byteLength;
891
+ let messageSize = emptyInvokeResponseLength;
892
+ let chunkedTransmissionTerminated = false;
893
+
894
+ /**
895
+ * Send a chunk when the message size limit would be exceeded.
896
+ */
897
+ const sendChunkIfNeeded = async (invokeResponse: InvokeResponseData): Promise<void> => {
793
898
  const encodedInvokeResponse = TlvInvokeResponseData.encodeTlv(invokeResponse);
794
899
  const invokeResponseBytes = TlvAny.getEncodedByteLength(encodedInvokeResponse);
795
900
 
796
- if (
797
- messageSize + invokeResponseBytes > exchange.maxPayloadSize ||
798
- invokeResultsProcessed === invokeRequests.length
799
- ) {
800
- let lastMessageProcessed = false;
801
- if (messageSize + invokeResponseBytes <= exchange.maxPayloadSize) {
802
- // last invoke response and matches in the message
803
- invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
804
- lastMessageProcessed = true;
805
- }
806
- // Send the response when the message is full or when all responses are processed
807
- if (invokeResponseMessage.invokeResponses.length > 0) {
808
- if (invokeRequests.length > 1) {
809
- logger.debug(
810
- `${lastMessageProcessed ? "Final " : ""}Invoke response`,
811
- Mark.OUTBOUND,
812
- Diagnostic.dict({ commands: invokeResponseMessage.invokeResponses.length }),
813
- );
814
- }
815
- const moreChunkedMessages = lastMessageProcessed ? undefined : true;
816
- await messenger.send(
817
- MessageType.InvokeResponse,
818
- TlvInvokeResponseForSend.encode({
819
- ...invokeResponseMessage,
820
- moreChunkedMessages,
821
- }),
822
- {
823
- logContext: {
824
- invokeMsgFlags: Diagnostic.asFlags({
825
- suppressResponse,
826
- moreChunkedMessages,
827
- }),
828
- },
829
- },
830
- );
831
- invokeResponseMessage.invokeResponses = [];
832
- messageSize = emptyInvokeResponseBytes.byteLength;
833
- }
834
- if (!lastMessageProcessed) {
835
- invokeResultsProcessed--; // Correct counter again because we recall the method
836
- return processResponseResult(invokeResponse);
901
+ // Check if adding this response would exceed message size
902
+ if (messageSize + invokeResponseBytes > exchange.maxPayloadSize && currentChunkResponses.length > 0) {
903
+ logger.debug(
904
+ "Invoke (chunk)",
905
+ Mark.OUTBOUND,
906
+ exchange.via,
907
+ Diagnostic.dict({ commands: currentChunkResponses.length }),
908
+ );
909
+
910
+ const chunkResponse: InvokeResponseForSend = {
911
+ ...emptyInvokeResponse,
912
+ invokeResponses: currentChunkResponses.map(r => TlvInvokeResponseData.encodeTlv(r)),
913
+ };
914
+
915
+ if (!(await messenger.sendInvokeResponseChunk(chunkResponse))) {
916
+ chunkedTransmissionTerminated = true;
917
+ return;
837
918
  }
838
- } else {
839
- invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
840
- messageSize += invokeResponseBytes;
919
+
920
+ // Reset for next chunk
921
+ currentChunkResponses.length = 0;
922
+ messageSize = emptyInvokeResponseLength;
841
923
  }
924
+
925
+ // Add to the current chunk
926
+ currentChunkResponses.push(invokeResponse);
927
+ messageSize += invokeResponseBytes;
842
928
  };
843
929
 
844
- for await (const chunk of this.#serverInteraction.invoke(
845
- request,
846
- this.#prepareOnlineContext(exchange, message, undefined, receivedWithinTimedInteraction),
847
- )) {
848
- if (suppressResponse) {
849
- throw new InternalError("Received response that should be suppressed for invoke");
930
+ // Process all invoke results
931
+ for await (const chunk of results) {
932
+ if (chunkedTransmissionTerminated) {
933
+ // Client terminated the chunked series, continue consuming but don't send
934
+ continue;
850
935
  }
936
+
851
937
  for (const data of chunk) {
852
938
  switch (data.kind) {
853
939
  case "cmd-response": {
854
940
  const { path: commandPath, commandRef, data: commandFields } = data;
855
- await processResponseResult({
941
+ await sendChunkIfNeeded({
856
942
  command: {
857
943
  commandPath,
858
944
  commandFields,
@@ -864,13 +950,32 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
864
950
 
865
951
  case "cmd-status": {
866
952
  const { path, commandRef, status, clusterStatus } = data;
867
- await processResponseResult({
953
+ await sendChunkIfNeeded({
868
954
  status: { commandPath: path, status: { status, clusterStatus }, commandRef },
869
955
  });
956
+ break;
870
957
  }
871
958
  }
872
959
  }
873
960
  }
961
+
962
+ // Send the final response if not already terminated
963
+ if (!chunkedTransmissionTerminated) {
964
+ if (currentChunkResponses.length > 0) {
965
+ logger.debug(
966
+ "Invoke (final)",
967
+ Mark.OUTBOUND,
968
+ exchange.via,
969
+ Diagnostic.dict({ commands: currentChunkResponses.length }),
970
+ );
971
+ }
972
+
973
+ const finalResponse: InvokeResponseForSend = {
974
+ ...emptyInvokeResponse,
975
+ invokeResponses: currentChunkResponses.map(r => TlvInvokeResponseData.encodeTlv(r)),
976
+ };
977
+ await messenger.sendInvokeResponse(finalResponse);
978
+ }
874
979
  }
875
980
 
876
981
  handleTimedRequest(exchange: MessageExchange, { timeout, interactionModelRevision }: TimedRequest) {
@@ -3,6 +3,7 @@ import { NotImplementedError } from "#general";
3
3
  import {
4
4
  Interactable,
5
5
  Invoke,
6
+ InvokeResult,
6
7
  NodeProtocol,
7
8
  Read,
8
9
  ReadResult,
@@ -35,12 +36,21 @@ export class OnlineServerInteraction implements Interactable<RemoteActorContext.
35
36
  throw new NotImplementedError("subscribe not implemented");
36
37
  }
37
38
 
39
+ /**
40
+ * Process write requests and return results.
41
+ * The caller is responsible for messaging/chunking and list state tracking.
42
+ */
38
43
  async write<T extends Write>(request: T, context: RemoteActorContext.Options): WriteResult<T> {
39
44
  return RemoteActorContext(context).act(session => this.#interaction.write(request, session));
40
45
  }
41
46
 
42
- async *invoke(request: Invoke, context: RemoteActorContext.Options) {
47
+ /**
48
+ * Process invoke requests and yield results.
49
+ * The caller is responsible for messaging/chunking.
50
+ */
51
+ async *invoke(request: Invoke, context: RemoteActorContext.Options): InvokeResult {
43
52
  const session = RemoteActorContext({ ...context, command: true }).open();
53
+
44
54
  try {
45
55
  for await (const chunk of this.#interaction.invoke(request, session)) {
46
56
  yield chunk;
@@ -48,6 +58,7 @@ export class OnlineServerInteraction implements Interactable<RemoteActorContext.
48
58
  } catch (error) {
49
59
  await session.reject(error);
50
60
  }
51
- return session.resolve(undefined);
61
+
62
+ await session.resolve(undefined);
52
63
  }
53
64
  }
@@ -55,8 +55,7 @@ export function RemoteWriter(node: ClientNode, structure: ClientStructure): Remo
55
55
  }
56
56
 
57
57
  const write = Write(...attrWrites);
58
- const response = await node.interaction.write(write);
59
- WriteResult.assertSuccess(response);
58
+ WriteResult.assertSuccess(await node.interaction.write(write));
60
59
  };
61
60
  }
62
61