@matter/protocol 0.16.0-alpha.0-20251030-e9ca79f93 → 0.16.0-alpha.0-20251101-70c8d51d7

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 (164) hide show
  1. package/dist/cjs/action/Interactable.d.ts +1 -0
  2. package/dist/cjs/action/Interactable.d.ts.map +1 -1
  3. package/dist/cjs/action/client/ClientInteraction.d.ts +25 -19
  4. package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
  5. package/dist/cjs/action/client/ClientInteraction.js +198 -94
  6. package/dist/cjs/action/client/ClientInteraction.js.map +2 -2
  7. package/dist/cjs/action/client/index.d.ts +1 -3
  8. package/dist/cjs/action/client/index.d.ts.map +1 -1
  9. package/dist/cjs/action/client/index.js +1 -3
  10. package/dist/cjs/action/client/index.js.map +1 -1
  11. package/dist/cjs/action/client/subscription/ClientSubscribe.d.ts +8 -0
  12. package/dist/cjs/action/client/subscription/ClientSubscribe.d.ts.map +1 -0
  13. package/dist/cjs/action/client/{ClientSubscription.js → subscription/ClientSubscribe.js} +3 -8
  14. package/dist/cjs/action/client/subscription/ClientSubscribe.js.map +6 -0
  15. package/dist/cjs/action/client/subscription/ClientSubscription.d.ts +38 -0
  16. package/dist/cjs/action/client/subscription/ClientSubscription.d.ts.map +1 -0
  17. package/dist/cjs/action/client/subscription/ClientSubscription.js +79 -0
  18. package/dist/cjs/action/client/subscription/ClientSubscription.js.map +6 -0
  19. package/dist/cjs/action/client/subscription/ClientSubscriptionHandler.d.ts.map +1 -0
  20. package/dist/cjs/action/client/{ClientSubscriptionHandler.js → subscription/ClientSubscriptionHandler.js} +5 -2
  21. package/dist/cjs/action/client/subscription/ClientSubscriptionHandler.js.map +6 -0
  22. package/dist/{esm/action/client → cjs/action/client/subscription}/ClientSubscriptions.d.ts +15 -8
  23. package/dist/cjs/action/client/subscription/ClientSubscriptions.d.ts.map +1 -0
  24. package/dist/cjs/action/client/subscription/ClientSubscriptions.js +133 -0
  25. package/dist/cjs/action/client/subscription/ClientSubscriptions.js.map +6 -0
  26. package/dist/cjs/action/client/subscription/PeerSubscription.d.ts +27 -0
  27. package/dist/cjs/action/client/subscription/PeerSubscription.d.ts.map +1 -0
  28. package/dist/cjs/action/client/subscription/PeerSubscription.js +57 -0
  29. package/dist/cjs/action/client/subscription/PeerSubscription.js.map +6 -0
  30. package/dist/cjs/action/client/subscription/SustainedSubscription.d.ts +57 -0
  31. package/dist/cjs/action/client/subscription/SustainedSubscription.d.ts.map +1 -0
  32. package/dist/cjs/action/client/subscription/SustainedSubscription.js +143 -0
  33. package/dist/cjs/action/client/subscription/SustainedSubscription.js.map +6 -0
  34. package/dist/cjs/action/client/subscription/index.d.ts +12 -0
  35. package/dist/cjs/action/client/subscription/index.d.ts.map +1 -0
  36. package/dist/cjs/action/client/subscription/index.js +29 -0
  37. package/dist/cjs/action/client/subscription/index.js.map +6 -0
  38. package/dist/cjs/action/errors.d.ts +7 -2
  39. package/dist/cjs/action/errors.d.ts.map +1 -1
  40. package/dist/cjs/action/errors.js +6 -3
  41. package/dist/cjs/action/errors.js.map +1 -1
  42. package/dist/cjs/action/request/Subscribe.d.ts +2 -2
  43. package/dist/cjs/action/request/Subscribe.d.ts.map +1 -1
  44. package/dist/cjs/action/request/Subscribe.js.map +1 -1
  45. package/dist/cjs/action/response/ReadResult.d.ts +1 -1
  46. package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
  47. package/dist/cjs/action/response/SubscribeResult.d.ts +2 -1
  48. package/dist/cjs/action/response/SubscribeResult.d.ts.map +1 -1
  49. package/dist/cjs/action/server/ServerInteraction.d.ts +0 -1
  50. package/dist/cjs/action/server/ServerInteraction.d.ts.map +1 -1
  51. package/dist/cjs/action/server/ServerInteraction.js +0 -3
  52. package/dist/cjs/action/server/ServerInteraction.js.map +1 -1
  53. package/dist/cjs/bdx/flow/InboundFlow.js +1 -1
  54. package/dist/cjs/bdx/flow/InboundFlow.js.map +1 -1
  55. package/dist/cjs/interaction/SubscriptionClient.d.ts +3 -3
  56. package/dist/cjs/interaction/SubscriptionClient.d.ts.map +1 -1
  57. package/dist/cjs/interaction/SubscriptionClient.js +0 -7
  58. package/dist/cjs/interaction/SubscriptionClient.js.map +1 -1
  59. package/dist/cjs/peer/PeerSet.d.ts +1 -1
  60. package/dist/cjs/peer/PeerSet.d.ts.map +1 -1
  61. package/dist/cjs/protocol/MessageExchange.js +1 -1
  62. package/dist/cjs/protocol/MessageExchange.js.map +1 -1
  63. package/dist/esm/action/Interactable.d.ts +1 -0
  64. package/dist/esm/action/Interactable.d.ts.map +1 -1
  65. package/dist/esm/action/client/ClientInteraction.d.ts +25 -19
  66. package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
  67. package/dist/esm/action/client/ClientInteraction.js +201 -95
  68. package/dist/esm/action/client/ClientInteraction.js.map +2 -2
  69. package/dist/esm/action/client/index.d.ts +1 -3
  70. package/dist/esm/action/client/index.d.ts.map +1 -1
  71. package/dist/esm/action/client/index.js +1 -3
  72. package/dist/esm/action/client/index.js.map +1 -1
  73. package/dist/esm/action/client/subscription/ClientSubscribe.d.ts +8 -0
  74. package/dist/esm/action/client/subscription/ClientSubscribe.d.ts.map +1 -0
  75. package/dist/esm/action/client/subscription/ClientSubscribe.js +1 -0
  76. package/dist/esm/action/client/subscription/ClientSubscribe.js.map +6 -0
  77. package/dist/esm/action/client/subscription/ClientSubscription.d.ts +38 -0
  78. package/dist/esm/action/client/subscription/ClientSubscription.d.ts.map +1 -0
  79. package/dist/esm/action/client/subscription/ClientSubscription.js +59 -0
  80. package/dist/esm/action/client/subscription/ClientSubscription.js.map +6 -0
  81. package/dist/esm/action/client/subscription/ClientSubscriptionHandler.d.ts.map +1 -0
  82. package/dist/esm/action/client/{ClientSubscriptionHandler.js → subscription/ClientSubscriptionHandler.js} +5 -2
  83. package/dist/esm/action/client/subscription/ClientSubscriptionHandler.js.map +6 -0
  84. package/dist/{cjs/action/client → esm/action/client/subscription}/ClientSubscriptions.d.ts +15 -8
  85. package/dist/esm/action/client/subscription/ClientSubscriptions.d.ts.map +1 -0
  86. package/dist/esm/action/client/subscription/ClientSubscriptions.js +113 -0
  87. package/dist/esm/action/client/subscription/ClientSubscriptions.js.map +6 -0
  88. package/dist/esm/action/client/subscription/PeerSubscription.d.ts +27 -0
  89. package/dist/esm/action/client/subscription/PeerSubscription.d.ts.map +1 -0
  90. package/dist/esm/action/client/subscription/PeerSubscription.js +37 -0
  91. package/dist/esm/action/client/subscription/PeerSubscription.js.map +6 -0
  92. package/dist/esm/action/client/subscription/SustainedSubscription.d.ts +57 -0
  93. package/dist/esm/action/client/subscription/SustainedSubscription.d.ts.map +1 -0
  94. package/dist/esm/action/client/subscription/SustainedSubscription.js +133 -0
  95. package/dist/esm/action/client/subscription/SustainedSubscription.js.map +6 -0
  96. package/dist/esm/action/client/subscription/index.d.ts +12 -0
  97. package/dist/esm/action/client/subscription/index.d.ts.map +1 -0
  98. package/dist/esm/action/client/subscription/index.js +12 -0
  99. package/dist/esm/action/client/subscription/index.js.map +6 -0
  100. package/dist/esm/action/errors.d.ts +7 -2
  101. package/dist/esm/action/errors.d.ts.map +1 -1
  102. package/dist/esm/action/errors.js +6 -3
  103. package/dist/esm/action/errors.js.map +1 -1
  104. package/dist/esm/action/request/Subscribe.d.ts +2 -2
  105. package/dist/esm/action/request/Subscribe.d.ts.map +1 -1
  106. package/dist/esm/action/request/Subscribe.js.map +1 -1
  107. package/dist/esm/action/response/ReadResult.d.ts +1 -1
  108. package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
  109. package/dist/esm/action/response/SubscribeResult.d.ts +2 -1
  110. package/dist/esm/action/response/SubscribeResult.d.ts.map +1 -1
  111. package/dist/esm/action/server/ServerInteraction.d.ts +0 -1
  112. package/dist/esm/action/server/ServerInteraction.d.ts.map +1 -1
  113. package/dist/esm/action/server/ServerInteraction.js +0 -3
  114. package/dist/esm/action/server/ServerInteraction.js.map +1 -1
  115. package/dist/esm/bdx/flow/InboundFlow.js +1 -1
  116. package/dist/esm/bdx/flow/InboundFlow.js.map +1 -1
  117. package/dist/esm/interaction/SubscriptionClient.d.ts +3 -3
  118. package/dist/esm/interaction/SubscriptionClient.d.ts.map +1 -1
  119. package/dist/esm/interaction/SubscriptionClient.js +1 -8
  120. package/dist/esm/interaction/SubscriptionClient.js.map +1 -1
  121. package/dist/esm/peer/PeerSet.d.ts +1 -1
  122. package/dist/esm/peer/PeerSet.d.ts.map +1 -1
  123. package/dist/esm/protocol/MessageExchange.js +1 -1
  124. package/dist/esm/protocol/MessageExchange.js.map +1 -1
  125. package/package.json +6 -6
  126. package/src/action/Interactable.ts +1 -0
  127. package/src/action/client/ClientInteraction.ts +273 -235
  128. package/src/action/client/index.ts +1 -3
  129. package/src/action/client/subscription/ClientSubscribe.ts +8 -0
  130. package/src/action/client/subscription/ClientSubscription.ts +88 -0
  131. package/src/action/client/{ClientSubscriptionHandler.ts → subscription/ClientSubscriptionHandler.ts} +5 -2
  132. package/src/action/client/subscription/ClientSubscriptions.ts +150 -0
  133. package/src/action/client/subscription/PeerSubscription.ts +51 -0
  134. package/src/action/client/subscription/SustainedSubscription.ts +199 -0
  135. package/src/action/client/subscription/index.ts +12 -0
  136. package/src/action/errors.ts +11 -6
  137. package/src/action/request/Subscribe.ts +2 -2
  138. package/src/action/response/ReadResult.ts +1 -1
  139. package/src/action/response/SubscribeResult.ts +2 -1
  140. package/src/action/server/ServerInteraction.ts +0 -5
  141. package/src/bdx/flow/InboundFlow.ts +1 -1
  142. package/src/interaction/SubscriptionClient.ts +4 -9
  143. package/src/protocol/MessageExchange.ts +1 -1
  144. package/dist/cjs/action/client/ClientSubscription.d.ts +0 -18
  145. package/dist/cjs/action/client/ClientSubscription.d.ts.map +0 -1
  146. package/dist/cjs/action/client/ClientSubscription.js.map +0 -6
  147. package/dist/cjs/action/client/ClientSubscriptionHandler.d.ts.map +0 -1
  148. package/dist/cjs/action/client/ClientSubscriptionHandler.js.map +0 -6
  149. package/dist/cjs/action/client/ClientSubscriptions.d.ts.map +0 -1
  150. package/dist/cjs/action/client/ClientSubscriptions.js +0 -145
  151. package/dist/cjs/action/client/ClientSubscriptions.js.map +0 -6
  152. package/dist/esm/action/client/ClientSubscription.d.ts +0 -18
  153. package/dist/esm/action/client/ClientSubscription.d.ts.map +0 -1
  154. package/dist/esm/action/client/ClientSubscription.js +0 -6
  155. package/dist/esm/action/client/ClientSubscription.js.map +0 -6
  156. package/dist/esm/action/client/ClientSubscriptionHandler.d.ts.map +0 -1
  157. package/dist/esm/action/client/ClientSubscriptionHandler.js.map +0 -6
  158. package/dist/esm/action/client/ClientSubscriptions.d.ts.map +0 -1
  159. package/dist/esm/action/client/ClientSubscriptions.js +0 -135
  160. package/dist/esm/action/client/ClientSubscriptions.js.map +0 -6
  161. package/src/action/client/ClientSubscription.ts +0 -19
  162. package/src/action/client/ClientSubscriptions.ts +0 -178
  163. /package/dist/cjs/action/client/{ClientSubscriptionHandler.d.ts → subscription/ClientSubscriptionHandler.d.ts} +0 -0
  164. /package/dist/esm/action/client/{ClientSubscriptionHandler.d.ts → subscription/ClientSubscriptionHandler.d.ts} +0 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type { Subscribe } from "#action/request/Subscribe.js";
8
+ import type { ActiveSubscription } from "#action/response/SubscribeResult.js";
9
+ import { Abort, Diagnostic, Logger } from "#general";
10
+ import { PeerAddress } from "#peer/PeerAddress.js";
11
+ import { ClientSubscribe } from "./ClientSubscribe.js";
12
+
13
+ const logger = Logger.get("ClientSubscription");
14
+
15
+ /**
16
+ * The client view of an established Matter subscription.
17
+ */
18
+ export abstract class ClientSubscription implements ActiveSubscription {
19
+ readonly request: Subscribe;
20
+ readonly peer: PeerAddress;
21
+ abstract subscriptionId: number;
22
+ abstract maxInterval: number;
23
+ abstract interactionModelRevision: number;
24
+
25
+ /**
26
+ * If the subscription has an async worker, this is the promise associated with the worker.
27
+ */
28
+ done?: Promise<void>;
29
+
30
+ readonly #closed: () => void;
31
+ readonly #abort: Abort;
32
+ #isClosed = false;
33
+
34
+ constructor({ request, peer, closed, abort }: ClientSubscription.Configuration) {
35
+ this.request = request;
36
+ this.peer = peer;
37
+ this.#closed = closed;
38
+ this.#abort = new Abort({
39
+ abort,
40
+ handler: this.close.bind(this),
41
+ });
42
+ }
43
+
44
+ close() {
45
+ if (this.#isClosed) {
46
+ return;
47
+ }
48
+ this.#isClosed = true;
49
+
50
+ this.#abort();
51
+ this.#closed();
52
+
53
+ const unhandledError = (e: unknown) => {
54
+ this.logger.error("Unhandled error in subscription to", Diagnostic.strong(this.peer.toString()), e);
55
+ };
56
+
57
+ if (this.done) {
58
+ this.done
59
+ .finally(() => {
60
+ this.request.closed?.();
61
+ })
62
+ .catch(unhandledError);
63
+ } else {
64
+ try {
65
+ this.request.closed?.();
66
+ } catch (e) {
67
+ unhandledError(e);
68
+ }
69
+ }
70
+ }
71
+
72
+ protected get abort() {
73
+ return this.#abort;
74
+ }
75
+
76
+ protected get logger() {
77
+ return logger;
78
+ }
79
+ }
80
+
81
+ export namespace ClientSubscription {
82
+ export interface Configuration {
83
+ request: ClientSubscribe;
84
+ peer: PeerAddress;
85
+ closed: () => void;
86
+ abort?: AbortSignal;
87
+ }
88
+ }
@@ -11,9 +11,10 @@ import { IncomingInteractionClientMessenger } from "#interaction/InteractionMess
11
11
  import { SubscriptionId } from "#interaction/Subscription.js";
12
12
  import { MessageExchange } from "#protocol/MessageExchange.js";
13
13
  import { ProtocolHandler } from "#protocol/ProtocolHandler.js";
14
+ import { SecureSession } from "#session/SecureSession.js";
14
15
  import { DataReport, INTERACTION_PROTOCOL_ID, Status } from "#types";
16
+ import { InputChunk } from "../InputChunk.js";
15
17
  import { ClientSubscriptions } from "./ClientSubscriptions.js";
16
- import { InputChunk } from "./InputChunk.js";
17
18
 
18
19
  const logger = Logger.get("ClientSubscriptionHandler");
19
20
 
@@ -50,7 +51,9 @@ export class ClientSubscriptionHandler implements ProtocolHandler {
50
51
  }
51
52
 
52
53
  // Ensure the subscription ID is valid
53
- const subscription = this.#subscriptions.get(subscriptionId);
54
+ const { session } = exchange.channel;
55
+ SecureSession.assert(session);
56
+ const subscription = this.#subscriptions.getPeer(session.peerAddress, subscriptionId);
54
57
  if (subscription === undefined) {
55
58
  logger.debug("Ignoring data report for unknown subscription ID", Diagnostic.strong(subscriptionId));
56
59
  await sendInvalid(messenger, subscriptionId);
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { ReadResult } from "#action/response/ReadResult.js";
8
+ import type { ActiveSubscription } from "#action/response/SubscribeResult.js";
9
+ import { BasicSet, Environment, Environmental, Millis, Time, Timer, Timestamp } from "#general";
10
+ import { SubscriptionId } from "#interaction/Subscription.js";
11
+ import { PeerAddress } from "#peer/PeerAddress.js";
12
+ import { ClientSubscription } from "./ClientSubscription.js";
13
+ import type { PeerSubscription } from "./PeerSubscription.js";
14
+
15
+ /**
16
+ * A managed set of {@link ActiveSubscription} instances.
17
+ */
18
+ export class ClientSubscriptions {
19
+ #active = new BasicSet<ClientSubscription>();
20
+ #peers = new Map<PeerAddress, Map<number, PeerSubscription>>();
21
+ #timeout?: Timer;
22
+
23
+ static [Environmental.create](env: Environment) {
24
+ const instance = new ClientSubscriptions();
25
+ env.set(ClientSubscriptions, instance);
26
+ return instance;
27
+ }
28
+
29
+ /**
30
+ * Register a user-facing {@link ClientSubscription}.
31
+ */
32
+ addActive(subscription: ClientSubscription) {
33
+ this.#active.add(subscription);
34
+ }
35
+
36
+ /**
37
+ * Register a {@link PeerSubscription}.
38
+ */
39
+ addPeer(subscription: PeerSubscription) {
40
+ let forPeer = this.#peers.get(subscription.peer);
41
+ if (forPeer === undefined) {
42
+ this.#peers.set(subscription.peer, (forPeer = new Map()));
43
+ }
44
+ forPeer.set(subscription.subscriptionId, subscription);
45
+
46
+ this.resetTimer();
47
+ }
48
+
49
+ /**
50
+ * Retrieve a {@link PeerSubscription} by ID.
51
+ */
52
+ getPeer(address: PeerAddress, id: SubscriptionId) {
53
+ return this.#peers.get(address)?.get(id);
54
+ }
55
+
56
+ /**
57
+ * Unregister a {@link PeerSubscription}.
58
+ */
59
+ delete(subscription: ClientSubscription) {
60
+ const forPeer = this.#peers.get(subscription.peer);
61
+ if (forPeer?.delete(subscription.subscriptionId)) {
62
+ if (!forPeer.size) {
63
+ this.#peers.delete(subscription.peer);
64
+ }
65
+ }
66
+ this.#active.delete(subscription);
67
+ }
68
+
69
+ /**
70
+ * Iterate over active subscriptions.
71
+ */
72
+ [Symbol.iterator]() {
73
+ return this.#active[Symbol.iterator]();
74
+ }
75
+
76
+ /**
77
+ * Terminate all subscriptions.
78
+ */
79
+ async close() {
80
+ if (this.#timeout) {
81
+ this.#timeout.stop();
82
+ this.#timeout = undefined;
83
+ }
84
+
85
+ for (const subscription of this.#active) {
86
+ subscription.close();
87
+ }
88
+
89
+ await this.#active.empty;
90
+ }
91
+
92
+ /**
93
+ * Restart the timeout timer for the current set of active subscriptions.
94
+ */
95
+ resetTimer() {
96
+ const now = Time.nowMs;
97
+ let nextTimeoutAt: Timestamp | undefined;
98
+
99
+ // Process each subscription
100
+ for (const peer of this.#peers.values()) {
101
+ for (const subscription of peer.values()) {
102
+ // If reading data reports, ignore for timeout purposes
103
+ if (subscription.isReading) {
104
+ continue;
105
+ }
106
+
107
+ // Update timeout or expire if timed out
108
+ let { timeoutAt } = subscription;
109
+ if (timeoutAt === undefined) {
110
+ // Set timeout time
111
+ timeoutAt = subscription.timeoutAt = Timestamp(now + subscription.timeout);
112
+ } else if (timeoutAt < now) {
113
+ // Timeout
114
+ subscription.timedOut();
115
+ continue;
116
+ }
117
+
118
+ // If this is the earliest timeout, record
119
+ if (nextTimeoutAt === undefined || nextTimeoutAt > timeoutAt) {
120
+ nextTimeoutAt = timeoutAt;
121
+ }
122
+ }
123
+ }
124
+
125
+ // If no subscriptions require timeout, disable timer
126
+ if (nextTimeoutAt === undefined) {
127
+ this.#timeout?.stop();
128
+ return;
129
+ }
130
+
131
+ // Create or update timer if not set for correct interval
132
+ if (this.#timeout) {
133
+ this.#timeout?.stop();
134
+ this.#timeout.interval = Millis(nextTimeoutAt - now);
135
+ } else {
136
+ this.#timeout = Time.getTimer(
137
+ "Subscription timeout",
138
+ Millis(nextTimeoutAt - now),
139
+ this.resetTimer.bind(this),
140
+ );
141
+ }
142
+ this.#timeout.start();
143
+ }
144
+ }
145
+
146
+ export namespace ClientSubscriptions {
147
+ export interface Listener {
148
+ (reports: AsyncIterable<ReadResult.Chunk>): Promise<void>;
149
+ }
150
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Diagnostic, Duration, Millis, Seconds, Timestamp } from "#general";
8
+ import type { SubscribeResponse } from "#types";
9
+ import { ClientSubscription } from "./ClientSubscription.js";
10
+
11
+ /**
12
+ * A Matter protocol-level subscription established with a peer.
13
+ */
14
+ export class PeerSubscription extends ClientSubscription {
15
+ readonly interactionModelRevision: number;
16
+ readonly maxInterval: number;
17
+ readonly subscriptionId: number;
18
+ isReading = false;
19
+
20
+ timeoutAt?: Timestamp;
21
+
22
+ constructor(config: PeerSubscription.Configuration) {
23
+ super(config);
24
+
25
+ const { subscriptionId, interactionModelRevision, maxInterval } = config.response;
26
+ this.subscriptionId = subscriptionId;
27
+ this.interactionModelRevision = interactionModelRevision;
28
+ this.maxInterval = maxInterval;
29
+ }
30
+
31
+ get timeout() {
32
+ return Millis(Seconds(this.maxInterval) + (this.request.maxPeerResponseTime ?? 0));
33
+ }
34
+
35
+ timedOut() {
36
+ this.logger.info(
37
+ "Subscription",
38
+ Diagnostic.strong(this.subscriptionId),
39
+ "timed out after",
40
+ Diagnostic.strong(Duration.format(this.timeout)),
41
+ );
42
+
43
+ this.close();
44
+ }
45
+ }
46
+
47
+ export namespace PeerSubscription {
48
+ export interface Configuration extends ClientSubscription.Configuration {
49
+ response: SubscribeResponse;
50
+ }
51
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Subscribe } from "#action/request/Subscribe.js";
8
+ import type { ActiveSubscription } from "#action/response/SubscribeResult.js";
9
+ import {
10
+ asError,
11
+ AsyncObservableValue,
12
+ Diagnostic,
13
+ Duration,
14
+ Hours,
15
+ ImplementationError,
16
+ Logger,
17
+ RetrySchedule,
18
+ Seconds,
19
+ Time,
20
+ } from "#general";
21
+ import { Specification } from "#model";
22
+ import { SubscribeResponse } from "#types";
23
+ import type { ClientSubscribe } from "./ClientSubscribe.js";
24
+ import { ClientSubscription } from "./ClientSubscription.js";
25
+ import { PeerSubscription } from "./PeerSubscription.js";
26
+
27
+ const logger = Logger.get("ClientSubscription");
28
+
29
+ /**
30
+ * An {@link ActiveSubscription} that remains active regardless of the state of the peer.
31
+ *
32
+ * This class performs retries in response to connection errors and timeouts. The underlying Matter subscription and
33
+ * thus {@link ActiveSubscription#subscriptionId} may change if the peer goes offline or experiences transient errors.
34
+ *
35
+ * TODO - need to make underlying exchange provider abortable and work out how the retry schedule at this level
36
+ * interacts with the MDNS and secure protocol retries. Will require some refactoring at lower levels. Leaving
37
+ * retries at this level relatively conservative for now
38
+ */
39
+ export class SustainedSubscription extends ClientSubscription {
40
+ #request: ClientSubscribe;
41
+ #subscription?: ActiveSubscription;
42
+ #retries: RetrySchedule;
43
+ #subscribe: (request: Subscribe) => Promise<PeerSubscription>;
44
+ #active = AsyncObservableValue(false);
45
+ #inactive = AsyncObservableValue(true);
46
+
47
+ constructor(config: SustainedSubscription.Configuration) {
48
+ super(config);
49
+
50
+ const { request, retries, subscribe } = config;
51
+
52
+ this.#request = request;
53
+ this.#retries = retries;
54
+ this.#subscribe = subscribe;
55
+ this.done = this.#run();
56
+ }
57
+
58
+ /**
59
+ * Emits when active state changes.
60
+ */
61
+ get active() {
62
+ return this.#active;
63
+ }
64
+
65
+ /**
66
+ * Emits when inactive state changes.
67
+ */
68
+ get inactive() {
69
+ return this.#inactive;
70
+ }
71
+
72
+ async #run() {
73
+ const updated = this.#request.updated?.bind(this.#request);
74
+
75
+ while (true) {
76
+ // Create request and promise that will inform us when the underlying subscription closes
77
+ const request = { ...this.#request, updated };
78
+ if (this.#request.updated) {
79
+ request.updated = this.#request.updated.bind(request);
80
+ }
81
+ const closed = new Promise<void>(resolve => {
82
+ request.closed = () => {
83
+ this.#subscription = undefined;
84
+ resolve();
85
+ };
86
+ });
87
+
88
+ // Subscribe
89
+ for (const retry of this.#retries) {
90
+ try {
91
+ this.#subscription = await this.#subscribe(request);
92
+ break;
93
+ } catch (e) {
94
+ if (this.abort.aborted) {
95
+ return;
96
+ }
97
+
98
+ logger.error(
99
+ `Failed to establish subscription to ${this.peer}, retry in ${Duration.format(retry)}:`,
100
+ Diagnostic.errorMessage(asError(e)),
101
+ );
102
+ }
103
+
104
+ const readyForRetry = Time.sleep("subscription retry", retry);
105
+ await this.abort.race(readyForRetry);
106
+ readyForRetry.cancel();
107
+ if (this.abort.aborted) {
108
+ break;
109
+ }
110
+ }
111
+
112
+ // Notify listeners of active subscription
113
+ await this.#inactive.emit(false);
114
+ await this.#active.emit(true);
115
+ if (this.abort.aborted) {
116
+ break;
117
+ }
118
+
119
+ // Wait for the subscription to close
120
+ await closed;
121
+
122
+ // Notify listeners of inactive subscription
123
+ await this.#active.emit(false);
124
+ await this.#inactive.emit(true);
125
+ if (this.abort.aborted) {
126
+ break;
127
+ }
128
+
129
+ // If aborted then we're done
130
+ if (this.abort.aborted) {
131
+ break;
132
+ }
133
+
134
+ // If we aren't aborted then we are here due to timeout
135
+ logger.error(`Replacing subscription to ${this.peer} due to timeout`);
136
+ }
137
+
138
+ // We only arrive here when closed
139
+ this.#request.closed?.();
140
+ }
141
+
142
+ get interactionModelRevision() {
143
+ return this.#subscription?.interactionModelRevision ?? Specification.INTERACTION_MODEL_REVISION;
144
+ }
145
+
146
+ get maxInterval() {
147
+ return this.#subscription?.maxInterval ?? Hours.one;
148
+ }
149
+
150
+ get subscriptionId() {
151
+ return this.#subscription?.subscriptionId ?? SustainedSubscription.NO_SUBSCRIPTION;
152
+ }
153
+ }
154
+
155
+ export namespace SustainedSubscription {
156
+ /**
157
+ * Configuration for {@link SustainedSubscription}.
158
+ */
159
+ export interface Configuration extends ClientSubscription.Configuration {
160
+ /**
161
+ * Function to establish underlying subscription.
162
+ */
163
+ subscribe: (request: Subscribe) => Promise<PeerSubscription>;
164
+
165
+ /**
166
+ * The schedule we use for retrying subscription connections.
167
+ *
168
+ * We handle reconnection separately at the exchange level. This retry schedule only applies to establishing a
169
+ * subscription once we have an active exchange. Exchange reconnection is handled by lower-level components.
170
+ */
171
+ retries: RetrySchedule;
172
+ }
173
+
174
+ export function assert(subscription: SubscribeResponse): asserts subscription is SustainedSubscription {
175
+ if (!(subscription instanceof SustainedSubscription)) {
176
+ throw new ImplementationError(`Non-sustained subscription provided where sustained subscription required`);
177
+ }
178
+ }
179
+
180
+ export const NO_SUBSCRIPTION = -1;
181
+
182
+ export const DefaultRetrySchedule: RetrySchedule.Configuration = {
183
+ // Protocol-level level happens at the exchange level and is faster; this is an application-level retry. Retry
184
+ // more slowly so we do not hammer devices that are experiencing transient errors
185
+ initialInterval: Seconds(15),
186
+
187
+ // Similarly, we have an exchange. If a device repeatedly fails to establish a subscription, give it plenty of
188
+ // time to recover. It's even possible our subscription attempt is invalid for some reason, in which case we
189
+ // an aggressive interval would be particularly bad form
190
+ maximumInterval: Hours(1),
191
+
192
+ // No timeout; we run until aborted
193
+ timeout: undefined,
194
+
195
+ backoffFactor: 2,
196
+
197
+ jitterFactor: 0.25,
198
+ };
199
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export * from "./ClientSubscribe.js";
8
+ export * from "./ClientSubscription.js";
9
+ export * from "./ClientSubscriptionHandler.js";
10
+ export * from "./ClientSubscriptions.js";
11
+ export * from "./PeerSubscription.js";
12
+ export * from "./SustainedSubscription.js";
@@ -10,7 +10,7 @@ import { Status, StatusResponseError } from "#types";
10
10
  export { SchemaImplementationError } from "#model";
11
11
 
12
12
  /**
13
- * Thrown due operational schema violations.
13
+ * Thrown due operational schema violation.
14
14
  */
15
15
  export class SchemaViolationError extends StatusResponseError {
16
16
  constructor(prefix: string, path: SchemaErrorPath, message: string, code: Status) {
@@ -80,6 +80,15 @@ export class ConstraintError extends ValidateError {
80
80
  }
81
81
  }
82
82
 
83
+ /**
84
+ * Thrown when a numeric value can't fit in an integer type.
85
+ */
86
+ export class IntegerRangeError extends ValidateError {
87
+ constructor(path: SchemaErrorPath, message: string) {
88
+ super(path, message, Status.ConstraintError);
89
+ }
90
+ }
91
+
83
92
  /**
84
93
  * Thrown when an enum value is not known based on Matter specification
85
94
  */
@@ -107,11 +116,7 @@ export class ConformanceError extends ValidateError {
107
116
  /**
108
117
  * Thrown when an enum value is not valid based on conformance definitions
109
118
  */
110
- export class EnumValueConformanceError extends ConformanceError {
111
- constructor(schema: Schema, path: SchemaErrorPath, message: string) {
112
- super(schema, path, message);
113
- }
114
- }
119
+ export class EnumValueConformanceError extends ConformanceError {}
115
120
 
116
121
  /**
117
122
  * Thrown for access attempts against a managed value that is no longer valid.
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ReadResult } from "#action/response/ReadResult.js";
8
- import { CanceledError, Duration, Seconds, TimeoutError, UINT16_MAX } from "#general";
8
+ import { Duration, Seconds, UINT16_MAX } from "#general";
9
9
  import { MalformedRequestError } from "./MalformedRequestError.js";
10
10
  import { Read } from "./Read.js";
11
11
 
@@ -29,7 +29,7 @@ export interface Subscribe extends Read {
29
29
  /**
30
30
  * Invoked when the subscription is no longer active.
31
31
  */
32
- closed?: (cause: CanceledError | TimeoutError) => void;
32
+ closed?: () => void;
33
33
  }
34
34
 
35
35
  export function Subscribe(options: Subscribe.Options, ...selectors: Read.Selector[]): Subscribe {
@@ -28,7 +28,7 @@ import type {
28
28
  * Iteration occurs in chunks for performance reasons. A chunk is an iterable of reports, one per output attribute or
29
29
  * event.
30
30
  */
31
- export interface ReadResult<Chunk = ReadResult.Chunk> extends AsyncIterable<ReadResult.Chunk> {}
31
+ export interface ReadResult<Chunk = ReadResult.Chunk> extends AsyncIterableIterator<ReadResult.Chunk> {}
32
32
 
33
33
  export namespace ReadResult {
34
34
  export type Chunk = Iterable<Report>;
@@ -4,10 +4,11 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { MaybePromise } from "#general";
7
8
  import { SubscribeResponse } from "#types";
8
9
 
9
10
  export type SubscribeResult = Promise<ActiveSubscription>;
10
11
 
11
12
  export interface ActiveSubscription extends SubscribeResponse {
12
- close(): void;
13
+ close(): MaybePromise<void>;
13
14
  }
@@ -65,11 +65,6 @@ export class ServerInteraction<SessionT extends InteractionSession = Interaction
65
65
  throw new NotImplementedError();
66
66
  }
67
67
 
68
- cancelSubscription(_id: number): void {
69
- // TODO
70
- throw new NotImplementedError();
71
- }
72
-
73
68
  write<T extends Write>(request: T, session: SessionT): WriteResult<T> {
74
69
  // TODO - validate request
75
70
 
@@ -34,7 +34,7 @@ export abstract class InboundFlow extends Flow {
34
34
  // Method to be used by main close() method to make sure all streams are correctly closed or cancelled
35
35
  this.#closeStreams = async (error?: unknown) => {
36
36
  if (writeController !== undefined) {
37
- if (error != undefined) {
37
+ if (error !== undefined) {
38
38
  // When this is called, we are either done successfully or failed, error the write controller in error case
39
39
  writeController.error(error);
40
40
  try {
@@ -4,7 +4,8 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Duration, Environment, Environmental, Logger, MaybePromise, Millis, Time, Timer } from "#general";
7
+ import type { ClientSubscriptionHandler } from "#action/client/subscription/ClientSubscriptionHandler.js";
8
+ import { Duration, Logger, MaybePromise, Millis, Time, Timer } from "#general";
8
9
  import { DecodedDataReport } from "#interaction/DecodedDataReport.js";
9
10
  import { MessageExchange } from "#protocol/MessageExchange.js";
10
11
  import { ProtocolHandler } from "#protocol/ProtocolHandler.js";
@@ -25,6 +26,8 @@ export interface RegisteredSubscription {
25
26
  * A simple protocol handler that handles exchanges starting with data reports.
26
27
  *
27
28
  * Incoming data reports must match to a subscription registered with {@link add} or the exchange is invalid.
29
+ *
30
+ * @deprecated new code uses {@link ClientSubscriptionHandler}
28
31
  */
29
32
  export class SubscriptionClient implements ProtocolHandler {
30
33
  readonly id = INTERACTION_PROTOCOL_ID;
@@ -32,14 +35,6 @@ export class SubscriptionClient implements ProtocolHandler {
32
35
  readonly #listeners = new Map<number, (dataReport: DecodedDataReport) => MaybePromise<void>>();
33
36
  readonly #timeouts = new Map<number, Timer>();
34
37
 
35
- constructor() {}
36
-
37
- static [Environmental.create](env: Environment) {
38
- const client = new SubscriptionClient();
39
- env.set(SubscriptionClient, client);
40
- return client;
41
- }
42
-
43
38
  /**
44
39
  * Register a subscription.
45
40
  */
@@ -687,7 +687,7 @@ export class MessageExchange {
687
687
  }
688
688
 
689
689
  get via() {
690
- if (this.session == undefined || !this.session.isSecure) {
690
+ if (this.session === undefined || !this.session.isSecure) {
691
691
  return this.channel.name; // already formatted as "via"
692
692
  }
693
693