@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.
- package/dist/cjs/action/Val.d.ts +4 -0
- package/dist/cjs/action/Val.d.ts.map +1 -1
- package/dist/cjs/action/Val.js.map +1 -1
- package/dist/cjs/action/client/ClientInteraction.d.ts +10 -8
- package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/cjs/action/client/ClientInteraction.js +152 -80
- package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
- package/dist/cjs/action/client/ClientSubscription.d.ts +17 -0
- package/dist/cjs/action/client/ClientSubscription.d.ts.map +1 -0
- package/dist/cjs/action/client/ClientSubscription.js +22 -0
- package/dist/cjs/action/client/ClientSubscription.js.map +6 -0
- package/dist/cjs/action/client/ClientSubscriptionHandler.d.ts +20 -0
- package/dist/cjs/action/client/ClientSubscriptionHandler.d.ts.map +1 -0
- package/dist/cjs/action/client/ClientSubscriptionHandler.js +117 -0
- package/dist/cjs/action/client/ClientSubscriptionHandler.js.map +6 -0
- package/dist/cjs/action/client/ClientSubscriptions.d.ts +45 -0
- package/dist/cjs/action/client/ClientSubscriptions.d.ts.map +1 -0
- package/dist/cjs/action/client/ClientSubscriptions.js +141 -0
- package/dist/cjs/action/client/ClientSubscriptions.js.map +6 -0
- package/dist/cjs/action/client/InputChunk.d.ts +12 -0
- package/dist/cjs/action/client/InputChunk.d.ts.map +1 -0
- package/dist/cjs/action/client/InputChunk.js +89 -0
- package/dist/cjs/action/client/InputChunk.js.map +6 -0
- package/dist/cjs/action/client/ReadScope.d.ts +26 -0
- package/dist/cjs/action/client/ReadScope.d.ts.map +1 -0
- package/dist/cjs/action/client/ReadScope.js +87 -0
- package/dist/cjs/action/client/ReadScope.js.map +6 -0
- package/dist/cjs/action/client/index.d.ts +5 -0
- package/dist/cjs/action/client/index.d.ts.map +1 -1
- package/dist/cjs/action/client/index.js +5 -0
- package/dist/cjs/action/client/index.js.map +1 -1
- package/dist/cjs/action/request/Read.d.ts +0 -4
- package/dist/cjs/action/request/Read.d.ts.map +1 -1
- package/dist/cjs/action/request/Read.js.map +1 -1
- package/dist/cjs/action/request/Subscribe.d.ts +14 -1
- package/dist/cjs/action/request/Subscribe.d.ts.map +1 -1
- package/dist/cjs/action/request/Subscribe.js +2 -2
- package/dist/cjs/action/request/Subscribe.js.map +1 -1
- package/dist/cjs/action/response/SubscribeResult.d.ts +3 -5
- package/dist/cjs/action/response/SubscribeResult.d.ts.map +1 -1
- package/dist/cjs/common/FailsafeContext.d.ts.map +1 -1
- package/dist/cjs/common/FailsafeContext.js +0 -1
- package/dist/cjs/common/FailsafeContext.js.map +1 -1
- package/dist/cjs/fabric/FabricAuthority.d.ts +6 -1
- package/dist/cjs/fabric/FabricAuthority.d.ts.map +1 -1
- package/dist/cjs/fabric/FabricAuthority.js +16 -0
- package/dist/cjs/fabric/FabricAuthority.js.map +1 -1
- package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionClient.js +8 -4
- package/dist/cjs/interaction/InteractionClient.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts +20 -16
- package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +18 -10
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/cjs/mdns/MdnsScanner.d.ts +5 -6
- package/dist/cjs/mdns/MdnsScanner.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsScanner.js.map +1 -1
- package/dist/cjs/protocol/DeviceCommissioner.d.ts +1 -1
- package/dist/cjs/protocol/DeviceCommissioner.d.ts.map +1 -1
- package/dist/cjs/protocol/DeviceCommissioner.js +2 -5
- package/dist/cjs/protocol/DeviceCommissioner.js.map +1 -1
- package/dist/cjs/securechannel/SecureChannelMessenger.d.ts +3 -0
- package/dist/cjs/securechannel/SecureChannelMessenger.d.ts.map +1 -1
- package/dist/cjs/securechannel/SecureChannelMessenger.js +4 -0
- package/dist/cjs/securechannel/SecureChannelMessenger.js.map +1 -1
- package/dist/esm/action/Val.d.ts +4 -0
- package/dist/esm/action/Val.d.ts.map +1 -1
- package/dist/esm/action/Val.js.map +1 -1
- package/dist/esm/action/client/ClientInteraction.d.ts +10 -8
- package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/esm/action/client/ClientInteraction.js +154 -82
- package/dist/esm/action/client/ClientInteraction.js.map +1 -1
- package/dist/esm/action/client/ClientSubscription.d.ts +17 -0
- package/dist/esm/action/client/ClientSubscription.d.ts.map +1 -0
- package/dist/esm/action/client/ClientSubscription.js +6 -0
- package/dist/esm/action/client/ClientSubscription.js.map +6 -0
- package/dist/esm/action/client/ClientSubscriptionHandler.d.ts +20 -0
- package/dist/esm/action/client/ClientSubscriptionHandler.d.ts.map +1 -0
- package/dist/esm/action/client/ClientSubscriptionHandler.js +97 -0
- package/dist/esm/action/client/ClientSubscriptionHandler.js.map +6 -0
- package/dist/esm/action/client/ClientSubscriptions.d.ts +45 -0
- package/dist/esm/action/client/ClientSubscriptions.d.ts.map +1 -0
- package/dist/esm/action/client/ClientSubscriptions.js +129 -0
- package/dist/esm/action/client/ClientSubscriptions.js.map +6 -0
- package/dist/esm/action/client/InputChunk.d.ts +12 -0
- package/dist/esm/action/client/InputChunk.d.ts.map +1 -0
- package/dist/esm/action/client/InputChunk.js +69 -0
- package/dist/esm/action/client/InputChunk.js.map +6 -0
- package/dist/esm/action/client/ReadScope.d.ts +26 -0
- package/dist/esm/action/client/ReadScope.d.ts.map +1 -0
- package/dist/esm/action/client/ReadScope.js +67 -0
- package/dist/esm/action/client/ReadScope.js.map +6 -0
- package/dist/esm/action/client/index.d.ts +5 -0
- package/dist/esm/action/client/index.d.ts.map +1 -1
- package/dist/esm/action/client/index.js +5 -0
- package/dist/esm/action/client/index.js.map +1 -1
- package/dist/esm/action/request/Read.d.ts +0 -4
- package/dist/esm/action/request/Read.d.ts.map +1 -1
- package/dist/esm/action/request/Read.js.map +1 -1
- package/dist/esm/action/request/Subscribe.d.ts +14 -1
- package/dist/esm/action/request/Subscribe.d.ts.map +1 -1
- package/dist/esm/action/request/Subscribe.js +2 -2
- package/dist/esm/action/request/Subscribe.js.map +1 -1
- package/dist/esm/action/response/SubscribeResult.d.ts +3 -5
- package/dist/esm/action/response/SubscribeResult.d.ts.map +1 -1
- package/dist/esm/common/FailsafeContext.d.ts.map +1 -1
- package/dist/esm/common/FailsafeContext.js +0 -1
- package/dist/esm/common/FailsafeContext.js.map +1 -1
- package/dist/esm/fabric/FabricAuthority.d.ts +6 -1
- package/dist/esm/fabric/FabricAuthority.d.ts.map +1 -1
- package/dist/esm/fabric/FabricAuthority.js +25 -1
- package/dist/esm/fabric/FabricAuthority.js.map +1 -1
- package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionClient.js +8 -4
- package/dist/esm/interaction/InteractionClient.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts +20 -16
- package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +18 -10
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/mdns/MdnsScanner.d.ts +5 -6
- package/dist/esm/mdns/MdnsScanner.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsScanner.js.map +1 -1
- package/dist/esm/protocol/DeviceCommissioner.d.ts +1 -1
- package/dist/esm/protocol/DeviceCommissioner.d.ts.map +1 -1
- package/dist/esm/protocol/DeviceCommissioner.js +2 -5
- package/dist/esm/protocol/DeviceCommissioner.js.map +1 -1
- package/dist/esm/securechannel/SecureChannelMessenger.d.ts +3 -0
- package/dist/esm/securechannel/SecureChannelMessenger.d.ts.map +1 -1
- package/dist/esm/securechannel/SecureChannelMessenger.js +4 -0
- package/dist/esm/securechannel/SecureChannelMessenger.js.map +1 -1
- package/package.json +6 -6
- package/src/action/Val.ts +5 -0
- package/src/action/client/ClientInteraction.ts +178 -90
- package/src/action/client/ClientSubscription.ts +18 -0
- package/src/action/client/ClientSubscriptionHandler.ts +137 -0
- package/src/action/client/ClientSubscriptions.ts +172 -0
- package/src/action/client/InputChunk.ts +79 -0
- package/src/action/client/ReadScope.ts +107 -0
- package/src/action/client/index.ts +5 -0
- package/src/action/request/Read.ts +0 -5
- package/src/action/request/Subscribe.ts +17 -3
- package/src/action/response/SubscribeResult.ts +3 -4
- package/src/common/FailsafeContext.ts +0 -1
- package/src/fabric/FabricAuthority.ts +29 -1
- package/src/interaction/InteractionClient.ts +8 -4
- package/src/interaction/InteractionMessenger.ts +18 -11
- package/src/mdns/MdnsScanner.ts +5 -6
- package/src/protocol/DeviceCommissioner.ts +2 -7
- 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
|
+
}
|
|
@@ -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 {
|
|
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
|
|
9
|
+
export type SubscribeResult = Promise<ActiveSubscription>;
|
|
11
10
|
|
|
12
|
-
export
|
|
13
|
-
|
|
11
|
+
export interface ActiveSubscription extends SubscribeResponse {
|
|
12
|
+
close(): void;
|
|
14
13
|
}
|
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { CertificateAuthority } from "#certificate/CertificateAuthority.js";
|
|
8
|
-
import {
|
|
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
|
}
|