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