@matter/protocol 0.17.7 → 0.17.8-alpha.0-20260801-92169d0aa
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/interaction/Subscription.d.ts +8 -2
- package/dist/cjs/interaction/Subscription.d.ts.map +1 -1
- package/dist/cjs/interaction/Subscription.js.map +1 -1
- package/dist/cjs/peer/RebootResubscribeArmer.js +1 -1
- package/dist/cjs/peer/RebootResubscribeArmer.js.map +1 -1
- package/dist/cjs/protocol/DeviceAdvertiser.js +2 -2
- package/dist/cjs/protocol/DeviceAdvertiser.js.map +1 -1
- package/dist/cjs/protocol/ExchangeManager.d.ts.map +1 -1
- package/dist/cjs/protocol/ExchangeManager.js +5 -1
- package/dist/cjs/protocol/ExchangeManager.js.map +1 -1
- package/dist/cjs/protocol/MessageExchange.d.ts +4 -2
- package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/cjs/protocol/MessageExchange.js +13 -3
- package/dist/cjs/protocol/MessageExchange.js.map +1 -1
- package/dist/cjs/session/NodeSession.d.ts +1 -1
- package/dist/cjs/session/NodeSession.d.ts.map +1 -1
- package/dist/cjs/session/NodeSession.js +27 -7
- package/dist/cjs/session/NodeSession.js.map +1 -1
- package/dist/cjs/session/Session.d.ts +1 -1
- package/dist/cjs/session/Session.d.ts.map +1 -1
- package/dist/cjs/session/Session.js +2 -2
- package/dist/cjs/session/Session.js.map +1 -1
- package/dist/cjs/session/SessionManager.d.ts +7 -2
- package/dist/cjs/session/SessionManager.d.ts.map +1 -1
- package/dist/cjs/session/SessionManager.js +54 -9
- package/dist/cjs/session/SessionManager.js.map +1 -1
- package/dist/esm/interaction/Subscription.d.ts +8 -2
- package/dist/esm/interaction/Subscription.d.ts.map +1 -1
- package/dist/esm/interaction/Subscription.js.map +1 -1
- package/dist/esm/peer/RebootResubscribeArmer.js +1 -1
- package/dist/esm/peer/RebootResubscribeArmer.js.map +1 -1
- package/dist/esm/protocol/DeviceAdvertiser.js +2 -2
- package/dist/esm/protocol/DeviceAdvertiser.js.map +1 -1
- package/dist/esm/protocol/ExchangeManager.d.ts.map +1 -1
- package/dist/esm/protocol/ExchangeManager.js +5 -1
- package/dist/esm/protocol/ExchangeManager.js.map +1 -1
- package/dist/esm/protocol/MessageExchange.d.ts +4 -2
- package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/esm/protocol/MessageExchange.js +13 -3
- package/dist/esm/protocol/MessageExchange.js.map +1 -1
- package/dist/esm/session/NodeSession.d.ts +1 -1
- package/dist/esm/session/NodeSession.d.ts.map +1 -1
- package/dist/esm/session/NodeSession.js +31 -8
- package/dist/esm/session/NodeSession.js.map +1 -1
- package/dist/esm/session/Session.d.ts +1 -1
- package/dist/esm/session/Session.d.ts.map +1 -1
- package/dist/esm/session/Session.js +2 -2
- package/dist/esm/session/Session.js.map +1 -1
- package/dist/esm/session/SessionManager.d.ts +7 -2
- package/dist/esm/session/SessionManager.d.ts.map +1 -1
- package/dist/esm/session/SessionManager.js +54 -9
- package/dist/esm/session/SessionManager.js.map +1 -1
- package/package.json +5 -5
- package/src/interaction/Subscription.ts +10 -2
- package/src/peer/RebootResubscribeArmer.ts +1 -1
- package/src/protocol/DeviceAdvertiser.ts +2 -2
- package/src/protocol/ExchangeManager.ts +5 -1
- package/src/protocol/MessageExchange.ts +18 -15
- package/src/session/NodeSession.ts +33 -9
- package/src/session/Session.ts +2 -2
- package/src/session/SessionManager.ts +77 -11
|
@@ -520,7 +520,11 @@ export class ExchangeManager implements Transport.Provider {
|
|
|
520
520
|
}
|
|
521
521
|
|
|
522
522
|
// Report peer loss to the session manager; this notifies all (relevant) sessions for the peer
|
|
523
|
-
await this.#sessions.handlePeerLoss(session.peerAddress,
|
|
523
|
+
await this.#sessions.handlePeerLoss(session.peerAddress, {
|
|
524
|
+
cause,
|
|
525
|
+
asOf: createdAt,
|
|
526
|
+
currentExchange: exchange,
|
|
527
|
+
});
|
|
524
528
|
},
|
|
525
529
|
|
|
526
530
|
retry: number => this.#sessions.retry.emit(session, number),
|
|
@@ -103,7 +103,7 @@ export interface ExchangeSendOptions {
|
|
|
103
103
|
/** Initial MRP retransmission time (default: calculated as by Matter specification) */
|
|
104
104
|
initialRetransmissionTime?: Duration;
|
|
105
105
|
|
|
106
|
-
/** Suppress peer-loss reporting
|
|
106
|
+
/** Suppress peer-loss reporting for this send. OR'd with {@link MessageExchange.Options.suppressPeerLoss}. */
|
|
107
107
|
suppressPeerLoss?: boolean;
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -119,7 +119,7 @@ export interface ExchangeReceiveOptions {
|
|
|
119
119
|
*/
|
|
120
120
|
expectedProcessingTime?: Duration;
|
|
121
121
|
|
|
122
|
-
/** Suppress peer-loss reporting
|
|
122
|
+
/** Suppress peer-loss reporting for this receive. OR'd with {@link MessageExchange.Options.suppressPeerLoss}. */
|
|
123
123
|
suppressPeerLoss?: boolean;
|
|
124
124
|
}
|
|
125
125
|
|
|
@@ -203,6 +203,7 @@ export class MessageExchange {
|
|
|
203
203
|
#onReceive?: MessageExchange.ReceiveNotifier;
|
|
204
204
|
readonly #addressOverride?: ServerAddressUdp;
|
|
205
205
|
readonly #peerAdditionalMrpDelay?: Duration;
|
|
206
|
+
readonly #suppressPeerLoss: boolean;
|
|
206
207
|
#receivedMessageToAck: Message | undefined;
|
|
207
208
|
#receivedMessageAckTimer = Time.getTimer("ack receipt timeout", MRP.STANDALONE_ACK_TIMEOUT, () => {
|
|
208
209
|
if (this.#receivedMessageToAck !== undefined) {
|
|
@@ -256,6 +257,7 @@ export class MessageExchange {
|
|
|
256
257
|
network,
|
|
257
258
|
peerAdditionalMrpDelay,
|
|
258
259
|
addressOverride,
|
|
260
|
+
suppressPeerLoss = false,
|
|
259
261
|
} = config;
|
|
260
262
|
|
|
261
263
|
this.#context = context;
|
|
@@ -269,6 +271,7 @@ export class MessageExchange {
|
|
|
269
271
|
this.#onReceive = onReceive;
|
|
270
272
|
this.#addressOverride = addressOverride;
|
|
271
273
|
this.#peerAdditionalMrpDelay = peerAdditionalMrpDelay;
|
|
274
|
+
this.#suppressPeerLoss = suppressPeerLoss;
|
|
272
275
|
|
|
273
276
|
const { activeThreshold, activeInterval, idleInterval } = this.session.parameters;
|
|
274
277
|
|
|
@@ -499,12 +502,8 @@ export class MessageExchange {
|
|
|
499
502
|
try {
|
|
500
503
|
await this.#send(messageType, payload);
|
|
501
504
|
} catch (e) {
|
|
502
|
-
// Only declare the peer as lost when this exchange has never received a response. If we already
|
|
503
|
-
// exchanged messages, the peer was reachable, and the later send-failure may be transient — declaring
|
|
504
|
-
// peer loss would unnecessarily close sessions and tear down subscriptions.
|
|
505
505
|
if (
|
|
506
|
-
|
|
507
|
-
this.#messageReceivedCounter === 0 &&
|
|
506
|
+
this.#reportsPeerLoss(options.suppressPeerLoss) &&
|
|
508
507
|
causedBy(e, TransientPeerCommunicationError, TimeoutError, NetworkError)
|
|
509
508
|
) {
|
|
510
509
|
await this.#context.peerLost(this, asError(e));
|
|
@@ -514,6 +513,14 @@ export class MessageExchange {
|
|
|
514
513
|
}
|
|
515
514
|
}
|
|
516
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Whether a failure here is evidence the peer is gone. Only if we never heard from it on this exchange: one that
|
|
518
|
+
* answered earlier was reachable, and peer loss closes every session with it.
|
|
519
|
+
*/
|
|
520
|
+
#reportsPeerLoss(suppressedForOperation?: boolean) {
|
|
521
|
+
return !this.#suppressPeerLoss && suppressedForOperation !== true && this.#messageReceivedCounter === 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
517
524
|
async #send(messageType: number, payload: Bytes, standaloneAckMessageId?: number) {
|
|
518
525
|
const {
|
|
519
526
|
expectAckOnly = false,
|
|
@@ -704,14 +711,7 @@ export class MessageExchange {
|
|
|
704
711
|
try {
|
|
705
712
|
return await this.#nextMessage(options);
|
|
706
713
|
} catch (e) {
|
|
707
|
-
|
|
708
|
-
// one message confirms the peer was reachable; a later timeout waiting for the next message in a
|
|
709
|
-
// multi-message exchange should not be treated as permanent peer absence.
|
|
710
|
-
if (
|
|
711
|
-
!options?.suppressPeerLoss &&
|
|
712
|
-
this.#messageReceivedCounter === 0 &&
|
|
713
|
-
causedBy(e, TransientPeerCommunicationError)
|
|
714
|
-
) {
|
|
714
|
+
if (this.#reportsPeerLoss(options?.suppressPeerLoss) && causedBy(e, TransientPeerCommunicationError)) {
|
|
715
715
|
await this.#context.peerLost(this, asError(e));
|
|
716
716
|
}
|
|
717
717
|
|
|
@@ -1119,6 +1119,9 @@ export namespace MessageExchange {
|
|
|
1119
1119
|
* instead of the session's default peer address.
|
|
1120
1120
|
*/
|
|
1121
1121
|
addressOverride?: ServerAddressUdp;
|
|
1122
|
+
|
|
1123
|
+
/** Waive peer-loss inference here. Required for exchanges to a peer that drives its own recovery. */
|
|
1124
|
+
suppressPeerLoss?: boolean;
|
|
1122
1125
|
}
|
|
1123
1126
|
|
|
1124
1127
|
export interface Config extends Options {
|
|
@@ -27,9 +27,13 @@ import {
|
|
|
27
27
|
Diagnostic,
|
|
28
28
|
Duration,
|
|
29
29
|
hex,
|
|
30
|
+
Instant,
|
|
30
31
|
InternalError,
|
|
31
32
|
Logger,
|
|
32
33
|
MatterFlowError,
|
|
34
|
+
Mutex,
|
|
35
|
+
Time,
|
|
36
|
+
Timer,
|
|
33
37
|
} from "@matter/general";
|
|
34
38
|
import { CaseAuthenticatedTag, FabricIndex, GlobalFabricId, NodeId } from "@matter/types";
|
|
35
39
|
import { SecureSession } from "./SecureSession.js";
|
|
@@ -55,6 +59,8 @@ export class NodeSession extends SecureSession {
|
|
|
55
59
|
readonly supportsMRP = true;
|
|
56
60
|
readonly type = SessionType.Unicast;
|
|
57
61
|
readonly #closedByPeer = AsyncObservableValue();
|
|
62
|
+
#rolloverCloseTimer?: Timer;
|
|
63
|
+
readonly #rolloverClose = new Mutex(this);
|
|
58
64
|
#isPeerLost = false;
|
|
59
65
|
|
|
60
66
|
// TODO - remove this; subscriptions should be owned by the peer, not the session
|
|
@@ -108,13 +114,7 @@ export class NodeSession extends SecureSession {
|
|
|
108
114
|
...config,
|
|
109
115
|
setActiveTimestamp: true, // We always set the active timestamp for Secure sessions
|
|
110
116
|
// Can be changed to a PersistedMessageCounter if we implement session storage
|
|
111
|
-
messageCounter: new MessageCounter(crypto, async () =>
|
|
112
|
-
// Secure Session Message Counter
|
|
113
|
-
// Expire/End the session before the counter rolls over
|
|
114
|
-
await this.initiateClose(async () => {
|
|
115
|
-
await this.closeSubscriptions(true);
|
|
116
|
-
});
|
|
117
|
-
}),
|
|
117
|
+
messageCounter: new MessageCounter(crypto, async () => this.#scheduleRolloverClose()),
|
|
118
118
|
messageReceptionState: new MessageReceptionStateEncryptedWithoutRollover(0),
|
|
119
119
|
});
|
|
120
120
|
|
|
@@ -269,10 +269,10 @@ export class NodeSession extends SecureSession {
|
|
|
269
269
|
return this.#fabric;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
override async closeSubscriptions(flush = false) {
|
|
272
|
+
override async closeSubscriptions(flush = false, currentExchange?: MessageExchange) {
|
|
273
273
|
const subscriptions = [...this.#subscriptions]; // get all values because subscriptions will remove themselves when cancelled
|
|
274
274
|
for (const subscription of subscriptions) {
|
|
275
|
-
await subscription.close(flush ? this : undefined);
|
|
275
|
+
await subscription.close(flush ? this : undefined, currentExchange);
|
|
276
276
|
}
|
|
277
277
|
return subscriptions.length;
|
|
278
278
|
}
|
|
@@ -337,7 +337,31 @@ export class NodeSession extends SecureSession {
|
|
|
337
337
|
});
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Wind the session down before its message counter rolls over. Notification arrives 100,000 messages ahead, on
|
|
342
|
+
* the stack of the send that consumed the counter, so closing there would make the flush wait on that send.
|
|
343
|
+
*/
|
|
344
|
+
#scheduleRolloverClose() {
|
|
345
|
+
if (this.#rolloverCloseTimer !== undefined || this.isClosing) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
logger.info(this.via, "Message counter nearing rollover, ending session");
|
|
350
|
+
|
|
351
|
+
this.#rolloverCloseTimer = Time.getTimer("session rollover close", Instant, () =>
|
|
352
|
+
this.#rolloverClose.run(async () => {
|
|
353
|
+
await this.initiateClose(async () => {
|
|
354
|
+
await this.closeSubscriptions(true);
|
|
355
|
+
});
|
|
356
|
+
}),
|
|
357
|
+
).start();
|
|
358
|
+
}
|
|
359
|
+
|
|
340
360
|
protected override async close() {
|
|
361
|
+
this.#rolloverCloseTimer?.stop();
|
|
362
|
+
this.#rolloverCloseTimer = undefined;
|
|
363
|
+
await this.#rolloverClose;
|
|
364
|
+
|
|
341
365
|
if (!this.#isPeerLost) {
|
|
342
366
|
try {
|
|
343
367
|
await this.gracefulClose.emit();
|
package/src/session/Session.ts
CHANGED
|
@@ -274,7 +274,7 @@ export abstract class Session {
|
|
|
274
274
|
async initiateForceClose(context: PeerLossContext) {
|
|
275
275
|
await this.initiateClose(async () => {
|
|
276
276
|
if (!context.keepSubscriptions) {
|
|
277
|
-
await this.closeSubscriptions();
|
|
277
|
+
await this.closeSubscriptions(false, context.currentExchange);
|
|
278
278
|
}
|
|
279
279
|
for (const exchange of this.#exchanges) {
|
|
280
280
|
if (exchange === context.currentExchange) {
|
|
@@ -317,7 +317,7 @@ export abstract class Session {
|
|
|
317
317
|
return !!this.#exchanges.size;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
async closeSubscriptions(
|
|
320
|
+
async closeSubscriptions(_flush = false, _currentExchange?: MessageExchange): Promise<number> {
|
|
321
321
|
return 0;
|
|
322
322
|
}
|
|
323
323
|
|
|
@@ -156,6 +156,12 @@ export interface SessionManagerContext {
|
|
|
156
156
|
|
|
157
157
|
const ID_SPACE_UPPER_BOUND = 0xffff;
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Sessions retained per peer. A new CASE does not close the peer's previous sessions, so a peer that reconnects
|
|
161
|
+
* repeatedly accumulates them. Per peer rather than global so one noisy peer cannot evict another's.
|
|
162
|
+
*/
|
|
163
|
+
const MAX_SESSIONS_PER_PEER = 5;
|
|
164
|
+
|
|
159
165
|
/** Storage key for the node-global Group Encrypted Data Message Counter in the session storage context. */
|
|
160
166
|
const GROUP_DATA_COUNTER_KEY = "groupDataCounter";
|
|
161
167
|
|
|
@@ -166,8 +172,15 @@ const GROUP_DATA_COUNTER_KEY = "groupDataCounter";
|
|
|
166
172
|
*/
|
|
167
173
|
const GROUP_DATA_COUNTER_RESERVE = 1000;
|
|
168
174
|
|
|
175
|
+
/** Thrown into a session closed because its peer exceeded {@link MAX_SESSIONS_PER_PEER}. */
|
|
176
|
+
export class SessionEvictedError extends ClosedError {
|
|
177
|
+
constructor(message = "Session evicted; peer holds too many sessions", options?: ErrorOptions) {
|
|
178
|
+
super(message, options);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
/**
|
|
170
|
-
* Thrown when communication terminates due node shutdown.
|
|
183
|
+
* Thrown when communication terminates due to node shutdown.
|
|
171
184
|
*/
|
|
172
185
|
export class ShutdownError extends ClosedError {
|
|
173
186
|
constructor(message = "Local node shutdown", options?: ErrorOptions) {
|
|
@@ -203,6 +216,7 @@ export class SessionManager {
|
|
|
203
216
|
readonly #construction: Construction<SessionManager>;
|
|
204
217
|
readonly #observers = new ObserverGroup();
|
|
205
218
|
readonly #subscriptionUpdateMutex = new Mutex(this);
|
|
219
|
+
readonly #sessionEvictionMutex = new Mutex(this);
|
|
206
220
|
#idUpperBound = ID_SPACE_UPPER_BOUND;
|
|
207
221
|
|
|
208
222
|
readonly #subscriptionsChanged = Observable<[session: NodeSession, subscription: Subscription]>();
|
|
@@ -224,6 +238,14 @@ export class SessionManager {
|
|
|
224
238
|
await this.deleteResumptionRecordsForFabric(fabric);
|
|
225
239
|
});
|
|
226
240
|
|
|
241
|
+
this.#sessions.added.on(session => {
|
|
242
|
+
// We are the responder, so nothing else reclaims these: an initiator's sessions are swept by the peer
|
|
243
|
+
// loss its own traffic reports
|
|
244
|
+
if (!session.isInitiator) {
|
|
245
|
+
this.#sessionEvictionMutex.run(() => this.#evictExcessSessionsFor(session));
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
227
249
|
// Add subscription monitors to new node sessions
|
|
228
250
|
this.#sessions.added.on(session => {
|
|
229
251
|
const subscriptionsChanged = (subscription: Subscription) => {
|
|
@@ -444,19 +466,25 @@ export class SessionManager {
|
|
|
444
466
|
return deletedCount > 0;
|
|
445
467
|
}
|
|
446
468
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
let oldestSession: NodeSession | undefined = undefined;
|
|
469
|
+
/** The session with the least recent traffic among those {@link filter} accepts, if any. */
|
|
470
|
+
#leastRecentlyUsedSession(filter: (session: NodeSession) => boolean) {
|
|
471
|
+
let oldest: NodeSession | undefined;
|
|
451
472
|
for (const session of this.#sessions) {
|
|
452
|
-
if (
|
|
453
|
-
|
|
473
|
+
if (filter(session) && (oldest === undefined || session.timestamp < oldest.timestamp)) {
|
|
474
|
+
oldest = session;
|
|
454
475
|
}
|
|
455
476
|
}
|
|
456
|
-
|
|
477
|
+
return oldest;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
findOldestInactiveSession() {
|
|
481
|
+
this.#construction.assert();
|
|
482
|
+
|
|
483
|
+
const oldest = this.#leastRecentlyUsedSession(() => true);
|
|
484
|
+
if (oldest === undefined) {
|
|
457
485
|
throw new MatterFlowError("No session found to close and all session ids are taken.");
|
|
458
486
|
}
|
|
459
|
-
return
|
|
487
|
+
return oldest;
|
|
460
488
|
}
|
|
461
489
|
|
|
462
490
|
async getNextAvailableSessionId() {
|
|
@@ -556,8 +584,45 @@ export class SessionManager {
|
|
|
556
584
|
/**
|
|
557
585
|
* Removes all Peer sessions and closes subscriptions.
|
|
558
586
|
*/
|
|
559
|
-
async handlePeerLoss(address: PeerAddress,
|
|
560
|
-
return await this.#handlePeerLoss({ address, asOf: asOf ?? Time.nowMs
|
|
587
|
+
async handlePeerLoss(address: PeerAddress, context: PeerLossContext) {
|
|
588
|
+
return await this.#handlePeerLoss({ ...context, address, asOf: context.asOf ?? Time.nowMs });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Close the peer's least recently active sessions once it exceeds {@link MAX_SESSIONS_PER_PEER}.
|
|
593
|
+
*
|
|
594
|
+
* Only sessions the peer established are eligible: ones we initiated are ours to manage, and the peer loss our
|
|
595
|
+
* own traffic reports already sweeps them. {@link added} is excluded so a burst of new sessions cannot evict the
|
|
596
|
+
* one that prompted the check.
|
|
597
|
+
*/
|
|
598
|
+
async #evictExcessSessionsFor(added: NodeSession) {
|
|
599
|
+
if (added.isPase || added.isClosing) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const address = added.peerAddress;
|
|
604
|
+
const claimsSlot = (session: NodeSession) =>
|
|
605
|
+
session !== added && !session.isInitiator && session.peerIs(address);
|
|
606
|
+
|
|
607
|
+
// A session already closing still occupies a slot until it completes, so it counts here but is not evicted
|
|
608
|
+
// again -- otherwise one that parks in deferredClose drops out of the count and the cap stops capping
|
|
609
|
+
let excess = this.#sessions.filter(claimsSlot).length + 1 - MAX_SESSIONS_PER_PEER;
|
|
610
|
+
|
|
611
|
+
while (excess-- > 0) {
|
|
612
|
+
const session = this.#leastRecentlyUsedSession(s => claimsSlot(s) && !s.isClosing);
|
|
613
|
+
if (session === undefined) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
logger.info(
|
|
618
|
+
session.via,
|
|
619
|
+
`Closing least recently used session; ${PeerAddress(address)} exceeds ${MAX_SESSIONS_PER_PEER} sessions`,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Force close rather than the graceful path: that sends CloseSession over MRP, and the eviction target is
|
|
623
|
+
// typically the peer we can no longer reach, so it would hold this mutex for a full retransmission window
|
|
624
|
+
await session.initiateForceClose({ cause: new SessionEvictedError() });
|
|
625
|
+
}
|
|
561
626
|
}
|
|
562
627
|
|
|
563
628
|
async #handlePeerLoss(
|
|
@@ -917,6 +982,7 @@ export class SessionManager {
|
|
|
917
982
|
}
|
|
918
983
|
|
|
919
984
|
await this.#subscriptionUpdateMutex;
|
|
985
|
+
await this.#sessionEvictionMutex;
|
|
920
986
|
|
|
921
987
|
const context: PeerLossContext = { cause: new ShutdownError("Session closed by node shutdown") };
|
|
922
988
|
|