@matter/protocol 0.17.3 → 0.17.4-alpha.0-20260623-5647634fc
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/client/ClientInteraction.d.ts +2 -0
- package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/cjs/action/client/ClientInteraction.js +6 -4
- package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
- package/dist/cjs/peer/Peer.d.ts +10 -1
- package/dist/cjs/peer/Peer.d.ts.map +1 -1
- package/dist/cjs/peer/Peer.js +15 -5
- package/dist/cjs/peer/Peer.js.map +1 -1
- package/dist/cjs/peer/PeerAddressMonitor.d.ts +19 -1
- package/dist/cjs/peer/PeerAddressMonitor.d.ts.map +1 -1
- package/dist/cjs/peer/PeerAddressMonitor.js +99 -65
- package/dist/cjs/peer/PeerAddressMonitor.js.map +1 -1
- package/dist/cjs/peer/PeerExchangeProvider.d.ts +5 -1
- package/dist/cjs/peer/PeerExchangeProvider.d.ts.map +1 -1
- package/dist/cjs/peer/PeerExchangeProvider.js +3 -0
- package/dist/cjs/peer/PeerExchangeProvider.js.map +1 -1
- package/dist/cjs/peer/PeerTimingParameters.d.ts +2 -2
- package/dist/cjs/peer/PeerTimingParameters.d.ts.map +1 -1
- package/dist/cjs/peer/PeerTimingParameters.js +2 -2
- package/dist/cjs/peer/PeerTimingParameters.js.map +1 -1
- package/dist/cjs/protocol/ExchangeProvider.d.ts +11 -0
- package/dist/cjs/protocol/ExchangeProvider.d.ts.map +1 -1
- package/dist/cjs/protocol/ExchangeProvider.js +8 -0
- package/dist/cjs/protocol/ExchangeProvider.js.map +1 -1
- package/dist/esm/action/client/ClientInteraction.d.ts +2 -0
- package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/esm/action/client/ClientInteraction.js +6 -4
- package/dist/esm/action/client/ClientInteraction.js.map +1 -1
- package/dist/esm/peer/Peer.d.ts +10 -1
- package/dist/esm/peer/Peer.d.ts.map +1 -1
- package/dist/esm/peer/Peer.js +15 -5
- package/dist/esm/peer/Peer.js.map +1 -1
- package/dist/esm/peer/PeerAddressMonitor.d.ts +19 -1
- package/dist/esm/peer/PeerAddressMonitor.d.ts.map +1 -1
- package/dist/esm/peer/PeerAddressMonitor.js +100 -65
- package/dist/esm/peer/PeerAddressMonitor.js.map +1 -1
- package/dist/esm/peer/PeerExchangeProvider.d.ts +5 -1
- package/dist/esm/peer/PeerExchangeProvider.d.ts.map +1 -1
- package/dist/esm/peer/PeerExchangeProvider.js +3 -0
- package/dist/esm/peer/PeerExchangeProvider.js.map +1 -1
- package/dist/esm/peer/PeerTimingParameters.d.ts +2 -2
- package/dist/esm/peer/PeerTimingParameters.d.ts.map +1 -1
- package/dist/esm/peer/PeerTimingParameters.js +3 -3
- package/dist/esm/peer/PeerTimingParameters.js.map +1 -1
- package/dist/esm/protocol/ExchangeProvider.d.ts +11 -0
- package/dist/esm/protocol/ExchangeProvider.d.ts.map +1 -1
- package/dist/esm/protocol/ExchangeProvider.js +8 -0
- package/dist/esm/protocol/ExchangeProvider.js.map +1 -1
- package/package.json +5 -5
- package/src/action/client/ClientInteraction.ts +7 -4
- package/src/peer/Peer.ts +20 -8
- package/src/peer/PeerAddressMonitor.ts +127 -92
- package/src/peer/PeerExchangeProvider.ts +5 -1
- package/src/peer/PeerTimingParameters.ts +5 -5
- package/src/protocol/ExchangeProvider.ts +12 -0
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { ReachabilityReason } from "#protocol/ExchangeProvider.js";
|
|
7
8
|
import {
|
|
9
|
+
Abort,
|
|
8
10
|
ChannelType,
|
|
9
11
|
Diagnostic,
|
|
10
12
|
Duration,
|
|
@@ -21,6 +23,9 @@ import { PeerUnresponsiveError } from "./PeerCommunicationError.js";
|
|
|
21
23
|
|
|
22
24
|
const logger = Logger.get("PeerAddressMonitor");
|
|
23
25
|
|
|
26
|
+
/** A liveness probe does not need the full MRP retransmission budget. */
|
|
27
|
+
const PROBE_MAX_RETRANSMISSIONS = 2;
|
|
28
|
+
|
|
24
29
|
/**
|
|
25
30
|
* Monitors mDNS-discovered addresses for a peer and probes the current session when the session's IP
|
|
26
31
|
* disappears from discovered addresses.
|
|
@@ -41,12 +46,11 @@ export class PeerAddressMonitor {
|
|
|
41
46
|
readonly #timer: Timer;
|
|
42
47
|
readonly #cooldownMin: Duration;
|
|
43
48
|
readonly #cooldownMax: Duration;
|
|
44
|
-
readonly #trackWork: (work: PromiseLike<
|
|
45
|
-
#probing?: Promise<
|
|
49
|
+
readonly #trackWork: (work: PromiseLike<unknown>) => void;
|
|
50
|
+
#probing?: Promise<boolean>;
|
|
46
51
|
|
|
47
|
-
// Fibonacci backoff state — resets
|
|
52
|
+
// Fibonacci backoff state — grows on each successful probe, resets only on a real reachability scare
|
|
48
53
|
#lastProbeAt?: Timestamp;
|
|
49
|
-
#lastProbedIp?: string;
|
|
50
54
|
#fibPrev: Duration = 0;
|
|
51
55
|
#fibCurr: Duration = 0;
|
|
52
56
|
|
|
@@ -55,7 +59,7 @@ export class PeerAddressMonitor {
|
|
|
55
59
|
stabilizationDelay: Duration,
|
|
56
60
|
probeCooldown: { minimum: Duration; maximum: Duration },
|
|
57
61
|
abort: AbortSignal,
|
|
58
|
-
trackWork: (work: PromiseLike<
|
|
62
|
+
trackWork: (work: PromiseLike<unknown>) => void,
|
|
59
63
|
) {
|
|
60
64
|
this.#peer = peer;
|
|
61
65
|
this.#abort = abort;
|
|
@@ -63,15 +67,7 @@ export class PeerAddressMonitor {
|
|
|
63
67
|
this.#cooldownMax = probeCooldown.maximum;
|
|
64
68
|
this.#trackWork = trackWork;
|
|
65
69
|
this.#timer = Time.getTimer("address check stabilization", stabilizationDelay, () => {
|
|
66
|
-
|
|
67
|
-
this.#probing = this.#check().finally(() => {
|
|
68
|
-
this.#probing = undefined;
|
|
69
|
-
|
|
70
|
-
// If address changes arrived during the probe, re-check
|
|
71
|
-
this.schedule();
|
|
72
|
-
});
|
|
73
|
-
this.#trackWork(this.#probing);
|
|
74
|
-
}
|
|
70
|
+
this.#trackWork(this.verifyReachability({ reason: "address-change", abort: this.#abort }));
|
|
75
71
|
});
|
|
76
72
|
}
|
|
77
73
|
|
|
@@ -94,110 +90,149 @@ export class PeerAddressMonitor {
|
|
|
94
90
|
this.#timer.stop();
|
|
95
91
|
}
|
|
96
92
|
|
|
97
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Tear down: stop the timer and await any in-flight run so teardown does not race a probe. The
|
|
95
|
+
* caller is expected to have aborted the peer first, so the in-flight run unwinds promptly.
|
|
96
|
+
*/
|
|
97
|
+
async close() {
|
|
98
|
+
this.#timer.stop();
|
|
99
|
+
await this.#probing;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Verify the peer is reachable, driving recovery. Coalesces concurrent triggers into one run.
|
|
104
|
+
*
|
|
105
|
+
* An `address-change` run probes the current address then walks discovered alternates (migrating in
|
|
106
|
+
* place on success); a `session-suspect` run only probes the current address and never walks alternates.
|
|
107
|
+
* Returns `true` if the session is usable afterward (current address answered, or an alternate
|
|
108
|
+
* answered and the session migrated); `false` if no address answered and the session was closed.
|
|
109
|
+
*/
|
|
110
|
+
async verifyReachability(options: { reason: ReachabilityReason; abort?: AbortSignal }): Promise<boolean> {
|
|
111
|
+
if (this.#probing === undefined) {
|
|
112
|
+
this.#probing = this.#run(options.reason).finally(() => {
|
|
113
|
+
this.#probing = undefined;
|
|
114
|
+
// Only an address-change run may have coalesced newer mDNS changes worth re-checking;
|
|
115
|
+
// a lone session-suspect run must not arm the debounce timer.
|
|
116
|
+
if (options.reason === "address-change") {
|
|
117
|
+
this.schedule();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const run = this.#probing;
|
|
123
|
+
// A joiner waits on the shared run but bails on its own abort without cancelling it for others.
|
|
124
|
+
return options.abort !== undefined && options.abort !== this.#abort
|
|
125
|
+
? ((await Abort.race(options.abort, run)) ?? false)
|
|
126
|
+
: run;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async #run(reason: ReachabilityReason): Promise<boolean> {
|
|
130
|
+
const abort = this.#abort;
|
|
98
131
|
const session = this.#peer.newestSession();
|
|
99
132
|
const interaction = this.#peer.interaction;
|
|
100
133
|
if (!session || !interaction) {
|
|
101
|
-
return;
|
|
134
|
+
return false;
|
|
102
135
|
}
|
|
103
136
|
|
|
104
137
|
const channel = session.channel.transportChannel;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// TCP has 1:1 session-connection binding plus OS keep-alive — connection drops evict
|
|
110
|
-
// the session directly, so probing the address is unnecessary.
|
|
111
|
-
if (channel.type === ChannelType.TCP) {
|
|
112
|
-
return;
|
|
138
|
+
// TCP/non-IP sessions are evicted when their connection drops, so address probing is moot.
|
|
139
|
+
if (!isIpNetworkChannel(channel) || channel.type === ChannelType.TCP) {
|
|
140
|
+
return true;
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
const currentAddress = channel.networkAddress;
|
|
116
|
-
const currentIp = currentAddress.ip;
|
|
117
144
|
const discoveredAddresses = this.#peer.service.addresses;
|
|
118
145
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// If the current address is still in the discovered set, all good
|
|
125
|
-
if (discoveredAddresses.has(currentAddress)) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const via = session.via;
|
|
130
|
-
|
|
131
|
-
// Reset backoff if the IP we're being asked about differs from last time
|
|
132
|
-
if (currentIp !== this.#lastProbedIp) {
|
|
146
|
+
if (reason === "session-suspect") {
|
|
147
|
+
// A subscription timeout is real evidence of trouble — drop accumulated trust so we probe now.
|
|
133
148
|
this.#resetBackoff();
|
|
134
149
|
}
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
if (reason === "address-change") {
|
|
152
|
+
// Only probe when the current address dropped out of the discovered set. Match by ip/port:
|
|
153
|
+
// discovered addresses are channel-agnostic, so `has()` (keyed by URL incl. protocol) would always miss.
|
|
154
|
+
const stillDiscovered = [...discoveredAddresses].some(addr => ServerAddress.isEqual(addr, currentAddress));
|
|
155
|
+
if (!discoveredAddresses.size || stillDiscovered) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
const lastKnownGood = Timestamp(Math.max(this.#lastProbeAt ?? 0, session.activeTimestamp));
|
|
159
|
+
if (lastKnownGood > 0 && Timestamp.delta(lastKnownGood) < this.#currentCooldown) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
logger.info(
|
|
163
|
+
session.via,
|
|
164
|
+
"Session address",
|
|
165
|
+
Diagnostic.strong(ServerAddress.urlFor(currentAddress)),
|
|
166
|
+
"no longer in mDNS results, probing",
|
|
167
|
+
);
|
|
168
|
+
this.#lastProbeAt = Time.nowMs;
|
|
141
169
|
}
|
|
142
170
|
|
|
143
|
-
logger.info(
|
|
144
|
-
via,
|
|
145
|
-
"Session address",
|
|
146
|
-
Diagnostic.strong(ServerAddress.urlFor(currentAddress)),
|
|
147
|
-
"no longer in mDNS results, probing",
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
this.#lastProbeAt = Time.nowMs;
|
|
151
|
-
this.#lastProbedIp = currentIp;
|
|
152
|
-
|
|
153
|
-
// Get probe network profile
|
|
154
171
|
const network = this.#peer.network;
|
|
155
172
|
const probeNetwork = network.probeAddress ?? network;
|
|
156
173
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
if (
|
|
175
|
+
await interaction.probe({
|
|
176
|
+
network: probeNetwork.id,
|
|
177
|
+
abort,
|
|
178
|
+
suppressPeerLoss: true,
|
|
179
|
+
maxRetransmissions: PROBE_MAX_RETRANSMISSIONS,
|
|
180
|
+
})
|
|
181
|
+
) {
|
|
182
|
+
if (reason === "address-change") {
|
|
183
|
+
this.#advanceBackoff();
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
161
186
|
}
|
|
162
187
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
// Only an mDNS address change walks alternates; a session-suspect probe stays on the current address.
|
|
189
|
+
if (reason === "address-change") {
|
|
190
|
+
for (const ipAddress of discoveredAddresses) {
|
|
191
|
+
if (abort.aborted) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (ServerAddress.isEqual(ipAddress, currentAddress)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const address: ServerAddressUdp = { ...ipAddress, type: "udp" };
|
|
198
|
+
if (
|
|
199
|
+
await interaction.probe({
|
|
200
|
+
network: probeNetwork.id,
|
|
201
|
+
abort,
|
|
202
|
+
addressOverride: address,
|
|
203
|
+
suppressPeerLoss: true,
|
|
204
|
+
maxRetransmissions: PROBE_MAX_RETRANSMISSIONS,
|
|
205
|
+
})
|
|
206
|
+
) {
|
|
207
|
+
logger.info(
|
|
208
|
+
session.via,
|
|
209
|
+
"Discovered address reachable, migrating session to",
|
|
210
|
+
Diagnostic.strong(ServerAddress.urlFor(address)),
|
|
211
|
+
);
|
|
212
|
+
session.channel.networkAddress = address;
|
|
213
|
+
// Subscriptions still target the old address for device->client reports; close them so they
|
|
214
|
+
// re-establish and hand the device our new socket.
|
|
215
|
+
interaction.subscriptions.closeForPeer(this.#peer.address);
|
|
216
|
+
this.#resetBackoff();
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
189
219
|
}
|
|
220
|
+
this.#resetBackoff();
|
|
190
221
|
}
|
|
191
222
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (this.#abort.aborted) {
|
|
195
|
-
return;
|
|
223
|
+
if (abort.aborted) {
|
|
224
|
+
return false;
|
|
196
225
|
}
|
|
197
226
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
227
|
+
logger.info(session.via, "All probes failed, closing session");
|
|
228
|
+
try {
|
|
229
|
+
await session.handlePeerLoss({ cause: new PeerUnresponsiveError() });
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// Best-effort recovery: a failure to close the unreachable session must not reject the run
|
|
232
|
+
// (which teardown awaits) — log and move on.
|
|
233
|
+
logger.warn(session.via, "Error closing unreachable session", error);
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
201
236
|
}
|
|
202
237
|
|
|
203
238
|
/** Current cooldown duration based on Fibonacci position. */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { PeerAddress } from "#peer/PeerAddress.js";
|
|
8
|
-
import { ExchangeProvider, NewExchangeOptions } from "#protocol/ExchangeProvider.js";
|
|
8
|
+
import { ExchangeProvider, NewExchangeOptions, type ReachabilityReason } from "#protocol/ExchangeProvider.js";
|
|
9
9
|
import type { MessageExchange } from "#protocol/MessageExchange.js";
|
|
10
10
|
import { MRP } from "#protocol/MRP.js";
|
|
11
11
|
import { ChannelType, Duration, InternalError } from "@matter/general";
|
|
@@ -58,6 +58,10 @@ export class PeerExchangeProvider extends ExchangeProvider {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
override verifyReachability(options: { reason: ReachabilityReason; abort?: AbortSignal }): Promise<boolean> {
|
|
62
|
+
return this.#peer.verifyReachability(options);
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
override async initiateExchange(options?: NewExchangeOptions): Promise<MessageExchange> {
|
|
62
66
|
const abort = options?.abort;
|
|
63
67
|
const isGroup = PeerAddress.isGroup(this.#peer.address);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Duration, merge as mergeObjects, Millis, Minutes, Seconds } from "@matter/general";
|
|
7
|
+
import { Duration, Hours, merge as mergeObjects, Millis, Minutes, Seconds } from "@matter/general";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Parameters that control network timing for Matter sessions controlled by matter.js.
|
|
@@ -105,11 +105,11 @@ export interface PeerTimingParameters {
|
|
|
105
105
|
addressChangeStabilizationDelay: Duration;
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Probe cooldown range for address-change probes
|
|
108
|
+
* Probe cooldown range for address-change probes.
|
|
109
109
|
*
|
|
110
110
|
* When mDNS keeps reporting the session IP as gone but probes succeed, the cooldown grows
|
|
111
111
|
* using a Fibonacci-like sequence from {@link minimum} to {@link maximum}. The cooldown
|
|
112
|
-
* resets
|
|
112
|
+
* resets on a probe failure or a subscription-liveness scare, not on a mere address change.
|
|
113
113
|
*
|
|
114
114
|
* Probes are also suppressed while the session is actively receiving data — the cooldown
|
|
115
115
|
* is measured from whichever is more recent: the last probe or the last received message.
|
|
@@ -183,8 +183,8 @@ export namespace PeerTimingParameters {
|
|
|
183
183
|
},
|
|
184
184
|
addressChangeStabilizationDelay: Seconds(10),
|
|
185
185
|
addressChangeProbeCooldown: {
|
|
186
|
-
minimum: Minutes(
|
|
187
|
-
maximum:
|
|
186
|
+
minimum: Minutes(10),
|
|
187
|
+
maximum: Hours(24),
|
|
188
188
|
},
|
|
189
189
|
};
|
|
190
190
|
}
|
|
@@ -61,6 +61,9 @@ export interface NewExchangeOptions extends Omit<InteractionSettings, "transacti
|
|
|
61
61
|
preferredTransport?: ChannelType;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Why a reachability check was requested. Gates entry conditions, not the probe mechanics. */
|
|
65
|
+
export type ReachabilityReason = "address-change" | "session-suspect";
|
|
66
|
+
|
|
64
67
|
/**
|
|
65
68
|
* Interface for obtaining a message exchange with a specific peer.
|
|
66
69
|
*/
|
|
@@ -89,6 +92,15 @@ export abstract class ExchangeProvider {
|
|
|
89
92
|
* The default implementation is a no-op (already connected).
|
|
90
93
|
*/
|
|
91
94
|
async connect(_options?: NewExchangeOptions): Promise<void> {}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Verify the peer is reachable, driving recovery (alternate-address migration or session close).
|
|
98
|
+
*
|
|
99
|
+
* The default reports reachable; only peer-backed providers have a reachability authority and override this.
|
|
100
|
+
*/
|
|
101
|
+
async verifyReachability(_options: { reason: ReachabilityReason; abort?: AbortSignal }): Promise<boolean> {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
/**
|