@matter/node 0.17.7 → 0.17.8-alpha.0-20260801-92169d0aa

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 (33) hide show
  1. package/dist/cjs/behavior/system/software-update/SoftwareUpdateManager.d.ts +2 -1
  2. package/dist/cjs/behavior/system/software-update/SoftwareUpdateManager.d.ts.map +1 -1
  3. package/dist/cjs/behavior/system/software-update/SoftwareUpdateManager.js +18 -9
  4. package/dist/cjs/behavior/system/software-update/SoftwareUpdateManager.js.map +1 -1
  5. package/dist/cjs/behavior/system/subscriptions/SubscriptionsServer.d.ts.map +1 -1
  6. package/dist/cjs/behavior/system/subscriptions/SubscriptionsServer.js +4 -1
  7. package/dist/cjs/behavior/system/subscriptions/SubscriptionsServer.js.map +1 -1
  8. package/dist/cjs/node/server/InteractionServer.d.ts.map +1 -1
  9. package/dist/cjs/node/server/InteractionServer.js +52 -35
  10. package/dist/cjs/node/server/InteractionServer.js.map +1 -1
  11. package/dist/cjs/node/server/ServerSubscription.d.ts +2 -2
  12. package/dist/cjs/node/server/ServerSubscription.d.ts.map +1 -1
  13. package/dist/cjs/node/server/ServerSubscription.js +41 -22
  14. package/dist/cjs/node/server/ServerSubscription.js.map +1 -1
  15. package/dist/esm/behavior/system/software-update/SoftwareUpdateManager.d.ts +2 -1
  16. package/dist/esm/behavior/system/software-update/SoftwareUpdateManager.d.ts.map +1 -1
  17. package/dist/esm/behavior/system/software-update/SoftwareUpdateManager.js +18 -9
  18. package/dist/esm/behavior/system/software-update/SoftwareUpdateManager.js.map +1 -1
  19. package/dist/esm/behavior/system/subscriptions/SubscriptionsServer.d.ts.map +1 -1
  20. package/dist/esm/behavior/system/subscriptions/SubscriptionsServer.js +16 -2
  21. package/dist/esm/behavior/system/subscriptions/SubscriptionsServer.js.map +1 -1
  22. package/dist/esm/node/server/InteractionServer.d.ts.map +1 -1
  23. package/dist/esm/node/server/InteractionServer.js +52 -35
  24. package/dist/esm/node/server/InteractionServer.js.map +1 -1
  25. package/dist/esm/node/server/ServerSubscription.d.ts +2 -2
  26. package/dist/esm/node/server/ServerSubscription.d.ts.map +1 -1
  27. package/dist/esm/node/server/ServerSubscription.js +44 -23
  28. package/dist/esm/node/server/ServerSubscription.js.map +1 -1
  29. package/package.json +6 -6
  30. package/src/behavior/system/software-update/SoftwareUpdateManager.ts +22 -9
  31. package/src/behavior/system/subscriptions/SubscriptionsServer.ts +22 -2
  32. package/src/node/server/InteractionServer.ts +59 -38
  33. package/src/node/server/ServerSubscription.ts +48 -26
@@ -7,16 +7,27 @@
7
7
  import { InteractionServer, PeerSubscription } from "#node/server/InteractionServer.js";
8
8
  import { ServerSubscription } from "#node/server/ServerSubscription.js";
9
9
  import {
10
+ causedBy,
10
11
  ChannelType,
11
12
  deepCopy,
12
13
  Logger,
13
14
  MatterAggregateError,
14
15
  MatterError,
15
16
  MaybePromise,
17
+ NetworkError,
18
+ NoResponseTimeoutError,
16
19
  Seconds,
17
20
  } from "@matter/general";
18
21
  import { DatatypeModel, FieldElement } from "@matter/model";
19
- import { GroupSession, PeerAddress, PeerAddressMap, PeerAddressSet, PeerSet, Subscription } from "@matter/protocol";
22
+ import {
23
+ GroupSession,
24
+ PeerAddress,
25
+ PeerAddressMap,
26
+ PeerAddressSet,
27
+ PeerSet,
28
+ Subscription,
29
+ TransientPeerCommunicationError,
30
+ } from "@matter/protocol";
20
31
  import { Status, StatusResponseError } from "@matter/types";
21
32
  import { Behavior } from "../../Behavior.js";
22
33
  import { SessionsBehavior } from "../sessions/SessionsBehavior.js";
@@ -162,7 +173,7 @@ export class SubscriptionsServer extends Behavior {
162
173
  }
163
174
 
164
175
  #subscriptionCancelled(subscription: Subscription): MaybePromise {
165
- if (subscription.isCanceledByPeer && this.state.persistenceEnabled !== false) {
176
+ if (subscription.isTerminated && this.state.persistenceEnabled !== false) {
166
177
  const { subscriptionId: id } = subscription;
167
178
  const subscriptionIndex = this.state.subscriptions.findIndex(({ subscriptionId }) => id === subscriptionId);
168
179
  if (subscriptionIndex !== -1) {
@@ -273,7 +284,16 @@ export class SubscriptionsServer extends Behavior {
273
284
  : sre.message
274
285
  : error,
275
286
  );
287
+ if (
288
+ causedBy(error, TransientPeerCommunicationError, NoResponseTimeoutError, NetworkError)
289
+ ) {
290
+ // Report sends do not declare peer loss, so nothing else stops this loop from
291
+ // spending a full MRP window on each of the peer's remaining subscriptions
292
+ break;
293
+ }
276
294
  if (isInvalidSubscription) {
295
+ // The peer dropped its state for us; too unlikely another of its subscriptions
296
+ // survived to spend an initial report finding out
277
297
  break;
278
298
  }
279
299
  continue;
@@ -79,6 +79,9 @@ const logger = Logger.get("InteractionServer");
79
79
  const MAX_READ_PATHS = 10_000;
80
80
  const MAX_SUBSCRIBE_PATHS = 10_000;
81
81
 
82
+ // A controller decides for itself when its sessions are gone; a report we cannot push does not prove they are
83
+ const SUBSCRIPTION_EXCHANGE_OPTIONS: MessageExchange.Options = { suppressPeerLoss: true };
84
+
82
85
  export interface PeerSubscription {
83
86
  subscriptionId: number;
84
87
  peerAddress: PeerAddress;
@@ -745,8 +748,16 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
745
748
  #initiateSubscriptionExchange(addressOrSession: PeerAddress | Session, protocolId: number) {
746
749
  const exchange =
747
750
  addressOrSession instanceof Session
748
- ? this.#context.exchangeManager.initiateExchangeForSession(addressOrSession, protocolId)
749
- : this.#context.exchangeManager.initiateExchange(addressOrSession, protocolId);
751
+ ? this.#context.exchangeManager.initiateExchangeForSession(
752
+ addressOrSession,
753
+ protocolId,
754
+ SUBSCRIPTION_EXCHANGE_OPTIONS,
755
+ )
756
+ : this.#context.exchangeManager.initiateExchange(
757
+ addressOrSession,
758
+ protocolId,
759
+ SUBSCRIPTION_EXCHANGE_OPTIONS,
760
+ );
750
761
 
751
762
  // Count subscription report messages we push and the acks we receive in response
752
763
  this.#countExchangeMessages(exchange);
@@ -813,47 +824,50 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
813
824
  }: PeerSubscription,
814
825
  session: NodeSession,
815
826
  ) {
816
- const exchange = this.#context.exchangeManager.initiateExchange(session.peerAddress, INTERACTION_PROTOCOL_ID);
827
+ const exchange = this.#initiateSubscriptionExchange(session, INTERACTION_PROTOCOL_ID);
828
+ const messenger = new InteractionServerMessenger(exchange);
817
829
 
818
- logger.info(
819
- `Reestablish subscription`,
820
- Mark.OUTBOUND,
821
- exchange.via,
822
- Diagnostic.dict({
823
- ...Subscription.diagnosticOf(subscriptionId),
824
- isFabricFiltered,
825
- maxInterval: Duration.format(maxInterval),
826
- sendInterval: Duration.format(sendInterval),
827
- }),
828
- );
830
+ let subscription: ServerSubscription | undefined;
831
+ try {
832
+ logger.info(
833
+ `Reestablish subscription`,
834
+ Mark.OUTBOUND,
835
+ exchange.via,
836
+ Diagnostic.dict({
837
+ ...Subscription.diagnosticOf(subscriptionId),
838
+ isFabricFiltered,
839
+ maxInterval: Duration.format(maxInterval),
840
+ sendInterval: Duration.format(sendInterval),
841
+ }),
842
+ );
829
843
 
830
- const context: ServerSubscriptionContext = {
831
- session,
832
- node: this.#node,
833
- initiateExchange: (addressOrSession, protocolId) =>
834
- this.#initiateSubscriptionExchange(addressOrSession, protocolId),
835
- };
844
+ const context: ServerSubscriptionContext = {
845
+ session,
846
+ node: this.#node,
847
+ initiateExchange: (addressOrSession, protocolId) =>
848
+ this.#initiateSubscriptionExchange(addressOrSession, protocolId),
849
+ };
836
850
 
837
- const subscription = new ServerSubscription({
838
- id: subscriptionId,
839
- context,
840
- request: {
841
- attributeRequests,
842
- eventRequests,
843
- isFabricFiltered,
844
- minIntervalFloorSeconds: Seconds.of(minIntervalFloor),
845
- maxIntervalCeilingSeconds: Seconds.of(maxIntervalCeiling),
846
- },
847
- subscriptionOptions: this.#subscriptionConfig,
848
- useAsMaxInterval: maxInterval,
849
- useAsSendInterval: sendInterval,
850
- });
851
+ subscription = new ServerSubscription({
852
+ id: subscriptionId,
853
+ context,
854
+ request: {
855
+ attributeRequests,
856
+ eventRequests,
857
+ isFabricFiltered,
858
+ minIntervalFloorSeconds: Seconds.of(minIntervalFloor),
859
+ maxIntervalCeilingSeconds: Seconds.of(maxIntervalCeiling),
860
+ },
861
+ subscriptionOptions: this.#subscriptionConfig,
862
+ useAsMaxInterval: maxInterval,
863
+ useAsSendInterval: sendInterval,
864
+ });
865
+
866
+ const readContext = this.#prepareOnlineContext(exchange, undefined, isFabricFiltered);
851
867
 
852
- const readContext = this.#prepareOnlineContext(exchange, undefined, isFabricFiltered);
853
- try {
854
868
  // Send initial data report to prime the subscription with initial data
855
869
  await subscription.sendInitialReport(
856
- new InteractionServerMessenger(exchange),
870
+ messenger,
857
871
  readContext,
858
872
  true, // Do not send status responses because we simulate that the subscription is still established
859
873
  );
@@ -873,8 +887,15 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
873
887
  );
874
888
  }
875
889
  } catch (error) {
876
- await subscription.close(); // Cleanup
890
+ await subscription?.close(); // Cleanup
877
891
  throw error;
892
+ } finally {
893
+ // Reports use their own exchanges; leaving this one open blocks the session from ever closing gracefully
894
+ try {
895
+ await messenger.close();
896
+ } catch (error) {
897
+ logger.warn("Error closing subscription re-establishment exchange", error);
898
+ }
878
899
  }
879
900
  return subscription;
880
901
  }
@@ -10,6 +10,7 @@ import { IcdManagementServer } from "#behaviors/icd-management";
10
10
  import type { ServerNode } from "#node/ServerNode.js";
11
11
  import {
12
12
  AsyncObservable,
13
+ causedBy,
13
14
  ClosedError,
14
15
  Diagnostic,
15
16
  Duration,
@@ -42,6 +43,7 @@ import {
42
43
  ReadResult,
43
44
  SessionClosedError,
44
45
  Subscription,
46
+ TransientPeerCommunicationError,
45
47
  } from "@matter/protocol";
46
48
  import {
47
49
  AttributeId,
@@ -130,7 +132,7 @@ export class ServerSubscription implements Subscription {
130
132
 
131
133
  #id: SubscriptionId;
132
134
  #isClosed = false;
133
- #isCanceledByPeer = false;
135
+ #isTerminated = false;
134
136
  #request: Omit<SubscribeRequest, "interactionModelRevision" | "keepSubscriptions">;
135
137
  #cancelled = AsyncObservable<[subscription: Subscription]>();
136
138
  #maxInterval?: Duration;
@@ -206,8 +208,8 @@ export class ServerSubscription implements Subscription {
206
208
  return this.#context.session;
207
209
  }
208
210
 
209
- get isCanceledByPeer() {
210
- return this.#isCanceledByPeer;
211
+ get isTerminated() {
212
+ return this.#isTerminated;
211
213
  }
212
214
 
213
215
  get request() {
@@ -245,7 +247,8 @@ export class ServerSubscription implements Subscription {
245
247
  }
246
248
 
247
249
  async handlePeerCancel() {
248
- this.#isCanceledByPeer = true;
250
+ logger.notice(`Subscription ${this.idStr} cancelled by peer`);
251
+ this.#isTerminated = true;
249
252
  // Force-close any in-flight send exchange so MRP retransmissions stop immediately.
250
253
  // Use try/finally so this.close() always runs even if the exchange close throws.
251
254
  try {
@@ -508,7 +511,7 @@ export class ServerSubscription implements Subscription {
508
511
  this.#sendUpdateErrorCounter = 0;
509
512
  }
510
513
  } catch (error) {
511
- if (this.#isClosed || this.#isCanceledByPeer) {
514
+ if (this.#isClosed || this.#isTerminated) {
512
515
  // No need to care about resubmissions when the server is closing or peer cancelled us
513
516
  return;
514
517
  }
@@ -535,20 +538,24 @@ export class ServerSubscription implements Subscription {
535
538
  this.#outstandingEventsMinNumber = eventsMinNumber; // newer number are always higher, so we can just set it
536
539
  }
537
540
  } else {
538
- logger.info(
539
- `Sending update failed 3 times in a row, canceling subscription ${this.idStr} and let controller subscribe again.`,
541
+ logger.notice(
542
+ `Giving up on subscription ${this.idStr} after 3 failed updates; the controller must subscribe again`,
540
543
  );
541
544
  this.#sendNextUpdateImmediately = false;
542
545
  if (
543
- error instanceof NoResponseTimeoutError ||
544
- error instanceof NetworkError ||
545
- error instanceof SessionClosedError
546
+ causedBy(
547
+ error,
548
+ TransientPeerCommunicationError,
549
+ NoResponseTimeoutError,
550
+ NetworkError,
551
+ SessionClosedError,
552
+ )
546
553
  ) {
547
- // Let's consider this subscription as dead and wait for a reconnect. We handle as if the
548
- // controller cancelled
549
- using _messaging = updating?.join("canceling");
550
- this.#isCanceledByPeer = true;
551
- await this.#cancel();
554
+ // The session is left alone: failing to push reports says nothing about whether the
555
+ // controller can still reach us, and recovery is its call regardless
556
+ using _messaging = updating?.join("abandoning");
557
+ this.#isTerminated = true;
558
+ await this.#closeFromUpdate();
552
559
  break;
553
560
  } else {
554
561
  throw error;
@@ -693,12 +700,12 @@ export class ServerSubscription implements Subscription {
693
700
  });
694
701
  }
695
702
 
696
- async #flush(flushViaSession?: Session) {
703
+ async #flush(flushViaSession?: Session, currentExchange?: MessageExchange) {
697
704
  this.#sendDelayTimer.stop();
698
705
  if (this.#outstandingAttributeUpdates !== undefined || this.#outstandingEventsMinNumber !== undefined) {
699
706
  logger.debug(`Flushing subscription ${this.idStr}${this.#isClosed ? " (for closing)" : ""}`);
700
707
  this.#triggerSendUpdate(true, flushViaSession);
701
- if (this.#currentUpdatePromise) {
708
+ if (this.#currentUpdatePromise && !this.#isSendingOn(currentExchange)) {
702
709
  using _waiting = this.#lifetime?.join("waiting on flush");
703
710
  await this.#currentUpdatePromise;
704
711
  }
@@ -708,21 +715,35 @@ export class ServerSubscription implements Subscription {
708
715
  /**
709
716
  * Closes the subscription and flushes all outstanding data updates if requested.
710
717
  */
711
- async close(flushViaSession?: Session) {
718
+ async close(flushViaSession?: Session, currentExchange?: MessageExchange) {
712
719
  if (this.#isClosed) {
713
720
  return;
714
721
  }
715
722
  this.#isClosed = true;
716
723
 
717
- await this.#cancel(flushViaSession);
724
+ await this.#cancel(flushViaSession, currentExchange);
718
725
 
719
- if (this.#currentUpdatePromise) {
726
+ if (this.#currentUpdatePromise && !this.#isSendingOn(currentExchange)) {
720
727
  using _waiting = this.#lifetime?.closing()?.join("waiting on update");
721
728
  await this.#currentUpdatePromise;
722
729
  }
723
730
  }
724
731
 
725
- async #cancel(flushViaSession?: Session) {
732
+ /** Close from within the update loop, which {@link close} would wait on -- it is our own caller. */
733
+ async #closeFromUpdate() {
734
+ if (this.#isClosed) {
735
+ return;
736
+ }
737
+ this.#isClosed = true;
738
+ await this.#cancel();
739
+ }
740
+
741
+ /** Whether our in-flight update is sending on this exchange, making it this close's own caller. */
742
+ #isSendingOn(currentExchange?: MessageExchange) {
743
+ return currentExchange !== undefined && currentExchange === this.#currentSendExchange;
744
+ }
745
+
746
+ async #cancel(flushViaSession?: Session, currentExchange?: MessageExchange) {
726
747
  const closing = this.#lifetime?.closing();
727
748
 
728
749
  this.#sendUpdatesActivated = false;
@@ -731,7 +752,7 @@ export class ServerSubscription implements Subscription {
731
752
 
732
753
  if (flushViaSession !== undefined) {
733
754
  using _flushing = closing?.join("flushing");
734
- await this.#flush(flushViaSession);
755
+ await this.#flush(flushViaSession, currentExchange);
735
756
  }
736
757
 
737
758
  this.#updateTimer.stop();
@@ -845,19 +866,20 @@ export class ServerSubscription implements Subscription {
845
866
  }
846
867
  } catch (error) {
847
868
  if (StatusResponseError.is(error, Status.InvalidSubscription, Status.Failure)) {
848
- logger.notice(`Subscription ${this.idStr} cancelled by peer`);
849
- this.#isCanceledByPeer = true;
869
+ logger.notice(`Subscription ${this.idStr} reported invalid by peer`);
870
+ this.#isTerminated = true;
850
871
  } else {
851
872
  StatusResponseError.accept(error);
852
873
  logger.info(`Subscription ${this.idStr} update failed:`, error);
853
874
  }
854
875
 
855
876
  using _canceling = lifetime?.join("canceling");
856
- await this.#cancel();
877
+ await this.#closeFromUpdate();
857
878
  } finally {
858
- this.#currentSendExchange = undefined;
859
879
  using _closing = lifetime?.join("closing messenger");
880
+ // Reset only after the close: it can still send, and #isSendingOn must recognise this exchange until then
860
881
  await messenger.close();
882
+ this.#currentSendExchange = undefined;
861
883
  }
862
884
  return true;
863
885
  }