@matter/protocol 0.15.1 → 0.15.2-alpha.0-20250703-2e16aba2b

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 (149) hide show
  1. package/dist/cjs/action/Val.d.ts +4 -0
  2. package/dist/cjs/action/Val.d.ts.map +1 -1
  3. package/dist/cjs/action/Val.js.map +1 -1
  4. package/dist/cjs/action/client/ClientInteraction.d.ts +10 -8
  5. package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
  6. package/dist/cjs/action/client/ClientInteraction.js +152 -80
  7. package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
  8. package/dist/cjs/action/client/ClientSubscription.d.ts +17 -0
  9. package/dist/cjs/action/client/ClientSubscription.d.ts.map +1 -0
  10. package/dist/cjs/action/client/ClientSubscription.js +22 -0
  11. package/dist/cjs/action/client/ClientSubscription.js.map +6 -0
  12. package/dist/cjs/action/client/ClientSubscriptionHandler.d.ts +20 -0
  13. package/dist/cjs/action/client/ClientSubscriptionHandler.d.ts.map +1 -0
  14. package/dist/cjs/action/client/ClientSubscriptionHandler.js +117 -0
  15. package/dist/cjs/action/client/ClientSubscriptionHandler.js.map +6 -0
  16. package/dist/cjs/action/client/ClientSubscriptions.d.ts +45 -0
  17. package/dist/cjs/action/client/ClientSubscriptions.d.ts.map +1 -0
  18. package/dist/cjs/action/client/ClientSubscriptions.js +141 -0
  19. package/dist/cjs/action/client/ClientSubscriptions.js.map +6 -0
  20. package/dist/cjs/action/client/InputChunk.d.ts +12 -0
  21. package/dist/cjs/action/client/InputChunk.d.ts.map +1 -0
  22. package/dist/cjs/action/client/InputChunk.js +89 -0
  23. package/dist/cjs/action/client/InputChunk.js.map +6 -0
  24. package/dist/cjs/action/client/ReadScope.d.ts +26 -0
  25. package/dist/cjs/action/client/ReadScope.d.ts.map +1 -0
  26. package/dist/cjs/action/client/ReadScope.js +87 -0
  27. package/dist/cjs/action/client/ReadScope.js.map +6 -0
  28. package/dist/cjs/action/client/index.d.ts +5 -0
  29. package/dist/cjs/action/client/index.d.ts.map +1 -1
  30. package/dist/cjs/action/client/index.js +5 -0
  31. package/dist/cjs/action/client/index.js.map +1 -1
  32. package/dist/cjs/action/request/Read.d.ts +0 -4
  33. package/dist/cjs/action/request/Read.d.ts.map +1 -1
  34. package/dist/cjs/action/request/Read.js.map +1 -1
  35. package/dist/cjs/action/request/Subscribe.d.ts +14 -1
  36. package/dist/cjs/action/request/Subscribe.d.ts.map +1 -1
  37. package/dist/cjs/action/request/Subscribe.js +2 -2
  38. package/dist/cjs/action/request/Subscribe.js.map +1 -1
  39. package/dist/cjs/action/response/SubscribeResult.d.ts +3 -5
  40. package/dist/cjs/action/response/SubscribeResult.d.ts.map +1 -1
  41. package/dist/cjs/common/FailsafeContext.d.ts.map +1 -1
  42. package/dist/cjs/common/FailsafeContext.js +0 -1
  43. package/dist/cjs/common/FailsafeContext.js.map +1 -1
  44. package/dist/cjs/fabric/FabricAuthority.d.ts +6 -1
  45. package/dist/cjs/fabric/FabricAuthority.d.ts.map +1 -1
  46. package/dist/cjs/fabric/FabricAuthority.js +16 -0
  47. package/dist/cjs/fabric/FabricAuthority.js.map +1 -1
  48. package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
  49. package/dist/cjs/interaction/InteractionClient.js +8 -4
  50. package/dist/cjs/interaction/InteractionClient.js.map +1 -1
  51. package/dist/cjs/interaction/InteractionMessenger.d.ts +20 -16
  52. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  53. package/dist/cjs/interaction/InteractionMessenger.js +18 -10
  54. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  55. package/dist/cjs/mdns/MdnsScanner.d.ts +5 -6
  56. package/dist/cjs/mdns/MdnsScanner.d.ts.map +1 -1
  57. package/dist/cjs/mdns/MdnsScanner.js.map +1 -1
  58. package/dist/cjs/protocol/DeviceCommissioner.d.ts +1 -1
  59. package/dist/cjs/protocol/DeviceCommissioner.d.ts.map +1 -1
  60. package/dist/cjs/protocol/DeviceCommissioner.js +2 -5
  61. package/dist/cjs/protocol/DeviceCommissioner.js.map +1 -1
  62. package/dist/cjs/securechannel/SecureChannelMessenger.d.ts +3 -0
  63. package/dist/cjs/securechannel/SecureChannelMessenger.d.ts.map +1 -1
  64. package/dist/cjs/securechannel/SecureChannelMessenger.js +4 -0
  65. package/dist/cjs/securechannel/SecureChannelMessenger.js.map +1 -1
  66. package/dist/esm/action/Val.d.ts +4 -0
  67. package/dist/esm/action/Val.d.ts.map +1 -1
  68. package/dist/esm/action/Val.js.map +1 -1
  69. package/dist/esm/action/client/ClientInteraction.d.ts +10 -8
  70. package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
  71. package/dist/esm/action/client/ClientInteraction.js +154 -82
  72. package/dist/esm/action/client/ClientInteraction.js.map +1 -1
  73. package/dist/esm/action/client/ClientSubscription.d.ts +17 -0
  74. package/dist/esm/action/client/ClientSubscription.d.ts.map +1 -0
  75. package/dist/esm/action/client/ClientSubscription.js +6 -0
  76. package/dist/esm/action/client/ClientSubscription.js.map +6 -0
  77. package/dist/esm/action/client/ClientSubscriptionHandler.d.ts +20 -0
  78. package/dist/esm/action/client/ClientSubscriptionHandler.d.ts.map +1 -0
  79. package/dist/esm/action/client/ClientSubscriptionHandler.js +97 -0
  80. package/dist/esm/action/client/ClientSubscriptionHandler.js.map +6 -0
  81. package/dist/esm/action/client/ClientSubscriptions.d.ts +45 -0
  82. package/dist/esm/action/client/ClientSubscriptions.d.ts.map +1 -0
  83. package/dist/esm/action/client/ClientSubscriptions.js +129 -0
  84. package/dist/esm/action/client/ClientSubscriptions.js.map +6 -0
  85. package/dist/esm/action/client/InputChunk.d.ts +12 -0
  86. package/dist/esm/action/client/InputChunk.d.ts.map +1 -0
  87. package/dist/esm/action/client/InputChunk.js +69 -0
  88. package/dist/esm/action/client/InputChunk.js.map +6 -0
  89. package/dist/esm/action/client/ReadScope.d.ts +26 -0
  90. package/dist/esm/action/client/ReadScope.d.ts.map +1 -0
  91. package/dist/esm/action/client/ReadScope.js +67 -0
  92. package/dist/esm/action/client/ReadScope.js.map +6 -0
  93. package/dist/esm/action/client/index.d.ts +5 -0
  94. package/dist/esm/action/client/index.d.ts.map +1 -1
  95. package/dist/esm/action/client/index.js +5 -0
  96. package/dist/esm/action/client/index.js.map +1 -1
  97. package/dist/esm/action/request/Read.d.ts +0 -4
  98. package/dist/esm/action/request/Read.d.ts.map +1 -1
  99. package/dist/esm/action/request/Read.js.map +1 -1
  100. package/dist/esm/action/request/Subscribe.d.ts +14 -1
  101. package/dist/esm/action/request/Subscribe.d.ts.map +1 -1
  102. package/dist/esm/action/request/Subscribe.js +2 -2
  103. package/dist/esm/action/request/Subscribe.js.map +1 -1
  104. package/dist/esm/action/response/SubscribeResult.d.ts +3 -5
  105. package/dist/esm/action/response/SubscribeResult.d.ts.map +1 -1
  106. package/dist/esm/common/FailsafeContext.d.ts.map +1 -1
  107. package/dist/esm/common/FailsafeContext.js +0 -1
  108. package/dist/esm/common/FailsafeContext.js.map +1 -1
  109. package/dist/esm/fabric/FabricAuthority.d.ts +6 -1
  110. package/dist/esm/fabric/FabricAuthority.d.ts.map +1 -1
  111. package/dist/esm/fabric/FabricAuthority.js +25 -1
  112. package/dist/esm/fabric/FabricAuthority.js.map +1 -1
  113. package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
  114. package/dist/esm/interaction/InteractionClient.js +8 -4
  115. package/dist/esm/interaction/InteractionClient.js.map +1 -1
  116. package/dist/esm/interaction/InteractionMessenger.d.ts +20 -16
  117. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  118. package/dist/esm/interaction/InteractionMessenger.js +18 -10
  119. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  120. package/dist/esm/mdns/MdnsScanner.d.ts +5 -6
  121. package/dist/esm/mdns/MdnsScanner.d.ts.map +1 -1
  122. package/dist/esm/mdns/MdnsScanner.js.map +1 -1
  123. package/dist/esm/protocol/DeviceCommissioner.d.ts +1 -1
  124. package/dist/esm/protocol/DeviceCommissioner.d.ts.map +1 -1
  125. package/dist/esm/protocol/DeviceCommissioner.js +2 -5
  126. package/dist/esm/protocol/DeviceCommissioner.js.map +1 -1
  127. package/dist/esm/securechannel/SecureChannelMessenger.d.ts +3 -0
  128. package/dist/esm/securechannel/SecureChannelMessenger.d.ts.map +1 -1
  129. package/dist/esm/securechannel/SecureChannelMessenger.js +4 -0
  130. package/dist/esm/securechannel/SecureChannelMessenger.js.map +1 -1
  131. package/package.json +6 -6
  132. package/src/action/Val.ts +5 -0
  133. package/src/action/client/ClientInteraction.ts +178 -90
  134. package/src/action/client/ClientSubscription.ts +18 -0
  135. package/src/action/client/ClientSubscriptionHandler.ts +137 -0
  136. package/src/action/client/ClientSubscriptions.ts +172 -0
  137. package/src/action/client/InputChunk.ts +79 -0
  138. package/src/action/client/ReadScope.ts +107 -0
  139. package/src/action/client/index.ts +5 -0
  140. package/src/action/request/Read.ts +0 -5
  141. package/src/action/request/Subscribe.ts +17 -3
  142. package/src/action/response/SubscribeResult.ts +3 -4
  143. package/src/common/FailsafeContext.ts +0 -1
  144. package/src/fabric/FabricAuthority.ts +29 -1
  145. package/src/interaction/InteractionClient.ts +8 -4
  146. package/src/interaction/InteractionMessenger.ts +18 -11
  147. package/src/mdns/MdnsScanner.ts +5 -6
  148. package/src/protocol/DeviceCommissioner.ts +2 -7
  149. package/src/securechannel/SecureChannelMessenger.ts +4 -0
@@ -0,0 +1,137 @@
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 { DecodedDataReport } from "#interaction/DecodedDataReport.js";
9
+ import { IncomingInteractionClientMessenger } from "#interaction/InteractionMessenger.js";
10
+ import { SubscriptionId } from "#interaction/Subscription.js";
11
+ import { MessageExchange } from "#protocol/MessageExchange.js";
12
+ import { ProtocolHandler } from "#protocol/ProtocolHandler.js";
13
+ import { DataReport, INTERACTION_PROTOCOL_ID, Status } from "#types";
14
+ import { Diagnostic, InternalError, Logger } from "@matter/general";
15
+ import { ClientSubscriptions } from "./ClientSubscriptions.js";
16
+ import { InputChunk } from "./InputChunk.js";
17
+
18
+ const logger = Logger.get("ClientSubscriptionHandler");
19
+
20
+ /**
21
+ * A protocol handler that informs {@link ClientSubscriptions} of new exchanges.
22
+ */
23
+ export class ClientSubscriptionHandler implements ProtocolHandler {
24
+ id = INTERACTION_PROTOCOL_ID;
25
+ requiresSecureSession = true;
26
+
27
+ #subscriptions: ClientSubscriptions;
28
+
29
+ constructor(subscriptions: ClientSubscriptions) {
30
+ this.#subscriptions = subscriptions;
31
+ }
32
+
33
+ async onNewExchange(exchange: MessageExchange) {
34
+ const messenger = new IncomingInteractionClientMessenger(exchange);
35
+ // Read the initial report
36
+ const reports = messenger.readDataReports();
37
+
38
+ const initialIteration = await reports.next();
39
+ if (initialIteration.done) {
40
+ throw new InternalError("Exchange initiated with no initial message");
41
+ }
42
+ const initialReport = initialIteration.value;
43
+
44
+ // Ensure there is a subscription ID present
45
+ const { subscriptionId } = initialReport;
46
+ if (subscriptionId === undefined) {
47
+ logger.debug("Ignoring unsolicited data report with no subscription ID");
48
+ await sendInvalid(messenger, undefined);
49
+ return;
50
+ }
51
+
52
+ // Ensure the subscription ID is valid
53
+ const subscription = this.#subscriptions.get(subscriptionId);
54
+ if (subscription === undefined) {
55
+ logger.debug("Ignoring data report for unknown subscription ID", Diagnostic.strong(subscriptionId));
56
+ await sendInvalid(messenger, subscriptionId);
57
+ return;
58
+ }
59
+
60
+ // If this is just a ping, only reset the timeout
61
+ if (!initialReport.attributeReports?.length && !initialReport.eventReports?.length) {
62
+ subscription.timeoutAtMs = undefined;
63
+ this.#subscriptions.resetTimer();
64
+ await exchange.close();
65
+ return;
66
+ }
67
+
68
+ // Pass the data to the recipient
69
+ try {
70
+ subscription.isReading = true;
71
+
72
+ if (subscription.request.updated) {
73
+ await subscription.request.updated(processReports(initialReport, reports, messenger));
74
+ } else {
75
+ // It doesn't make sense to have the callback undefined but we allow it in the type because they may
76
+ // be handled by intermediate interactables. So we handle the case here too, but just iterate and throw
77
+ // away the reports
78
+ for await (const _chunk of reports);
79
+ }
80
+ } finally {
81
+ subscription.isReading = false;
82
+ subscription.timeoutAtMs = undefined;
83
+ this.#subscriptions.resetTimer();
84
+ await exchange.close();
85
+ }
86
+ }
87
+
88
+ async close() {}
89
+ }
90
+
91
+ async function sendInvalid(messenger: IncomingInteractionClientMessenger, subscriptionId?: SubscriptionId) {
92
+ await messenger.sendStatus(Status.InvalidSubscription, {
93
+ multipleMessageInteraction: true,
94
+ logContext: {
95
+ subId: subscriptionId,
96
+ },
97
+ });
98
+ await messenger.close();
99
+ }
100
+
101
+ /**
102
+ * Convert incoming data reports into a {@link ReadResult}.
103
+ *
104
+ * Parses incoming reports and validates subscription IDs.
105
+ */
106
+ async function* processReports(
107
+ initialReport: DataReport,
108
+ otherReports: AsyncIterable<DataReport>,
109
+ messenger: IncomingInteractionClientMessenger,
110
+ ): ReadResult {
111
+ yield InputChunk(initialReport);
112
+
113
+ const { subscriptionId } = initialReport;
114
+
115
+ for await (const report of otherReports) {
116
+ const decoded = DecodedDataReport(report);
117
+
118
+ if (decoded.subscriptionId === undefined) {
119
+ logger.debug(
120
+ "Ignoring data report for incorrect subscription id",
121
+ Diagnostic.strong(decoded.subscriptionId),
122
+ );
123
+ await sendInvalid(messenger, decoded.subscriptionId);
124
+ }
125
+
126
+ if (decoded.subscriptionId !== subscriptionId) {
127
+ logger.debug(
128
+ "Ignoring data report for incorrect subscription id",
129
+ Diagnostic.strong(decoded.subscriptionId),
130
+ );
131
+ await sendInvalid(messenger, decoded.subscriptionId);
132
+ continue;
133
+ }
134
+
135
+ yield InputChunk(report);
136
+ }
137
+ }
@@ -0,0 +1,172 @@
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 { ReadResult } from "#action/response/ReadResult.js";
9
+ import { ActiveSubscription } from "#action/response/SubscribeResult.js";
10
+ import {
11
+ BasicSet,
12
+ CanceledError,
13
+ Diagnostic,
14
+ Environment,
15
+ Environmental,
16
+ Logger,
17
+ Time,
18
+ TimeoutError,
19
+ Timer,
20
+ } from "#general";
21
+ import { SubscriptionId } from "#interaction/Subscription.js";
22
+ import { SubscribeResponse } from "#types";
23
+ import { ClientSubscription } from "./ClientSubscription.js";
24
+
25
+ const logger = Logger.get("ClientSubscriptions");
26
+
27
+ /**
28
+ * A managed set of {@link ClientSubscription} instances.
29
+ */
30
+ export class ClientSubscriptions {
31
+ #subscriptions = new BasicSet<ClientSubscription>();
32
+ #timeout?: Timer;
33
+ #nextTimeoutAt?: number;
34
+
35
+ static [Environmental.create](env: Environment) {
36
+ const instance = new ClientSubscriptions();
37
+ env.set(ClientSubscriptions, instance);
38
+ return instance;
39
+ }
40
+
41
+ /**
42
+ * Register an active subscription.
43
+ */
44
+ add(request: Subscribe, response: SubscribeResponse): ActiveSubscription {
45
+ const subscription: ClientSubscription = {
46
+ ...response,
47
+ request,
48
+ close: () => this.#closeOne(subscription, new CanceledError()),
49
+ timeoutAtMs: undefined,
50
+ isClosed: false,
51
+ isReading: true,
52
+ };
53
+
54
+ this.#subscriptions.add(subscription);
55
+ this.resetTimer();
56
+
57
+ return subscription;
58
+ }
59
+
60
+ /**
61
+ * Retrieve a subscription by ID.
62
+ */
63
+ get(id: SubscriptionId) {
64
+ return this.#subscriptions.get("subscriptionId", id);
65
+ }
66
+
67
+ /**
68
+ * Iterate over active subscriptions.
69
+ */
70
+ [Symbol.iterator]() {
71
+ return this.#subscriptions[Symbol.iterator]();
72
+ }
73
+
74
+ /**
75
+ * Terminate all subscriptions.
76
+ */
77
+ async close() {
78
+ if (this.#timeout) {
79
+ this.#timeout.stop();
80
+ this.#timeout = undefined;
81
+ }
82
+ for (const subscription of this.#subscriptions) {
83
+ subscription.close();
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Restart the timeout timer for the current set of active subscriptions.
89
+ */
90
+ resetTimer() {
91
+ const now = Time.nowMs();
92
+ let nextTimeoutAt: number | undefined;
93
+
94
+ // Process each subscription
95
+ for (const subscription of this.#subscriptions) {
96
+ // If reading data reports, ignore for timeout purposes
97
+ if (subscription.isReading) {
98
+ continue;
99
+ }
100
+
101
+ // Update timeout or expire if timed out
102
+ let { timeoutAtMs } = subscription;
103
+ if (timeoutAtMs === undefined) {
104
+ // Set timeout time
105
+ timeoutAtMs = subscription.timeoutAtMs = timeoutFor(subscription);
106
+ } else if (timeoutAtMs < now) {
107
+ // Timeout
108
+ this.#timeOut(subscription);
109
+ continue;
110
+ }
111
+
112
+ // If this is the earliest timeout, record
113
+ if (nextTimeoutAt === undefined || nextTimeoutAt > timeoutAtMs) {
114
+ nextTimeoutAt = timeoutAtMs;
115
+ }
116
+ }
117
+
118
+ // If no subscriptions require timeout, disable timer
119
+ if (nextTimeoutAt === undefined) {
120
+ this.#nextTimeoutAt = undefined;
121
+ this.#timeout?.stop();
122
+ return;
123
+ }
124
+
125
+ // Create or update timer if not set for correct interval
126
+ if (nextTimeoutAt !== this.#nextTimeoutAt) {
127
+ this.#nextTimeoutAt = nextTimeoutAt;
128
+ if (this.#timeout) {
129
+ this.#timeout?.stop();
130
+ this.#timeout.intervalMs = nextTimeoutAt - now;
131
+ } else {
132
+ this.#timeout = Time.getTimer("SubscriptionTimeout", nextTimeoutAt - now, this.resetTimer.bind(this));
133
+ }
134
+ }
135
+ }
136
+
137
+ #closeOne(subscription: ClientSubscription, cause: CanceledError | TimeoutError) {
138
+ if (subscription.isClosed) {
139
+ return;
140
+ }
141
+ subscription.isClosed = true;
142
+ this.#subscriptions.delete(subscription);
143
+
144
+ try {
145
+ subscription.request.closed?.(cause);
146
+ } catch (e) {
147
+ logger.error("Error canceling subscription", Diagnostic.strong(subscription.subscriptionId), e);
148
+ }
149
+ }
150
+
151
+ #timeOut(registration: ClientSubscription) {
152
+ logger.info(
153
+ "Subscription",
154
+ Diagnostic.strong(registration.subscriptionId),
155
+ "timed out after",
156
+ Diagnostic.strong(`${timeoutFor(registration)}ms`),
157
+ );
158
+
159
+ // TODO - rather than closing, put into dormant state and resubscribe when connection reestablishes
160
+ this.#closeOne(registration, new TimeoutError());
161
+ }
162
+ }
163
+
164
+ function timeoutFor(subscription: ClientSubscription) {
165
+ return subscription.maxInterval * 1000 + (subscription.request.maxPeerResponseTime ?? 0);
166
+ }
167
+
168
+ export namespace ClientSubscriptions {
169
+ export interface Listener {
170
+ (reports: AsyncIterable<ReadResult.Chunk>): Promise<void>;
171
+ }
172
+ }
@@ -0,0 +1,79 @@
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 { DecodedDataReport } from "#interaction/DecodedDataReport.js";
9
+ import { DataReport, Status, TlvAny } from "#types";
10
+
11
+ /**
12
+ * Converts a {@link DataReport} into a {@link ReadResult.Chunk}.
13
+ */
14
+ export function* InputChunk(input: DataReport): ReadResult.Chunk {
15
+ const report = DecodedDataReport(input);
16
+
17
+ for (const attr of report.attributeReports) {
18
+ yield {
19
+ kind: "attr-value",
20
+ tlv: TlvAny,
21
+ ...attr,
22
+ };
23
+ }
24
+
25
+ if (report.attributeStatus) {
26
+ for (const attr of report.attributeStatus) {
27
+ yield {
28
+ kind: "attr-status",
29
+ path: attr.path,
30
+ status: attr.status ?? Status.Failure, // TODO - attr.status shouldn't be optional?
31
+ clusterStatus: attr.clusterStatus,
32
+ };
33
+ }
34
+ }
35
+
36
+ for (const event of report.eventReports) {
37
+ for (const occurrence of event.events) {
38
+ yield {
39
+ kind: "event-value",
40
+ path: event.path,
41
+ value: occurrence,
42
+ number: occurrence.eventNumber,
43
+ priority: occurrence.priority,
44
+ timestamp: Number(
45
+ // TODO - this may not be useful, need to determine correct form
46
+ occurrence.epochTimestamp ??
47
+ occurrence.systemTimestamp ??
48
+ occurrence.deltaEpochTimestamp ??
49
+ occurrence.deltaSystemTimestamp ??
50
+ 0,
51
+ ),
52
+
53
+ // TODO - temporary, field will be removed
54
+ tlv: TlvAny,
55
+ };
56
+ }
57
+ }
58
+
59
+ if (report.eventStatus) {
60
+ for (const event of report.eventStatus) {
61
+ if (event.status !== undefined) {
62
+ yield {
63
+ kind: "event-status",
64
+ path: event.path,
65
+ status: event.status,
66
+ clusterStatus: event.clusterStatus,
67
+ };
68
+ }
69
+ if (event.clusterStatus !== undefined) {
70
+ yield {
71
+ kind: "event-status",
72
+ path: event.path,
73
+ status: event.status ?? Status.Failure,
74
+ clusterStatus: event.clusterStatus,
75
+ };
76
+ }
77
+ }
78
+ }
79
+ }
@@ -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 { Read } from "#action/request/Read.js";
8
+ import { ClusterId, EndpointNumber } from "#types";
9
+
10
+ /**
11
+ * This utility tells you whether a given endpoint/cluster is in scope for a read.
12
+ */
13
+ export interface ReadScope {
14
+ /**
15
+ * Is a cluster included in the read?
16
+ *
17
+ * This is useful to determine if the read should include a version filter.
18
+ */
19
+ isRelevant(endpoint: EndpointNumber, cluster: ClusterId): boolean;
20
+
21
+ /**
22
+ * Are all attributes in a cluster included in a read?
23
+ *
24
+ * This is useful to determine if the cluster's version should be updated in response to a read.
25
+ */
26
+ isWildcard(endpoint: EndpointNumber, cluster: ClusterId): boolean;
27
+ }
28
+
29
+ interface Check {
30
+ (endpoint: EndpointNumber, cluster: ClusterId): boolean;
31
+ }
32
+
33
+ export function ReadScope(read: Read): ReadScope {
34
+ return {
35
+ isRelevant(endpoint, cluster) {
36
+ this.isRelevant = generateScopeTester(read, "any");
37
+ return this.isRelevant(endpoint, cluster);
38
+ },
39
+
40
+ isWildcard(endpoint, cluster) {
41
+ this.isWildcard = generateScopeTester(read, "all");
42
+ return this.isWildcard(endpoint, cluster);
43
+ },
44
+ };
45
+ }
46
+
47
+ function generateScopeTester(read: Read, attrRequirement: "any" | "all"): Check {
48
+ if (!read.attributeRequests?.length) {
49
+ return isNever;
50
+ }
51
+
52
+ let wildcardEndpointClusters: undefined | Set<ClusterId>;
53
+ let specificEndpointClusters: undefined | Record<EndpointNumber, true | Set<ClusterId>>;
54
+
55
+ for (const { endpointId, clusterId, attributeId } of read.attributeRequests) {
56
+ // Ignore path if it addresses a specific attribute and we are only interested in wildcard attributes
57
+ if (attributeId !== undefined && attrRequirement === "all") {
58
+ continue;
59
+ }
60
+
61
+ // Wildcard endpoint
62
+ if (endpointId === undefined) {
63
+ // Full wildcard read; short-circuit all subsequent logic
64
+ if (clusterId === undefined) {
65
+ return isAlways;
66
+ }
67
+
68
+ // Wildcard cluster across all endpoints
69
+ (wildcardEndpointClusters ??= new Set()).add(clusterId);
70
+ continue;
71
+ }
72
+
73
+ // Specific endpoint with wildcard cluster cases
74
+ if (clusterId === undefined) {
75
+ (specificEndpointClusters ??= {})[endpointId] = true;
76
+ continue;
77
+ }
78
+
79
+ // Specific endpoint and cluster
80
+ if (attributeId === undefined) {
81
+ let ep = specificEndpointClusters?.[endpointId];
82
+ if (ep === true) {
83
+ continue;
84
+ }
85
+ if (ep === undefined) {
86
+ ep = (specificEndpointClusters ??= {})[endpointId] = new Set();
87
+ }
88
+ ep.add(clusterId);
89
+ }
90
+ }
91
+
92
+ return (endpoint, cluster) => {
93
+ if (wildcardEndpointClusters?.has(cluster)) {
94
+ return true;
95
+ }
96
+ const ep = specificEndpointClusters?.[endpoint];
97
+ return ep === true || !!ep?.has(cluster);
98
+ };
99
+ }
100
+
101
+ function isNever() {
102
+ return false;
103
+ }
104
+
105
+ function isAlways() {
106
+ return true;
107
+ }
@@ -5,3 +5,8 @@
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
+ export * from "./InputChunk.js";
12
+ export * from "./ReadScope.js";
@@ -23,11 +23,6 @@ import { Specifier } from "./Specifier.js";
23
23
  */
24
24
  export interface Read extends ReadRequest {}
25
25
 
26
- /**
27
- * Formulate a read request using Matter numeric IDs.
28
- */
29
- export function Read(options: Read.Options): Read;
30
-
31
26
  /**
32
27
  * Formulate a read request with extended options and name-based IDs.
33
28
  */
@@ -4,7 +4,8 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { UINT16_MAX } from "#general";
7
+ import type { ReadResult } from "#action/response/ReadResult.js";
8
+ import { CanceledError, TimeoutError, UINT16_MAX } from "#general";
8
9
  import { MalformedRequestError } from "./MalformedRequestError.js";
9
10
  import { Read } from "./Read.js";
10
11
 
@@ -18,10 +19,21 @@ export interface Subscribe extends Read {
18
19
  keepSubscriptions: boolean;
19
20
  minIntervalFloorSeconds?: number;
20
21
  maxIntervalCeilingSeconds?: number;
22
+ maxPeerResponseTime?: number;
23
+
24
+ /**
25
+ * Invoked when subscribed data changes.
26
+ */
27
+ updated?: (data: ReadResult) => Promise<void>;
28
+
29
+ /**
30
+ * Invoked when the subscription is no longer active.
31
+ */
32
+ closed?: (cause: CanceledError | TimeoutError) => void;
21
33
  }
22
34
 
23
- export function Subscribe(options: Subscribe.Options): Subscribe {
24
- const subscribe = Read(options) as unknown as Subscribe;
35
+ export function Subscribe(options: Subscribe.Options, ...selectors: Read.Selector[]): Subscribe {
36
+ const subscribe = Read(options, ...selectors) as unknown as Subscribe;
25
37
 
26
38
  const { keepSubscriptions, minIntervalFloorSeconds, maxIntervalCeilingSeconds } = options;
27
39
  subscribe.keepSubscriptions = keepSubscriptions ?? true;
@@ -48,6 +60,8 @@ export namespace Subscribe {
48
60
  keepSubscriptions?: boolean;
49
61
  minIntervalFloorSeconds?: number;
50
62
  maxIntervalCeilingSeconds?: number;
63
+ update?: Subscribe["updated"];
64
+ closed?: Subscribe["closed"];
51
65
  }
52
66
  }
53
67
 
@@ -5,10 +5,9 @@
5
5
  */
6
6
 
7
7
  import { SubscribeResponse } from "#types";
8
- import { ReadResult } from "./ReadResult.js";
9
8
 
10
- export interface SubscribeResult extends AsyncIterator<SubscribeResult.Chunk> {}
9
+ export type SubscribeResult = Promise<ActiveSubscription>;
11
10
 
12
- export namespace SubscribeResult {
13
- export type Chunk = ReadResult.Chunk | SubscribeResponse;
11
+ export interface ActiveSubscription extends SubscribeResponse {
12
+ close(): void;
14
13
  }
@@ -183,7 +183,6 @@ export abstract class FailsafeContext {
183
183
  }
184
184
 
185
185
  async close() {
186
- await this.#construction;
187
186
  await this.#construction.close(async () => {
188
187
  if (this.#failsafe) {
189
188
  await this.#failsafe.close();
@@ -5,7 +5,16 @@
5
5
  */
6
6
 
7
7
  import { CertificateAuthority } from "#certificate/CertificateAuthority.js";
8
- import { Bytes, CRYPTO_SYMMETRIC_KEY_LENGTH, Environment, Environmental, ImplementationError, Logger } from "#general";
8
+ import {
9
+ Bytes,
10
+ Construction,
11
+ CRYPTO_SYMMETRIC_KEY_LENGTH,
12
+ Environment,
13
+ Environmental,
14
+ ImplementationError,
15
+ Logger,
16
+ Observable,
17
+ } from "#general";
9
18
  import { CaseAuthenticatedTag, FabricId, FabricIndex, NodeId, VendorId } from "#types";
10
19
  import { Fabric, FabricBuilder } from "./Fabric.js";
11
20
  import { FabricManager } from "./FabricManager.js";
@@ -48,14 +57,25 @@ export const DEFAULT_FABRIC_ID = FabricId(1);
48
57
  * Manages fabrics controlled locally associated with a specific CA.
49
58
  */
50
59
  export class FabricAuthority {
60
+ #construction: Construction<FabricAuthority>;
51
61
  #ca: CertificateAuthority;
52
62
  #fabrics: FabricManager;
53
63
  #config: FabricAuthorityConfiguration;
64
+ #fabricAdded = new Observable<[Fabric]>();
54
65
 
55
66
  constructor({ ca, fabrics, config }: FabricAuthorityContext) {
56
67
  this.#ca = ca;
57
68
  this.#fabrics = fabrics;
58
69
  this.#config = config;
70
+
71
+ this.#construction = Construction(this, async () => {
72
+ await this.#ca.construction;
73
+ await this.#fabrics.construction;
74
+ });
75
+ }
76
+
77
+ get construction() {
78
+ return this.#construction;
59
79
  }
60
80
 
61
81
  /**
@@ -89,6 +109,13 @@ export class FabricAuthority {
89
109
  return Array.from(this.#fabrics).filter(this.hasControlOf.bind(this));
90
110
  }
91
111
 
112
+ /**
113
+ * Emits after creating a new fabric.
114
+ */
115
+ get fabricAdded() {
116
+ return this.#fabricAdded;
117
+ }
118
+
92
119
  /**
93
120
  * Determine whether a fabric belongs to this authority.
94
121
  */
@@ -137,6 +164,7 @@ export class FabricAuthority {
137
164
  this.#fabrics.addFabric(fabric);
138
165
 
139
166
  logger.debug(`Created new controller fabric ${index}`);
167
+ this.#fabricAdded.emit(fabric);
140
168
 
141
169
  return fabric;
142
170
  }