@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
@@ -12,33 +12,37 @@ import { Subscribe } from "#action/request/Subscribe.js";
12
12
  import { Write } from "#action/request/Write.js";
13
13
  import { DecodedInvokeResult, InvokeResult } from "#action/response/InvokeResult.js";
14
14
  import { ReadResult } from "#action/response/ReadResult.js";
15
- import { SubscribeResult } from "#action/response/SubscribeResult.js";
16
15
  import { WriteResult } from "#action/response/WriteResult.js";
17
16
  import {
17
+ Abort,
18
18
  BasicSet,
19
19
  Diagnostic,
20
20
  Duration,
21
+ Entropy,
21
22
  Environment,
22
- Environmental,
23
23
  ImplementationError,
24
24
  isObject,
25
25
  Logger,
26
- PromiseQueue,
26
+ RetrySchedule,
27
27
  Seconds,
28
28
  } from "#general";
29
29
  import { InteractionClientMessenger, MessageType } from "#interaction/InteractionMessenger.js";
30
- import { InteractionQueue } from "#peer/InteractionQueue.js";
31
30
  import { ExchangeProvider } from "#protocol/ExchangeProvider.js";
31
+ import { SecureSession } from "#session/SecureSession.js";
32
32
  import { Status, TlvNoResponse, TlvSubscribeResponse } from "#types";
33
- import { ClientSubscriptions } from "./ClientSubscriptions.js";
34
33
  import { InputChunk } from "./InputChunk.js";
34
+ import { ClientSubscribe } from "./subscription/ClientSubscribe.js";
35
+ import { ClientSubscription } from "./subscription/ClientSubscription.js";
36
+ import { ClientSubscriptions } from "./subscription/ClientSubscriptions.js";
37
+ import { PeerSubscription } from "./subscription/PeerSubscription.js";
38
+ import { SustainedSubscription } from "./subscription/SustainedSubscription.js";
35
39
 
36
40
  const logger = Logger.get("ClientInteraction");
37
41
 
38
42
  export interface ClientInteractionContext {
39
- exchanges: ExchangeProvider;
40
- subscriptions: ClientSubscriptions;
41
- queue: PromiseQueue;
43
+ environment: Environment;
44
+ abort?: Abort.Signal;
45
+ sustainRetries?: RetrySchedule.Configuration;
42
46
  }
43
47
 
44
48
  export const DEFAULT_MIN_INTERVAL_FLOOR = Seconds(1);
@@ -54,18 +58,22 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
54
58
  {
55
59
  readonly #exchanges: ExchangeProvider;
56
60
  readonly #subscriptions: ClientSubscriptions;
57
- readonly #queue?: PromiseQueue;
58
61
  readonly #interactions = new BasicSet<Read | Write | Invoke | Subscribe>();
59
- #closed = false;
60
-
61
- constructor(context: ClientInteractionContext) {
62
- this.#exchanges = context.exchanges;
63
- this.#subscriptions = context.subscriptions;
64
- this.#queue = context.queue;
62
+ readonly #abort;
63
+ readonly #sustainRetries: RetrySchedule;
64
+
65
+ constructor({ environment, abort, sustainRetries }: ClientInteractionContext) {
66
+ this.#exchanges = environment.get(ExchangeProvider);
67
+ this.#subscriptions = environment.get(ClientSubscriptions);
68
+ this.#abort = Abort.subtask(abort);
69
+ this.#sustainRetries = new RetrySchedule(
70
+ environment.get(Entropy),
71
+ RetrySchedule.Configuration(SustainedSubscription.DefaultRetrySchedule, sustainRetries),
72
+ );
65
73
  }
66
74
 
67
75
  async close() {
68
- this.#closed = true;
76
+ this.#abort();
69
77
 
70
78
  while (this.#interactions.size) {
71
79
  await this.#interactions.deleted;
@@ -76,21 +84,10 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
76
84
  return this.#subscriptions;
77
85
  }
78
86
 
79
- get queue() {
80
- return this.#queue;
81
- }
82
-
83
- static [Environmental.create](env: Environment) {
84
- const instance = new ClientInteraction({
85
- exchanges: env.get(ExchangeProvider),
86
- subscriptions: env.get(ClientSubscriptions),
87
- queue: env.get(InteractionQueue),
88
- });
89
- env.set(ClientInteraction, instance);
90
- return instance;
91
- }
92
-
93
- async *read(request: Read, _session?: SessionT): ReadResult {
87
+ /**
88
+ * Read attributes and events.
89
+ */
90
+ async *read(request: Read, session?: SessionT): ReadResult {
94
91
  const readPathsCount = (request.attributeRequests?.length ?? 0) + (request.eventRequests?.length ?? 0);
95
92
  if (readPathsCount > 9) {
96
93
  logger.debug(
@@ -98,215 +95,209 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
98
95
  );
99
96
  }
100
97
 
101
- this.#begin(request);
102
-
103
- let messenger: undefined | InteractionClientMessenger;
104
- try {
105
- messenger = await InteractionClientMessenger.create(this.#exchanges);
106
-
107
- logger.debug("Read »", messenger.exchange.via, request);
108
- await messenger.sendReadRequest(request);
98
+ await using context = await this.#begin(request, session);
99
+ const { checkAbort, messenger } = context;
109
100
 
110
- let attributeReportCount = 0;
111
- let eventReportCount = 0;
101
+ logger.debug("Read »", messenger.exchange.via, request);
102
+ await messenger.sendReadRequest(request);
103
+ checkAbort();
112
104
 
113
- for await (const report of messenger.readDataReports()) {
114
- attributeReportCount += report.attributeReports?.length ?? 0;
115
- eventReportCount += report.eventReports?.length ?? 0;
116
- yield InputChunk(report);
117
- }
105
+ let attributeReportCount = 0;
106
+ let eventReportCount = 0;
118
107
 
119
- logger.debug(
120
- "Read «",
121
- messenger.exchange.via,
122
- Diagnostic.weak(
123
- attributeReportCount + eventReportCount === 0
124
- ? "(empty)"
125
- : Diagnostic.dict({ attributes: attributeReportCount, events: eventReportCount }),
126
- ),
127
- );
128
- } finally {
129
- await messenger?.close();
130
- this.#end(request);
108
+ for await (const report of messenger.readDataReports()) {
109
+ checkAbort();
110
+ attributeReportCount += report.attributeReports?.length ?? 0;
111
+ eventReportCount += report.eventReports?.length ?? 0;
112
+ yield InputChunk(report);
113
+ checkAbort();
131
114
  }
115
+
116
+ logger.debug(
117
+ "Read «",
118
+ messenger.exchange.via,
119
+ Diagnostic.weak(
120
+ attributeReportCount + eventReportCount === 0
121
+ ? "(empty)"
122
+ : Diagnostic.dict({ attributes: attributeReportCount, events: eventReportCount }),
123
+ ),
124
+ );
132
125
  }
133
126
 
134
127
  /**
135
- * Write chosen attributes remotely to the node.
136
- * The returned attribute write status information is returned. No error is thrown for individual attribute write
137
- * failures.
128
+ * Update node attributes.
129
+ *
130
+ * Writes with the Matter protocol are generally not atomic, so this method only throws if the entire action fails.
131
+ * You must check each {@link WriteResult.AttributeStatus} to determine whether individual updates failed.
138
132
  */
139
- async write<T extends Write>(request: T, _session?: SessionT): WriteResult<T> {
140
- this.#begin(request);
141
-
142
- let messenger: undefined | InteractionClientMessenger;
143
- try {
144
- messenger = await InteractionClientMessenger.create(this.#exchanges);
145
-
146
- if (request.timedRequest) {
147
- await messenger.sendTimedRequest(request.timeout ?? DEFAULT_TIMED_REQUEST_TIMEOUT);
148
- }
149
-
150
- logger.info("Write »", messenger.exchange.via, request);
151
-
152
- const response = await messenger.sendWriteCommand(request);
153
- if (request.suppressResponse) {
154
- return undefined as Awaited<WriteResult<T>>;
155
- }
156
- if (!response || !response.writeResponses?.length) {
157
- return [] as Awaited<WriteResult<T>>;
158
- }
133
+ async write<T extends Write>(request: T, session?: SessionT): WriteResult<T> {
134
+ await using context = await this.#begin(request, session);
135
+ const { checkAbort, messenger } = context;
159
136
 
160
- let successCount = 0;
161
- let failureCount = 0;
162
- const result = response.writeResponses.map(
163
- ({
164
- path: { nodeId, endpointId, clusterId, attributeId, listIndex },
165
- status: { status, clusterStatus },
166
- }) => {
167
- if (status === Status.Success) {
168
- successCount++;
169
- } else {
170
- failureCount++;
171
- }
172
- return {
173
- kind: "attr-status",
174
- path: {
175
- nodeId,
176
- endpointId: endpointId!,
177
- clusterId: clusterId!,
178
- attributeId: attributeId!,
179
- listIndex,
180
- },
181
- status,
182
- clusterStatus,
183
- };
184
- },
185
- ) as Awaited<WriteResult<T>>;
137
+ if (request.timedRequest) {
138
+ await messenger.sendTimedRequest(request.timeout ?? DEFAULT_TIMED_REQUEST_TIMEOUT);
139
+ checkAbort();
140
+ }
186
141
 
187
- logger.info(
188
- "Write «",
189
- messenger.exchange.via,
190
- Diagnostic.weak(
191
- successCount + failureCount === 0
192
- ? "(empty)"
193
- : Diagnostic.dict({ success: successCount, failure: failureCount }),
194
- ),
195
- );
142
+ logger.info("Write »", messenger.exchange.via, request);
196
143
 
197
- return result;
198
- } finally {
199
- await messenger?.close();
200
- this.#end(request);
144
+ const response = await messenger.sendWriteCommand(request);
145
+ checkAbort();
146
+ if (request.suppressResponse) {
147
+ return undefined as Awaited<WriteResult<T>>;
148
+ }
149
+ if (!response || !response.writeResponses?.length) {
150
+ return [] as Awaited<WriteResult<T>>;
201
151
  }
202
- }
203
-
204
- async *invoke(request: ClientInvoke, _session?: SessionT): DecodedInvokeResult {
205
- this.#begin(request);
206
152
 
207
- let messenger: InteractionClientMessenger | undefined;
208
- try {
209
- messenger = await InteractionClientMessenger.create(this.#exchanges);
153
+ let successCount = 0;
154
+ let failureCount = 0;
155
+ const result = response.writeResponses.map(
156
+ ({
157
+ path: { nodeId, endpointId, clusterId, attributeId, listIndex },
158
+ status: { status, clusterStatus },
159
+ }) => {
160
+ if (status === Status.Success) {
161
+ successCount++;
162
+ } else {
163
+ failureCount++;
164
+ }
165
+ return {
166
+ kind: "attr-status",
167
+ path: {
168
+ nodeId,
169
+ endpointId: endpointId!,
170
+ clusterId: clusterId!,
171
+ attributeId: attributeId!,
172
+ listIndex,
173
+ },
174
+ status,
175
+ clusterStatus,
176
+ };
177
+ },
178
+ ) as Awaited<WriteResult<T>>;
179
+
180
+ logger.info(
181
+ "Write «",
182
+ messenger.exchange.via,
183
+ Diagnostic.weak(
184
+ successCount + failureCount === 0
185
+ ? "(empty)"
186
+ : Diagnostic.dict({ success: successCount, failure: failureCount }),
187
+ ),
188
+ );
189
+
190
+ return result;
191
+ }
210
192
 
211
- if (request.timedRequest) {
212
- await messenger.sendTimedRequest(request.timeout ?? DEFAULT_TIMED_REQUEST_TIMEOUT);
213
- }
193
+ /**
194
+ * Invoke one or more commands.
195
+ */
196
+ async *invoke(request: ClientInvoke, session?: SessionT): DecodedInvokeResult {
197
+ await using context = await this.#begin(request, session);
198
+ const { checkAbort, messenger } = context;
214
199
 
215
- logger.info(
216
- "Invoke »",
217
- messenger.exchange.via,
218
- Diagnostic.asFlags({ suppressResponse: request.suppressResponse, timed: request.timedRequest }),
219
- request,
220
- );
200
+ if (request.timedRequest) {
201
+ await messenger.sendTimedRequest(request.timeout ?? DEFAULT_TIMED_REQUEST_TIMEOUT);
202
+ checkAbort();
203
+ }
221
204
 
222
- const { expectedProcessingTime, useExtendedFailSafeMessageResponseTimeout } = request;
223
- const result = await messenger.sendInvokeCommand(
224
- request,
225
- expectedProcessingTime ??
226
- (useExtendedFailSafeMessageResponseTimeout
227
- ? DEFAULT_MINIMUM_RESPONSE_TIMEOUT_WITH_FAILSAFE
228
- : undefined),
229
- );
230
- if (!request.suppressResponse) {
231
- if (result && result.invokeResponses?.length) {
232
- const chunk: InvokeResult.Chunk = result.invokeResponses
233
- .map(response => {
234
- if (response.command !== undefined) {
235
- const {
236
- commandPath: { endpointId, clusterId, commandId },
237
- commandRef,
238
- commandFields,
239
- } = response.command;
240
- const cmd = request.commands.get(commandRef);
241
- if (!cmd) {
242
- throw new ImplementationError(
243
- `No response schema found for commandRef ${commandRef} (endpoint ${endpointId}, cluster ${clusterId}, command ${commandId})`,
244
- );
245
- }
246
- const responseSchema = Invoke.commandOf(cmd).responseSchema;
247
- if (commandFields === undefined && responseSchema !== TlvNoResponse) {
248
- throw new ImplementationError(
249
- `No command fields found for commandRef ${commandRef} (endpoint ${endpointId}, cluster ${clusterId}, command ${commandId})`,
250
- );
251
- }
252
-
253
- const data =
254
- commandFields === undefined ? undefined : responseSchema.decodeTlv(commandFields);
255
-
256
- logger.info(
257
- "Invoke «",
258
- messenger!.exchange.via,
259
- Diagnostic.strong(resolvePathForSpecifier(cmd)),
260
- isObject(data) ? Diagnostic.dict(data) : Diagnostic.weak("(no payload)"),
205
+ logger.info(
206
+ "Invoke »",
207
+ messenger.exchange.via,
208
+ Diagnostic.asFlags({ suppressResponse: request.suppressResponse, timed: request.timedRequest }),
209
+ request,
210
+ );
211
+
212
+ const { expectedProcessingTime, useExtendedFailSafeMessageResponseTimeout } = request;
213
+ const result = await messenger.sendInvokeCommand(
214
+ request,
215
+ expectedProcessingTime ??
216
+ (useExtendedFailSafeMessageResponseTimeout
217
+ ? DEFAULT_MINIMUM_RESPONSE_TIMEOUT_WITH_FAILSAFE
218
+ : undefined),
219
+ );
220
+ checkAbort();
221
+ if (!request.suppressResponse) {
222
+ if (result && result.invokeResponses?.length) {
223
+ const chunk: InvokeResult.Chunk = result.invokeResponses
224
+ .map(response => {
225
+ if (response.command !== undefined) {
226
+ const {
227
+ commandPath: { endpointId, clusterId, commandId },
228
+ commandRef,
229
+ commandFields,
230
+ } = response.command;
231
+ const cmd = request.commands.get(commandRef);
232
+ if (!cmd) {
233
+ throw new ImplementationError(
234
+ `No response schema found for commandRef ${commandRef} (endpoint ${endpointId}, cluster ${clusterId}, command ${commandId})`,
261
235
  );
262
-
263
- const res: InvokeResult.DecodedCommandResponse = {
264
- kind: "cmd-response",
265
- path: {
266
- endpointId: endpointId!,
267
- clusterId,
268
- commandId,
269
- },
270
- commandRef,
271
- data,
272
- };
273
- return res;
274
- } else if (response.status !== undefined) {
275
- const {
276
- commandPath: { endpointId, clusterId, commandId },
277
- commandRef,
278
- status: { status, clusterStatus },
279
- } = response.status;
280
- const res: InvokeResult.CommandStatus = {
281
- kind: "cmd-status",
282
- path: {
283
- endpointId: endpointId!,
284
- clusterId: clusterId,
285
- commandId: commandId,
286
- },
287
- commandRef,
288
- status,
289
- clusterStatus,
290
- };
291
- return res;
292
- } else {
293
- // Should not happen but if we ignore the response?
294
- return undefined;
295
236
  }
296
- })
297
- .filter(r => r !== undefined);
298
- yield chunk;
299
- } else {
300
- yield [];
301
- }
237
+ const responseSchema = Invoke.commandOf(cmd).responseSchema;
238
+ if (commandFields === undefined && responseSchema !== TlvNoResponse) {
239
+ throw new ImplementationError(
240
+ `No command fields found for commandRef ${commandRef} (endpoint ${endpointId}, cluster ${clusterId}, command ${commandId})`,
241
+ );
242
+ }
243
+
244
+ const data =
245
+ commandFields === undefined ? undefined : responseSchema.decodeTlv(commandFields);
246
+
247
+ logger.info(
248
+ "Invoke «",
249
+ messenger.exchange.via,
250
+ Diagnostic.strong(resolvePathForSpecifier(cmd)),
251
+ isObject(data) ? Diagnostic.dict(data) : Diagnostic.weak("(no payload)"),
252
+ );
253
+
254
+ const res: InvokeResult.DecodedCommandResponse = {
255
+ kind: "cmd-response",
256
+ path: {
257
+ endpointId: endpointId!,
258
+ clusterId,
259
+ commandId,
260
+ },
261
+ commandRef,
262
+ data,
263
+ };
264
+ return res;
265
+ } else if (response.status !== undefined) {
266
+ const {
267
+ commandPath: { endpointId, clusterId, commandId },
268
+ commandRef,
269
+ status: { status, clusterStatus },
270
+ } = response.status;
271
+ const res: InvokeResult.CommandStatus = {
272
+ kind: "cmd-status",
273
+ path: {
274
+ endpointId: endpointId!,
275
+ clusterId: clusterId,
276
+ commandId: commandId,
277
+ },
278
+ commandRef,
279
+ status,
280
+ clusterStatus,
281
+ };
282
+ return res;
283
+ } else {
284
+ // Should not happen but if we ignore the response?
285
+ return undefined;
286
+ }
287
+ })
288
+ .filter(r => r !== undefined);
289
+ yield chunk;
290
+ } else {
291
+ yield [];
302
292
  }
303
- } finally {
304
- await messenger?.close();
305
- this.#end(request);
293
+ checkAbort();
306
294
  }
307
295
  }
308
296
 
309
- async subscribe(request: Subscribe, _session?: SessionT): SubscribeResult {
297
+ /**
298
+ * Subscribe to attribute values and events.
299
+ */
300
+ async subscribe(request: ClientSubscribe, session?: SessionT) {
310
301
  const subscriptionPathsCount = (request.attributeRequests?.length ?? 0) + (request.eventRequests?.length ?? 0);
311
302
  if (subscriptionPathsCount > 3) {
312
303
  logger.debug("Subscribe interactions with more then 3 paths might be not allowed by the device.");
@@ -321,11 +312,12 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
321
312
  }
322
313
  }
323
314
 
324
- this.#begin(request);
315
+ SecureSession.assert(this.#exchanges.session);
316
+ const peer = this.#exchanges.session.peerAddress;
325
317
 
326
- let messenger: undefined | InteractionClientMessenger;
327
- try {
328
- messenger = await InteractionClientMessenger.create(this.#exchanges);
318
+ const subscribe = async (request: ClientSubscribe) => {
319
+ await using context = await this.#begin(request, session);
320
+ const { checkAbort, messenger } = context;
329
321
 
330
322
  logger.info(
331
323
  "Subscribe »",
@@ -343,8 +335,10 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
343
335
  maxIntervalCeilingSeconds: Seconds.of(DEFAULT_MIN_INTERVAL_FLOOR), // TODO use better max fallback
344
336
  ...request,
345
337
  });
338
+ checkAbort();
346
339
 
347
340
  await this.#handleSubscriptionResponse(request, readChunks(messenger));
341
+ checkAbort();
348
342
 
349
343
  const responseMessage = await messenger.nextMessage(MessageType.SubscribeResponse);
350
344
  const response = TlvSubscribeResponse.decode(responseMessage.payload);
@@ -353,20 +347,40 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
353
347
  "Subscription successful «",
354
348
  messenger.exchange.via,
355
349
  Diagnostic.dict({
356
- subId: response.subscriptionId,
350
+ id: response.subscriptionId,
357
351
  interval: Duration.format(Seconds(response.maxInterval)),
358
352
  }),
359
353
  );
360
354
 
361
- return this.#subscriptions.add(request, response);
362
- } finally {
363
- await messenger?.close();
364
- this.#end(request);
355
+ const subscription = new PeerSubscription({
356
+ request,
357
+ peer,
358
+ closed: () => this.#subscriptions.delete(subscription),
359
+ response,
360
+ abort: session?.abort,
361
+ });
362
+ this.#subscriptions.addPeer(subscription);
363
+
364
+ return subscription;
365
+ };
366
+
367
+ let subscription: ClientSubscription;
368
+ if (request.sustain) {
369
+ subscription = new SustainedSubscription({
370
+ subscribe,
371
+ peer,
372
+ closed: () => this.#subscriptions.delete(subscription),
373
+ request,
374
+ abort: session?.abort,
375
+ retries: this.#sustainRetries,
376
+ });
377
+ } else {
378
+ subscription = await subscribe(request);
365
379
  }
366
- }
367
380
 
368
- cancelSubscription(id: number) {
369
- this.#subscriptions.get(id)?.close();
381
+ this.#subscriptions.addActive(subscription);
382
+
383
+ return subscription;
370
384
  }
371
385
 
372
386
  async #handleSubscriptionResponse(request: Subscribe, result: ReadResult) {
@@ -382,18 +396,42 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
382
396
  }
383
397
  }
384
398
 
385
- #begin(request: Read | Write | Invoke | Subscribe) {
386
- if (this.#closed) {
399
+ async #begin(request: Read | Write | Invoke | Subscribe, session?: SessionT) {
400
+ if (this.#abort.aborted) {
387
401
  throw new ImplementationError("Client interaction unavailable after close");
388
402
  }
389
403
  this.#interactions.add(request);
390
- }
391
404
 
392
- #end(request: Read | Write | Invoke | Subscribe) {
393
- this.#interactions.delete(request);
405
+ const checkAbort = Abort.checkerFor(session);
406
+
407
+ const messenger = await InteractionClientMessenger.create(this.#exchanges);
408
+
409
+ const context: RequestContext = {
410
+ checkAbort,
411
+ messenger,
412
+ [Symbol.asyncDispose]: async () => {
413
+ await messenger.close();
414
+ this.#interactions.delete(request);
415
+ },
416
+ };
417
+
418
+ try {
419
+ context.checkAbort();
420
+ } catch (e) {
421
+ await context[Symbol.asyncDispose]();
422
+ }
423
+
424
+ return context;
394
425
  }
395
426
  }
396
427
 
428
+ interface RequestContext {
429
+ checkAbort(): void;
430
+ messenger: InteractionClientMessenger;
431
+
432
+ [Symbol.asyncDispose](): Promise<void>;
433
+ }
434
+
397
435
  async function* readChunks(messenger: InteractionClientMessenger) {
398
436
  for await (const report of messenger.readDataReports()) {
399
437
  yield InputChunk(report);
@@ -5,8 +5,6 @@
5
5
  */
6
6
 
7
7
  export * from "./ClientInteraction.js";
8
- export * from "./ClientSubscription.js";
9
- export * from "./ClientSubscriptionHandler.js";
10
- export * from "./ClientSubscriptions.js";
11
8
  export * from "./InputChunk.js";
12
9
  export * from "./ReadScope.js";
10
+ export * from "./subscription/index.js";
@@ -0,0 +1,8 @@
1
+ import { Subscribe } from "#action/request/Subscribe.js";
2
+
3
+ export interface ClientSubscribe extends Subscribe {
4
+ /**
5
+ * If true the subscription is virtualized and the underlying subscription is reestablished when lost.
6
+ */
7
+ sustain?: boolean;
8
+ }