@matter/protocol 0.12.4-alpha.0-20250217-b0bba5179 → 0.12.4-alpha.0-20250224-e0964a795

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 (219) hide show
  1. package/dist/cjs/action/client/ClientInteraction.d.ts +38 -0
  2. package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -0
  3. package/dist/cjs/action/client/ClientInteraction.js +91 -0
  4. package/dist/cjs/action/client/ClientInteraction.js.map +6 -0
  5. package/dist/cjs/action/client/index.d.ts +7 -0
  6. package/dist/cjs/action/client/index.d.ts.map +1 -0
  7. package/dist/cjs/action/client/index.js +24 -0
  8. package/dist/cjs/action/client/index.js.map +6 -0
  9. package/dist/cjs/action/index.d.ts +1 -0
  10. package/dist/cjs/action/index.d.ts.map +1 -1
  11. package/dist/cjs/action/index.js +1 -0
  12. package/dist/cjs/action/index.js.map +1 -1
  13. package/dist/cjs/interaction/DecodedDataReport.d.ts +15 -0
  14. package/dist/cjs/interaction/DecodedDataReport.d.ts.map +1 -0
  15. package/dist/cjs/interaction/DecodedDataReport.js +42 -0
  16. package/dist/cjs/interaction/DecodedDataReport.js.map +6 -0
  17. package/dist/cjs/interaction/InteractionClient.d.ts +13 -23
  18. package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
  19. package/dist/cjs/interaction/InteractionClient.js +99 -127
  20. package/dist/cjs/interaction/InteractionClient.js.map +1 -1
  21. package/dist/cjs/interaction/InteractionMessenger.d.ts +94 -1
  22. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  23. package/dist/cjs/interaction/InteractionMessenger.js +56 -37
  24. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  25. package/dist/cjs/interaction/InteractionServer.d.ts +4 -2
  26. package/dist/cjs/interaction/InteractionServer.d.ts.map +1 -1
  27. package/dist/cjs/interaction/InteractionServer.js +12 -4
  28. package/dist/cjs/interaction/InteractionServer.js.map +1 -1
  29. package/dist/cjs/interaction/SubscriptionClient.d.ts +38 -0
  30. package/dist/cjs/interaction/SubscriptionClient.d.ts.map +1 -0
  31. package/dist/cjs/interaction/SubscriptionClient.js +98 -0
  32. package/dist/cjs/interaction/SubscriptionClient.js.map +6 -0
  33. package/dist/cjs/interaction/index.d.ts +1 -0
  34. package/dist/cjs/interaction/index.d.ts.map +1 -1
  35. package/dist/cjs/interaction/index.js +1 -0
  36. package/dist/cjs/interaction/index.js.map +1 -1
  37. package/dist/cjs/peer/ControllerCommissioner.d.ts +2 -2
  38. package/dist/cjs/peer/ControllerCommissioner.d.ts.map +1 -1
  39. package/dist/cjs/peer/ControllerCommissioner.js +4 -3
  40. package/dist/cjs/peer/ControllerCommissioner.js.map +1 -1
  41. package/dist/cjs/peer/InteractionQueue.d.ts +11 -0
  42. package/dist/cjs/peer/InteractionQueue.d.ts.map +1 -0
  43. package/dist/cjs/peer/InteractionQueue.js +42 -0
  44. package/dist/cjs/peer/InteractionQueue.js.map +6 -0
  45. package/dist/cjs/peer/PeerAddressStore.d.ts +1 -1
  46. package/dist/cjs/peer/PeerAddressStore.d.ts.map +1 -1
  47. package/dist/cjs/peer/PeerSet.d.ts +16 -7
  48. package/dist/cjs/peer/PeerSet.d.ts.map +1 -1
  49. package/dist/cjs/peer/PeerSet.js +58 -61
  50. package/dist/cjs/peer/PeerSet.js.map +1 -1
  51. package/dist/cjs/peer/PhysicalDeviceProperties.d.ts +26 -0
  52. package/dist/cjs/peer/PhysicalDeviceProperties.d.ts.map +1 -0
  53. package/dist/cjs/peer/PhysicalDeviceProperties.js +74 -0
  54. package/dist/cjs/peer/PhysicalDeviceProperties.js.map +6 -0
  55. package/dist/cjs/peer/index.d.ts +1 -0
  56. package/dist/cjs/peer/index.d.ts.map +1 -1
  57. package/dist/cjs/peer/index.js +1 -0
  58. package/dist/cjs/peer/index.js.map +1 -1
  59. package/dist/cjs/protocol/ExchangeManager.d.ts +1 -0
  60. package/dist/cjs/protocol/ExchangeManager.d.ts.map +1 -1
  61. package/dist/cjs/protocol/ExchangeManager.js +18 -11
  62. package/dist/cjs/protocol/ExchangeManager.js.map +1 -1
  63. package/dist/cjs/protocol/ExchangeProvider.d.ts +13 -1
  64. package/dist/cjs/protocol/ExchangeProvider.d.ts.map +1 -1
  65. package/dist/cjs/protocol/ExchangeProvider.js +2 -0
  66. package/dist/cjs/protocol/ExchangeProvider.js.map +1 -1
  67. package/dist/cjs/protocol/ProtocolHandler.d.ts +1 -1
  68. package/dist/cjs/protocol/ProtocolHandler.d.ts.map +1 -1
  69. package/dist/cjs/securechannel/SecureChannelProtocol.d.ts +1 -1
  70. package/dist/cjs/securechannel/SecureChannelProtocol.d.ts.map +1 -1
  71. package/dist/cjs/securechannel/SecureChannelProtocol.js +1 -3
  72. package/dist/cjs/securechannel/SecureChannelProtocol.js.map +1 -1
  73. package/dist/cjs/session/InsecureSession.d.ts.map +1 -1
  74. package/dist/cjs/session/InsecureSession.js +1 -0
  75. package/dist/cjs/session/InsecureSession.js.map +1 -1
  76. package/dist/cjs/session/SecureSession.d.ts.map +1 -1
  77. package/dist/cjs/session/SecureSession.js +4 -0
  78. package/dist/cjs/session/SecureSession.js.map +1 -1
  79. package/dist/cjs/session/Session.d.ts +3 -1
  80. package/dist/cjs/session/Session.d.ts.map +1 -1
  81. package/dist/cjs/session/Session.js +5 -1
  82. package/dist/cjs/session/Session.js.map +1 -1
  83. package/dist/cjs/session/SessionManager.d.ts.map +1 -1
  84. package/dist/cjs/session/SessionManager.js +1 -0
  85. package/dist/cjs/session/SessionManager.js.map +1 -1
  86. package/dist/cjs/session/case/CaseClient.d.ts.map +1 -1
  87. package/dist/cjs/session/case/CaseClient.js +22 -15
  88. package/dist/cjs/session/case/CaseClient.js.map +1 -1
  89. package/dist/cjs/session/case/CaseServer.d.ts +1 -1
  90. package/dist/cjs/session/case/CaseServer.d.ts.map +1 -1
  91. package/dist/cjs/session/case/CaseServer.js +2 -4
  92. package/dist/cjs/session/case/CaseServer.js.map +1 -1
  93. package/dist/cjs/session/pase/PaseServer.d.ts +2 -3
  94. package/dist/cjs/session/pase/PaseServer.d.ts.map +1 -1
  95. package/dist/cjs/session/pase/PaseServer.js +12 -14
  96. package/dist/cjs/session/pase/PaseServer.js.map +1 -1
  97. package/dist/esm/action/client/ClientInteraction.d.ts +38 -0
  98. package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -0
  99. package/dist/esm/action/client/ClientInteraction.js +71 -0
  100. package/dist/esm/action/client/ClientInteraction.js.map +6 -0
  101. package/dist/esm/action/client/index.d.ts +7 -0
  102. package/dist/esm/action/client/index.d.ts.map +1 -0
  103. package/dist/esm/action/client/index.js +7 -0
  104. package/dist/esm/action/client/index.js.map +6 -0
  105. package/dist/esm/action/index.d.ts +1 -0
  106. package/dist/esm/action/index.d.ts.map +1 -1
  107. package/dist/esm/action/index.js +1 -0
  108. package/dist/esm/action/index.js.map +1 -1
  109. package/dist/esm/interaction/DecodedDataReport.d.ts +15 -0
  110. package/dist/esm/interaction/DecodedDataReport.d.ts.map +1 -0
  111. package/dist/esm/interaction/DecodedDataReport.js +22 -0
  112. package/dist/esm/interaction/DecodedDataReport.js.map +6 -0
  113. package/dist/esm/interaction/InteractionClient.d.ts +13 -23
  114. package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
  115. package/dist/esm/interaction/InteractionClient.js +101 -134
  116. package/dist/esm/interaction/InteractionClient.js.map +1 -1
  117. package/dist/esm/interaction/InteractionMessenger.d.ts +94 -1
  118. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  119. package/dist/esm/interaction/InteractionMessenger.js +56 -37
  120. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  121. package/dist/esm/interaction/InteractionServer.d.ts +4 -2
  122. package/dist/esm/interaction/InteractionServer.d.ts.map +1 -1
  123. package/dist/esm/interaction/InteractionServer.js +12 -4
  124. package/dist/esm/interaction/InteractionServer.js.map +1 -1
  125. package/dist/esm/interaction/SubscriptionClient.d.ts +38 -0
  126. package/dist/esm/interaction/SubscriptionClient.d.ts.map +1 -0
  127. package/dist/esm/interaction/SubscriptionClient.js +78 -0
  128. package/dist/esm/interaction/SubscriptionClient.js.map +6 -0
  129. package/dist/esm/interaction/index.d.ts +1 -0
  130. package/dist/esm/interaction/index.d.ts.map +1 -1
  131. package/dist/esm/interaction/index.js +1 -0
  132. package/dist/esm/interaction/index.js.map +1 -1
  133. package/dist/esm/peer/ControllerCommissioner.d.ts +2 -2
  134. package/dist/esm/peer/ControllerCommissioner.d.ts.map +1 -1
  135. package/dist/esm/peer/ControllerCommissioner.js +6 -5
  136. package/dist/esm/peer/ControllerCommissioner.js.map +1 -1
  137. package/dist/esm/peer/InteractionQueue.d.ts +11 -0
  138. package/dist/esm/peer/InteractionQueue.d.ts.map +1 -0
  139. package/dist/esm/peer/InteractionQueue.js +22 -0
  140. package/dist/esm/peer/InteractionQueue.js.map +6 -0
  141. package/dist/esm/peer/PeerAddressStore.d.ts +1 -1
  142. package/dist/esm/peer/PeerAddressStore.d.ts.map +1 -1
  143. package/dist/esm/peer/PeerSet.d.ts +16 -7
  144. package/dist/esm/peer/PeerSet.d.ts.map +1 -1
  145. package/dist/esm/peer/PeerSet.js +59 -62
  146. package/dist/esm/peer/PeerSet.js.map +1 -1
  147. package/dist/esm/peer/PhysicalDeviceProperties.d.ts +26 -0
  148. package/dist/esm/peer/PhysicalDeviceProperties.d.ts.map +1 -0
  149. package/dist/esm/peer/PhysicalDeviceProperties.js +54 -0
  150. package/dist/esm/peer/PhysicalDeviceProperties.js.map +6 -0
  151. package/dist/esm/peer/index.d.ts +1 -0
  152. package/dist/esm/peer/index.d.ts.map +1 -1
  153. package/dist/esm/peer/index.js +1 -0
  154. package/dist/esm/peer/index.js.map +1 -1
  155. package/dist/esm/protocol/ExchangeManager.d.ts +1 -0
  156. package/dist/esm/protocol/ExchangeManager.d.ts.map +1 -1
  157. package/dist/esm/protocol/ExchangeManager.js +18 -11
  158. package/dist/esm/protocol/ExchangeManager.js.map +1 -1
  159. package/dist/esm/protocol/ExchangeProvider.d.ts +13 -1
  160. package/dist/esm/protocol/ExchangeProvider.d.ts.map +1 -1
  161. package/dist/esm/protocol/ExchangeProvider.js +2 -0
  162. package/dist/esm/protocol/ExchangeProvider.js.map +1 -1
  163. package/dist/esm/protocol/ProtocolHandler.d.ts +1 -1
  164. package/dist/esm/protocol/ProtocolHandler.d.ts.map +1 -1
  165. package/dist/esm/securechannel/SecureChannelProtocol.d.ts +1 -1
  166. package/dist/esm/securechannel/SecureChannelProtocol.d.ts.map +1 -1
  167. package/dist/esm/securechannel/SecureChannelProtocol.js +1 -3
  168. package/dist/esm/securechannel/SecureChannelProtocol.js.map +1 -1
  169. package/dist/esm/session/InsecureSession.d.ts.map +1 -1
  170. package/dist/esm/session/InsecureSession.js +1 -0
  171. package/dist/esm/session/InsecureSession.js.map +1 -1
  172. package/dist/esm/session/SecureSession.d.ts.map +1 -1
  173. package/dist/esm/session/SecureSession.js +4 -0
  174. package/dist/esm/session/SecureSession.js.map +1 -1
  175. package/dist/esm/session/Session.d.ts +3 -1
  176. package/dist/esm/session/Session.d.ts.map +1 -1
  177. package/dist/esm/session/Session.js +6 -2
  178. package/dist/esm/session/Session.js.map +1 -1
  179. package/dist/esm/session/SessionManager.d.ts.map +1 -1
  180. package/dist/esm/session/SessionManager.js +1 -0
  181. package/dist/esm/session/SessionManager.js.map +1 -1
  182. package/dist/esm/session/case/CaseClient.d.ts.map +1 -1
  183. package/dist/esm/session/case/CaseClient.js +22 -15
  184. package/dist/esm/session/case/CaseClient.js.map +1 -1
  185. package/dist/esm/session/case/CaseServer.d.ts +1 -1
  186. package/dist/esm/session/case/CaseServer.d.ts.map +1 -1
  187. package/dist/esm/session/case/CaseServer.js +2 -4
  188. package/dist/esm/session/case/CaseServer.js.map +1 -1
  189. package/dist/esm/session/pase/PaseServer.d.ts +2 -3
  190. package/dist/esm/session/pase/PaseServer.d.ts.map +1 -1
  191. package/dist/esm/session/pase/PaseServer.js +12 -14
  192. package/dist/esm/session/pase/PaseServer.js.map +1 -1
  193. package/package.json +6 -6
  194. package/src/action/client/ClientInteraction.ts +110 -0
  195. package/src/action/client/index.ts +7 -0
  196. package/src/action/index.ts +1 -0
  197. package/src/interaction/DecodedDataReport.ts +29 -0
  198. package/src/interaction/InteractionClient.ts +112 -164
  199. package/src/interaction/InteractionMessenger.ts +63 -43
  200. package/src/interaction/InteractionServer.ts +18 -5
  201. package/src/interaction/SubscriptionClient.ts +107 -0
  202. package/src/interaction/index.ts +1 -0
  203. package/src/peer/ControllerCommissioner.ts +7 -6
  204. package/src/peer/InteractionQueue.ts +22 -0
  205. package/src/peer/PeerAddressStore.ts +1 -1
  206. package/src/peer/PeerSet.ts +69 -76
  207. package/src/peer/PhysicalDeviceProperties.ts +80 -0
  208. package/src/peer/index.ts +1 -0
  209. package/src/protocol/ExchangeManager.ts +19 -11
  210. package/src/protocol/ExchangeProvider.ts +14 -1
  211. package/src/protocol/ProtocolHandler.ts +1 -1
  212. package/src/securechannel/SecureChannelProtocol.ts +1 -3
  213. package/src/session/InsecureSession.ts +1 -0
  214. package/src/session/SecureSession.ts +4 -0
  215. package/src/session/Session.ts +7 -2
  216. package/src/session/SessionManager.ts +1 -0
  217. package/src/session/case/CaseClient.ts +18 -12
  218. package/src/session/case/CaseServer.ts +3 -5
  219. package/src/session/pase/PaseServer.ts +13 -15
@@ -227,9 +227,11 @@ export interface InteractionContext {
227
227
  * Translates interactions from the Matter protocol to matter.js APIs.
228
228
  */
229
229
  export class InteractionServer implements ProtocolHandler, InteractionRecipient {
230
+ readonly id = INTERACTION_PROTOCOL_ID;
230
231
  #context: InteractionContext;
231
232
  #nextSubscriptionId = Crypto.getRandomUInt32();
232
233
  #isClosing = false;
234
+ #clientHandler?: ProtocolHandler;
233
235
  readonly #subscriptionConfig: ServerSubscriptionConfig;
234
236
  readonly #maxPathsPerInvoke;
235
237
 
@@ -244,10 +246,6 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
244
246
  });
245
247
  }
246
248
 
247
- getId() {
248
- return INTERACTION_PROTOCOL_ID;
249
- }
250
-
251
249
  protected get isClosing() {
252
250
  return this.#isClosing;
253
251
  }
@@ -256,13 +254,28 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
256
254
  return this.#maxPathsPerInvoke;
257
255
  }
258
256
 
259
- async onNewExchange(exchange: MessageExchange) {
257
+ async onNewExchange(exchange: MessageExchange, message: Message) {
260
258
  // Note - changes here must be copied to TransactionalInteractionServer as it does not call super() to avoid
261
259
  // the stack frame
262
260
  if (this.#isClosing) return; // We are closing, ignore anything newly incoming
261
+
262
+ // An incoming data report as the first message is not a valid server operation. We instead delegate to a
263
+ // client implementation if available
264
+ if (message.payloadHeader.messageType === MessageType.SubscribeRequest && this.#clientHandler) {
265
+ return this.#clientHandler.onNewExchange(exchange, message);
266
+ }
267
+
263
268
  await new InteractionServerMessenger(exchange).handleRequest(this);
264
269
  }
265
270
 
271
+ get clientHandler(): ProtocolHandler | undefined {
272
+ return this.#clientHandler;
273
+ }
274
+
275
+ set clientHandler(clientHandler: ProtocolHandler) {
276
+ this.#clientHandler = clientHandler;
277
+ }
278
+
266
279
  async #collectEventDataForRead(
267
280
  { eventRequests, eventFilters, isFabricFiltered }: ReadRequest,
268
281
  exchange: MessageExchange,
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { MessageExchange } from "#protocol/MessageExchange.js";
8
+ import { ProtocolHandler } from "#protocol/ProtocolHandler.js";
9
+ import { Environment, Environmental, Logger, MaybePromise, Time, Timer } from "@matter/general";
10
+ import { INTERACTION_PROTOCOL_ID } from "@matter/types";
11
+ import { DataReport, IncomingInteractionClientMessenger } from "./InteractionMessenger.js";
12
+
13
+ const logger = Logger.get("SubscriptionClient");
14
+
15
+ export interface RegisteredSubscription {
16
+ id: number;
17
+ maximumPeerResponseTime: number;
18
+ maxIntervalS: number;
19
+ onData: (dataReport: DataReport) => MaybePromise<void>;
20
+ onTimeout?: () => void;
21
+ }
22
+
23
+ /**
24
+ * A simple protocol handler that handles exchanges starting with data reports.
25
+ *
26
+ * Incoming data reports must match to a subscription registered with {@link add} or the exchange is invalid.
27
+ */
28
+ export class SubscriptionClient implements ProtocolHandler {
29
+ readonly #listeners = new Map<number, (dataReport: DataReport) => MaybePromise<void>>();
30
+ readonly #timeouts = new Map<number, Timer>();
31
+
32
+ constructor() {}
33
+
34
+ static [Environmental.create](env: Environment) {
35
+ const client = new SubscriptionClient();
36
+ env.set(SubscriptionClient, client);
37
+ return client;
38
+ }
39
+
40
+ readonly id = INTERACTION_PROTOCOL_ID;
41
+
42
+ /**
43
+ * Register a subscription.
44
+ */
45
+ add(subscription: RegisteredSubscription) {
46
+ const { id, onData, onTimeout } = subscription;
47
+
48
+ this.#listeners.set(id, onData);
49
+ if (onTimeout) {
50
+ let timer = this.#timeouts.get(id);
51
+ if (timer !== undefined) {
52
+ timer.stop();
53
+ this.#timeouts.delete(id);
54
+ }
55
+
56
+ const maxIntervalMs = subscription.maxIntervalS * 1000 + subscription.maximumPeerResponseTime;
57
+
58
+ timer = Time.getTimer("Subscription timeout", maxIntervalMs, () => {
59
+ logger.info(`Subscription ${id} timed out after ${maxIntervalMs}ms`);
60
+ this.delete(id);
61
+ onTimeout();
62
+ }).start();
63
+
64
+ this.#timeouts.set(id, timer);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Unregister a subscription.
70
+ */
71
+ delete(id: number) {
72
+ this.#listeners.delete(id);
73
+ const timer = this.#timeouts.get(id);
74
+ if (timer !== undefined) {
75
+ timer.stop();
76
+ this.#timeouts.delete(id);
77
+ }
78
+ }
79
+
80
+ async onNewExchange(exchange: MessageExchange) {
81
+ const messenger = new IncomingInteractionClientMessenger(exchange);
82
+
83
+ let dataReport: DataReport;
84
+ try {
85
+ // TODO Adjust this to getting packages as callback when received to handle error cases and checks outside
86
+ dataReport = await messenger.readAggregateDataReport([...this.#listeners.keys()]);
87
+ } finally {
88
+ messenger.close().catch(error => logger.info("Error closing client messenger", error));
89
+ }
90
+ const subscriptionId = dataReport.subscriptionId as number; // this is checked in the messenger already because we hand over allowed list
91
+
92
+ const listener = this.#listeners.get(subscriptionId);
93
+ const timer = this.#timeouts.get(subscriptionId);
94
+
95
+ if (timer !== undefined) {
96
+ timer.stop().start(); // Restart timer because we received data
97
+ }
98
+
99
+ await listener?.(dataReport);
100
+ }
101
+
102
+ async close() {
103
+ this.#listeners.clear();
104
+ this.#timeouts.forEach(timer => timer.stop());
105
+ this.#timeouts.clear();
106
+ }
107
+ }
@@ -14,3 +14,4 @@ export * from "./InteractionMessenger.js";
14
14
  export * from "./InteractionServer.js";
15
15
  export * from "./ServerSubscription.js";
16
16
  export * from "./Subscription.js";
17
+ export * from "./SubscriptionClient.js";
@@ -27,11 +27,11 @@ import { ChannelStatusResponseError } from "#securechannel/index.js";
27
27
  import { PaseClient } from "#session/index.js";
28
28
  import { SessionManager } from "#session/SessionManager.js";
29
29
  import { DiscoveryCapabilitiesBitmap, NodeId, SECURE_CHANNEL_PROTOCOL_ID, TypeFromPartialBitSchema } from "#types";
30
- import { InteractionClient } from "../interaction/InteractionClient.js";
30
+ import { InteractionClient, InteractionClientProvider } from "../interaction/InteractionClient.js";
31
31
  import { ExchangeManager, MessageChannel } from "../protocol/ExchangeManager.js";
32
32
  import { DedicatedChannelExchangeProvider } from "../protocol/ExchangeProvider.js";
33
33
  import { PeerAddress } from "./PeerAddress.js";
34
- import { NodeDiscoveryType, PeerSet } from "./PeerSet.js";
34
+ import { NodeDiscoveryType } from "./PeerSet.js";
35
35
 
36
36
  const logger = Logger.get("PeerCommissioner");
37
37
 
@@ -108,7 +108,7 @@ export interface DiscoveryAndCommissioningOptions extends CommissioningOptions {
108
108
  * Interfaces {@link ControllerCommissioner} with other components.
109
109
  */
110
110
  export interface ControllerCommissionerContext {
111
- peers: PeerSet;
111
+ clients: InteractionClientProvider;
112
112
  scanners: ScannerSet;
113
113
  netInterfaces: NetInterfaceSet;
114
114
  sessions: SessionManager;
@@ -130,7 +130,7 @@ export class ControllerCommissioner {
130
130
 
131
131
  static [Environmental.create](env: Environment) {
132
132
  const instance = new ControllerCommissioner({
133
- peers: env.get(PeerSet),
133
+ clients: env.get(InteractionClientProvider),
134
134
  scanners: env.get(ScannerSet),
135
135
  netInterfaces: env.get(NetInterfaceSet),
136
136
  sessions: env.get(SessionManager),
@@ -385,6 +385,7 @@ export class ControllerCommissioner {
385
385
  // Use the created secure session to do the commissioning
386
386
  new InteractionClient(
387
387
  new DedicatedChannelExchangeProvider(this.#context.exchanges, paseSecureMessageChannel),
388
+ this.#context.clients.peers.subscriptionClient,
388
389
  address,
389
390
  ),
390
391
  this.#context.ca,
@@ -406,7 +407,7 @@ export class ControllerCommissioner {
406
407
  }
407
408
 
408
409
  // Look for the device broadcast over MDNS and do CASE pairing
409
- return await this.#context.peers.connect(
410
+ return await this.#context.clients.connect(
410
411
  address,
411
412
  {
412
413
  discoveryType: NodeDiscoveryType.TimedDiscovery,
@@ -422,7 +423,7 @@ export class ControllerCommissioner {
422
423
  await commissioningManager.executeCommissioning();
423
424
  } catch (error) {
424
425
  // We might have added data for an operational address that we need to cleanup
425
- await this.#context.peers.delete(address);
426
+ await this.#context.clients.peers.delete(address);
426
427
  throw error;
427
428
  } finally {
428
429
  if (!paseSecureMessageChannel.closed) {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Environment, Environmental, PromiseQueue } from "@matter/general";
8
+
9
+ const CONCURRENT_QUEUED_INTERACTIONS = 4;
10
+ const INTERACTION_QUEUE_DELAY_MS = 100;
11
+
12
+ export class InteractionQueue extends PromiseQueue {
13
+ constructor() {
14
+ super(CONCURRENT_QUEUED_INTERACTIONS, INTERACTION_QUEUE_DELAY_MS);
15
+ }
16
+
17
+ static [Environmental.create](env: Environment) {
18
+ const instance = new InteractionQueue();
19
+ env.set(InteractionQueue, instance);
20
+ return instance;
21
+ }
22
+ }
@@ -18,7 +18,7 @@ export abstract class PeerAddressStore {
18
18
  abstract loadPeers(): MaybePromise<Iterable<OperationalPeer>>;
19
19
  abstract updatePeer(peer: OperationalPeer): MaybePromise<void>;
20
20
  abstract deletePeer(address: PeerAddress): MaybePromise<void>;
21
- abstract createNodeStore(address: PeerAddress): Promise<PeerDataStore>;
21
+ abstract createNodeStore(address: PeerAddress): MaybePromise<PeerDataStore | undefined>;
22
22
  }
23
23
 
24
24
  export abstract class PeerDataStore {
@@ -7,6 +7,7 @@
7
7
  import { DiscoveryData, ScannerSet } from "#common/Scanner.js";
8
8
  import {
9
9
  anyPromise,
10
+ AsyncObservable,
10
11
  BasicSet,
11
12
  ChannelType,
12
13
  Construction,
@@ -22,13 +23,12 @@ import {
22
23
  NetInterfaceSet,
23
24
  NoResponseTimeoutError,
24
25
  ObservableSet,
25
- PromiseQueue,
26
26
  ServerAddressIp,
27
27
  serverAddressToString,
28
28
  Time,
29
29
  Timer,
30
30
  } from "#general";
31
- import { InteractionClient } from "#interaction/InteractionClient.js";
31
+ import { SubscriptionClient } from "#interaction/SubscriptionClient.js";
32
32
  import { MdnsScanner } from "#mdns/MdnsScanner.js";
33
33
  import { PeerAddress, PeerAddressMap } from "#peer/PeerAddress.js";
34
34
  import { CaseClient, SecureSession, Session } from "#session/index.js";
@@ -39,6 +39,7 @@ import { ChannelNotConnectedError, ExchangeManager, MessageChannel } from "../pr
39
39
  import { ReconnectableExchangeProvider } from "../protocol/ExchangeProvider.js";
40
40
  import { RetransmissionLimitReachedError } from "../protocol/MessageExchange.js";
41
41
  import { ControllerDiscovery, DiscoveryError, PairRetransmissionLimitReachedError } from "./ControllerDiscovery.js";
42
+ import { InteractionQueue } from "./InteractionQueue.js";
42
43
  import { OperationalPeer } from "./OperationalPeer.js";
43
44
  import { PeerAddressStore, PeerDataStore } from "./PeerAddressStore.js";
44
45
 
@@ -47,9 +48,6 @@ const logger = Logger.get("PeerSet");
47
48
  const RECONNECTION_POLLING_INTERVAL_MS = 600_000; // 10 minutes
48
49
  const RETRANSMISSION_DISCOVERY_TIMEOUT_S = 5;
49
50
 
50
- const CONCURRENT_QUEUED_INTERACTIONS = 4;
51
- const INTERACTION_QUEUE_DELAY_MS = 100;
52
-
53
51
  /**
54
52
  * Types of discovery that may be performed when connecting operationally.
55
53
  */
@@ -93,6 +91,7 @@ export interface PeerSetContext {
93
91
  sessions: SessionManager;
94
92
  channels: ChannelManager;
95
93
  exchanges: ExchangeManager;
94
+ subscriptionClient: SubscriptionClient;
96
95
  scanners: ScannerSet;
97
96
  netInterfaces: NetInterfaceSet;
98
97
  store: PeerAddressStore;
@@ -105,6 +104,7 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
105
104
  readonly #sessions: SessionManager;
106
105
  readonly #channels: ChannelManager;
107
106
  readonly #exchanges: ExchangeManager;
107
+ readonly #subscriptionClient: SubscriptionClient;
108
108
  readonly #scanners: ScannerSet;
109
109
  readonly #netInterfaces: NetInterfaceSet;
110
110
  readonly #caseClient: CaseClient;
@@ -117,16 +117,17 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
117
117
  }>();
118
118
  readonly #construction: Construction<PeerSet>;
119
119
  readonly #store: PeerAddressStore;
120
- readonly #interactionQueue = new PromiseQueue(CONCURRENT_QUEUED_INTERACTIONS, INTERACTION_QUEUE_DELAY_MS);
120
+ readonly #interactionQueue = new InteractionQueue();
121
121
  readonly #nodeCachedData = new PeerAddressMap<PeerDataStore>(); // Temporarily until we store it in new API
122
- readonly #clients = new PeerAddressMap<InteractionClient>();
122
+ readonly #disconnected = AsyncObservable<[address: PeerAddress]>();
123
123
 
124
124
  constructor(context: PeerSetContext) {
125
- const { sessions, channels, exchanges, scanners, netInterfaces, store } = context;
125
+ const { sessions, channels, exchanges, subscriptionClient, scanners, netInterfaces, store } = context;
126
126
 
127
127
  this.#sessions = sessions;
128
128
  this.#channels = channels;
129
129
  this.#exchanges = exchanges;
130
+ this.#subscriptionClient = subscriptionClient;
130
131
  this.#scanners = scanners;
131
132
  this.#netInterfaces = netInterfaces;
132
133
  this.#store = store;
@@ -165,6 +166,10 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
165
166
  return this.#peers.deleted;
166
167
  }
167
168
 
169
+ get disconnected() {
170
+ return this.#disconnected;
171
+ }
172
+
168
173
  has(item: PeerAddress | OperationalPeer) {
169
174
  if ("address" in item) {
170
175
  return this.#peers.has(item);
@@ -201,6 +206,7 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
201
206
  sessions: env.get(SessionManager),
202
207
  channels: env.get(ChannelManager),
203
208
  exchanges: env.get(ExchangeManager),
209
+ subscriptionClient: env.get(SubscriptionClient),
204
210
  scanners: env.get(ScannerSet),
205
211
  netInterfaces: env.get(NetInterfaceSet),
206
212
  store: env.get(PeerAddressStore),
@@ -213,20 +219,18 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
213
219
  return this.#peers;
214
220
  }
215
221
 
216
- /**
217
- * Connect to a node on a fabric.
218
- */
219
- async connect(
220
- address: PeerAddress,
221
- discoveryOptions: DiscoveryOptions,
222
- allowUnknownPeer = false,
223
- ): Promise<InteractionClient> {
224
- await this.#ensureConnection(address, discoveryOptions, allowUnknownPeer);
222
+ get subscriptionClient() {
223
+ return this.#subscriptionClient;
224
+ }
225
225
 
226
- return this.initializeInteractionClient(address, discoveryOptions);
226
+ get interactionQueue() {
227
+ return this.#interactionQueue;
227
228
  }
228
229
 
229
- async #ensureConnection(address: PeerAddress, discoveryOptions: DiscoveryOptions, allowUnknownPeer = false) {
230
+ /**
231
+ * Ensure there is a channel to the designated peer.
232
+ */
233
+ async ensureConnection(address: PeerAddress, discoveryOptions: DiscoveryOptions, allowUnknownPeer = false) {
230
234
  if (!this.#peersByAddress.has(address) && !allowUnknownPeer) {
231
235
  throw new UnknownNodeError(`Cannot connect to unknown device ${PeerAddress(address)}`);
232
236
  }
@@ -256,62 +260,44 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
256
260
  }
257
261
  }
258
262
 
259
- async initializeInteractionClient(address: PeerAddress, discoveryOptions?: DiscoveryOptions) {
260
- const existingClient = this.#clients.get(address);
261
- if (existingClient !== undefined) {
262
- return existingClient;
263
- }
264
-
265
- const nodeStore = this.get(address)?.dataStore;
266
- await nodeStore?.construction; // Lazy initialize the data if not already done
267
-
263
+ /**
264
+ * Obtain an exchange provider for the designated peer.
265
+ */
266
+ async exchangeProviderFor(address: PeerAddress, discoveryOptions?: DiscoveryOptions) {
268
267
  let initiallyConnected = this.#channels.hasChannel(address);
269
- const client = new InteractionClient(
270
- new ReconnectableExchangeProvider(this.#exchanges, this.#channels, address, async () => {
271
- if (!initiallyConnected && !this.#channels.hasChannel(address)) {
272
- // We got an uninitialized node, so do the first connection as usual
273
- await this.#ensureConnection(address, { discoveryType: NodeDiscoveryType.None });
274
- initiallyConnected = true; // We only do this connection once, rest is handled in following code
275
- if (this.#channels.hasChannel(address)) {
276
- return;
277
- }
268
+ return new ReconnectableExchangeProvider(this.#exchanges, this.#channels, address, async () => {
269
+ if (!initiallyConnected && !this.#channels.hasChannel(address)) {
270
+ // We got an uninitialized node, so do the first connection as usual
271
+ await this.ensureConnection(address, { discoveryType: NodeDiscoveryType.None });
272
+ initiallyConnected = true; // We only do this connection once, rest is handled in following code
273
+ if (this.#channels.hasChannel(address)) {
274
+ return;
278
275
  }
276
+ }
279
277
 
280
- if (!this.#channels.hasChannel(address)) {
281
- throw new RetransmissionLimitReachedError(
282
- `Device ${PeerAddress(address)} is currently not reachable.`,
283
- );
284
- }
285
- await this.#channels.removeAllNodeChannels(address);
278
+ if (!this.#channels.hasChannel(address)) {
279
+ throw new RetransmissionLimitReachedError(`Device ${PeerAddress(address)} is currently not reachable.`);
280
+ }
281
+ await this.#channels.removeAllNodeChannels(address);
286
282
 
287
- // Enrich discoveryData with data from the node store when not provided
288
- const { discoveryData } = discoveryOptions ?? {
289
- discoveryData: this.#peersByAddress.get(address)?.discoveryData,
290
- };
291
- // Try to use first result for one last try before we need to reconnect
292
- const operationalAddress = this.#knownOperationalAddressFor(address, true);
293
- if (operationalAddress === undefined) {
294
- logger.info(
295
- `Re-discovering device failed (no address found), remove all sessions for ${PeerAddress(address)}`,
296
- );
297
- // We remove all sessions, this also informs the PairedNode class
298
- await this.#sessions.removeAllSessionsForNode(address);
299
- throw new RetransmissionLimitReachedError(
300
- `No operational address found for ${PeerAddress(address)}`,
301
- );
302
- }
303
- if (
304
- (await this.#reconnectKnownAddress(address, operationalAddress, discoveryData, 2_000)) === undefined
305
- ) {
306
- throw new RetransmissionLimitReachedError(`${PeerAddress(address)} is not reachable.`);
307
- }
308
- }),
309
- address,
310
- this.#interactionQueue,
311
- nodeStore,
312
- );
313
- this.#clients.set(address, client);
314
- return client;
283
+ // Enrich discoveryData with data from the node store when not provided
284
+ const { discoveryData } = discoveryOptions ?? {
285
+ discoveryData: this.#peersByAddress.get(address)?.discoveryData,
286
+ };
287
+ // Try to use first result for one last try before we need to reconnect
288
+ const operationalAddress = this.#knownOperationalAddressFor(address, true);
289
+ if (operationalAddress === undefined) {
290
+ logger.info(
291
+ `Re-discovering device failed (no address found), remove all sessions for ${PeerAddress(address)}`,
292
+ );
293
+ // We remove all sessions, this also informs the PairedNode class
294
+ await this.#sessions.removeAllSessionsForNode(address);
295
+ throw new RetransmissionLimitReachedError(`No operational address found for ${PeerAddress(address)}`);
296
+ }
297
+ if ((await this.#reconnectKnownAddress(address, operationalAddress, discoveryData, 2_000)) === undefined) {
298
+ throw new RetransmissionLimitReachedError(`${PeerAddress(address)} is not reachable.`);
299
+ }
300
+ });
315
301
  }
316
302
 
317
303
  /**
@@ -327,10 +313,15 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
327
313
  /**
328
314
  * Terminate any active peer connection.
329
315
  */
330
- async disconnect(address: PeerAddress, sendSessionClose = true) {
331
- this.#clients.get(address)?.removeAllSubscriptions();
316
+ async disconnect(peer: PeerAddress | OperationalPeer, sendSessionClose = true) {
317
+ const address = this.get(peer)?.address;
318
+ if (address === undefined) {
319
+ return;
320
+ }
321
+
332
322
  await this.#sessions.removeAllSessionsForNode(address, sendSessionClose);
333
323
  await this.#channels.removeAllNodeChannels(address);
324
+ await this.#disconnected.emit(address);
334
325
  }
335
326
 
336
327
  /**
@@ -348,7 +339,6 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
348
339
  await this.#store.deletePeer(address);
349
340
  await this.disconnect(address, false);
350
341
  await this.#sessions.deleteResumptionRecord(address);
351
- this.#clients.delete(address);
352
342
  }
353
343
 
354
344
  async close() {
@@ -358,8 +348,11 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
358
348
  // This ends discovery without triggering promises
359
349
  mdnsScanner?.cancelOperationalDeviceDiscovery(this.#sessions.fabricFor(address), address.nodeId, false);
360
350
  }
361
- this.#clients.forEach(client => client.close());
362
- this.#clients.clear();
351
+
352
+ for (const { address } of this.#peers) {
353
+ await this.disconnect(address, false);
354
+ }
355
+
363
356
  this.#interactionQueue.close();
364
357
  this.#runningPeerReconnections.forEach(({ rejecter }) =>
365
358
  rejecter(new ChannelNotConnectedError("PeerSet closed")),
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Logger } from "#general";
8
+
9
+ const logger = Logger.get("PhysicalDeviceProperties");
10
+
11
+ const DEFAULT_SUBSCRIPTION_FLOOR_DEFAULT_S = 1;
12
+ const DEFAULT_SUBSCRIPTION_FLOOR_ICD_S = 0;
13
+ const DEFAULT_SUBSCRIPTION_CEILING_WIFI_S = 60;
14
+ const DEFAULT_SUBSCRIPTION_CEILING_THREAD_S = 60;
15
+ const DEFAULT_SUBSCRIPTION_CEILING_THREAD_SLEEPY_S = 180;
16
+ const DEFAULT_SUBSCRIPTION_CEILING_BATTERY_POWERED_S = 600;
17
+
18
+ export interface PhysicalDeviceProperties {
19
+ threadConnected: boolean;
20
+ wifiConnected: boolean;
21
+ ethernetConnected: boolean;
22
+ rootEndpointServerList: number[];
23
+ isBatteryPowered: boolean;
24
+ isIntermittentlyConnected: boolean;
25
+ isThreadSleepyEndDevice: boolean;
26
+ }
27
+
28
+ export namespace PhysicalDeviceProperties {
29
+ export function determineSubscriptionParameters(options?: {
30
+ properties?: PhysicalDeviceProperties;
31
+ description?: string;
32
+ subscribeMinIntervalFloorSeconds?: number;
33
+ subscribeMaxIntervalCeilingSeconds?: number;
34
+ }) {
35
+ const { properties } = options ?? {};
36
+
37
+ let {
38
+ description,
39
+ subscribeMinIntervalFloorSeconds: minIntervalFloorSeconds,
40
+ subscribeMaxIntervalCeilingSeconds: maxIntervalCeilingSeconds,
41
+ } = options ?? {};
42
+
43
+ if (description === undefined) {
44
+ description = "Node";
45
+ }
46
+
47
+ const { isBatteryPowered, isIntermittentlyConnected, threadConnected, isThreadSleepyEndDevice } =
48
+ properties ?? {};
49
+
50
+ if (isIntermittentlyConnected) {
51
+ if (minIntervalFloorSeconds !== undefined && minIntervalFloorSeconds !== DEFAULT_SUBSCRIPTION_FLOOR_ICD_S) {
52
+ logger.info(
53
+ `${description}: Overwriting minIntervalFloorSeconds for intermittently connected device to ${DEFAULT_SUBSCRIPTION_FLOOR_ICD_S}`,
54
+ );
55
+ minIntervalFloorSeconds = DEFAULT_SUBSCRIPTION_FLOOR_ICD_S;
56
+ }
57
+ }
58
+
59
+ const defaultCeiling = isBatteryPowered
60
+ ? DEFAULT_SUBSCRIPTION_CEILING_BATTERY_POWERED_S
61
+ : isThreadSleepyEndDevice
62
+ ? DEFAULT_SUBSCRIPTION_CEILING_THREAD_SLEEPY_S
63
+ : threadConnected
64
+ ? DEFAULT_SUBSCRIPTION_CEILING_THREAD_S
65
+ : DEFAULT_SUBSCRIPTION_CEILING_WIFI_S;
66
+ if (maxIntervalCeilingSeconds === undefined) {
67
+ maxIntervalCeilingSeconds = defaultCeiling;
68
+ }
69
+ if (maxIntervalCeilingSeconds < defaultCeiling) {
70
+ logger.debug(
71
+ `${description}: maxIntervalCeilingSeconds ideally is ${defaultCeiling}s instead of ${maxIntervalCeilingSeconds}s due to device type`,
72
+ );
73
+ }
74
+
75
+ return {
76
+ minIntervalFloorSeconds: minIntervalFloorSeconds ?? DEFAULT_SUBSCRIPTION_FLOOR_DEFAULT_S,
77
+ maxIntervalCeilingSeconds,
78
+ };
79
+ }
80
+ }
package/src/peer/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from "./OperationalPeer.js";
11
11
  export * from "./PeerAddress.js";
12
12
  export * from "./PeerAddressStore.js";
13
13
  export * from "./PeerSet.js";
14
+ export * from "./PhysicalDeviceProperties.js";