@riddix/hamh 2.1.0-alpha.834 → 2.1.0-alpha.836
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/backend/cli.js
CHANGED
|
@@ -132058,6 +132058,12 @@ var init_bridge_config_schema = __esm({
|
|
|
132058
132058
|
description: "Anchor each device to its Home Assistant entity registry id instead of the entity id, so renaming an entity no longer re-adds it in your controller (Alexa, Google Home, Apple Home) and keeps groups and automations. Records are seeded from the start, so enabling this later is safe and never re-adds existing devices. Default off.",
|
|
132059
132059
|
type: "boolean",
|
|
132060
132060
|
default: false
|
|
132061
|
+
},
|
|
132062
|
+
wedgeWatchdog: {
|
|
132063
|
+
title: "Wedge Watchdog (Apple 'Updating' workaround)",
|
|
132064
|
+
description: "Rotate the one session that looks wedged, subscriptions still alive but no inbound request from the controller for about 45 minutes, earlier than the blind session rotation. Targets Apple Home tiles stuck on 'Updating' where the controller keeps acking but stops consuming data. A false positive only triggers a transparent reconnect, the same as normal rotation. Default off.",
|
|
132065
|
+
type: "boolean",
|
|
132066
|
+
default: false
|
|
132061
132067
|
}
|
|
132062
132068
|
}
|
|
132063
132069
|
};
|
|
@@ -156805,7 +156811,23 @@ function seedExistingSessionStarts(startedAt, sessions, now = Date.now()) {
|
|
|
156805
156811
|
}
|
|
156806
156812
|
}
|
|
156807
156813
|
|
|
156814
|
+
// src/services/bridges/wedge-watchdog.ts
|
|
156815
|
+
var WEDGE_WARMUP_MS = 15 * 60 * 1e3;
|
|
156816
|
+
var WEDGE_IM_SILENCE_MS = 45 * 60 * 1e3;
|
|
156817
|
+
var WEDGE_MIN_ROTATE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
156818
|
+
function decideWedgeRotation(input) {
|
|
156819
|
+
const {
|
|
156820
|
+
subscriptionCount,
|
|
156821
|
+
sessionAgeMs,
|
|
156822
|
+
lastImRequestMsAgo,
|
|
156823
|
+
lastRotatedMsAgo
|
|
156824
|
+
} = input;
|
|
156825
|
+
const imSilenceMs = lastImRequestMsAgo == null ? sessionAgeMs : lastImRequestMsAgo;
|
|
156826
|
+
return subscriptionCount > 0 && sessionAgeMs > WEDGE_WARMUP_MS && imSilenceMs > WEDGE_IM_SILENCE_MS && (lastRotatedMsAgo == null || lastRotatedMsAgo > WEDGE_MIN_ROTATE_INTERVAL_MS);
|
|
156827
|
+
}
|
|
156828
|
+
|
|
156808
156829
|
// src/services/bridges/bridge.ts
|
|
156830
|
+
var imWrapMarker = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
|
|
156809
156831
|
var AUTO_FORCE_SYNC_INTERVAL_MS = 9e4;
|
|
156810
156832
|
var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS = 2500;
|
|
156811
156833
|
var MDNS_ADDRESS_CHECK_INTERVAL_MS = 6e4;
|
|
@@ -156853,6 +156875,12 @@ var Bridge = class {
|
|
|
156853
156875
|
sessionStartedAt = /* @__PURE__ */ new Map();
|
|
156854
156876
|
rotationTimer = null;
|
|
156855
156877
|
maxSessionAgeMs = 0;
|
|
156878
|
+
// Wedge watchdog: last inbound Interaction Model request time per session and
|
|
156879
|
+
// last time the watchdog rotated it, both keyed by the long-lived session
|
|
156880
|
+
// object so they clear when the session goes away.
|
|
156881
|
+
lastImRequestAt = /* @__PURE__ */ new WeakMap();
|
|
156882
|
+
wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
|
|
156883
|
+
wedgeWatchdogTimer = null;
|
|
156856
156884
|
// Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
|
|
156857
156885
|
// change forces a fresh operational announcement (#415).
|
|
156858
156886
|
mdnsAddressTimer = null;
|
|
@@ -156912,6 +156940,8 @@ var Bridge = class {
|
|
|
156912
156940
|
const nowMs = Date.now();
|
|
156913
156941
|
const lastActiveMsAgo = typeof s.activeTimestamp === "number" && s.activeTimestamp > 0 ? nowMs - s.activeTimestamp : null;
|
|
156914
156942
|
const lastAnyActivityMsAgo = typeof s.timestamp === "number" ? nowMs - s.timestamp : null;
|
|
156943
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
156944
|
+
const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
|
|
156915
156945
|
const startedAt = this.sessionStartedAt.get(s.id);
|
|
156916
156946
|
return {
|
|
156917
156947
|
id: s.id,
|
|
@@ -156920,6 +156950,7 @@ var Bridge = class {
|
|
|
156920
156950
|
subscriptionCount: subCount,
|
|
156921
156951
|
lastActiveMsAgo,
|
|
156922
156952
|
lastAnyActivityMsAgo,
|
|
156953
|
+
lastImRequestMsAgo,
|
|
156923
156954
|
isPeerActive: Boolean(s.isPeerActive),
|
|
156924
156955
|
ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
|
|
156925
156956
|
};
|
|
@@ -157086,6 +157117,7 @@ var Bridge = class {
|
|
|
157086
157117
|
this.wireSessionDiagnostics();
|
|
157087
157118
|
this.wireFabricWarnings();
|
|
157088
157119
|
this.startSessionRotation();
|
|
157120
|
+
this.startWedgeWatchdog();
|
|
157089
157121
|
this.startMdnsAddressWatch();
|
|
157090
157122
|
logMemoryUsage(this.log, "bridge running");
|
|
157091
157123
|
diagnosticEventBus.emit("bridge_started", `Bridge started`, {
|
|
@@ -157285,6 +157317,29 @@ ${e?.toString()}`);
|
|
|
157285
157317
|
};
|
|
157286
157318
|
sessionManager.sessions.added.on(this.sessionAddedHandler);
|
|
157287
157319
|
sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
|
|
157320
|
+
this.wireImRequestTracking();
|
|
157321
|
+
} catch {
|
|
157322
|
+
}
|
|
157323
|
+
}
|
|
157324
|
+
// Stamp the time of every inbound Interaction Model request per session by
|
|
157325
|
+
// wrapping InteractionServer.onNewExchange. The wedge watchdog reads these to
|
|
157326
|
+
// tell a live-but-consuming controller from one that only keeps acking.
|
|
157327
|
+
wireImRequestTracking() {
|
|
157328
|
+
try {
|
|
157329
|
+
const is = this.server.env.get(InteractionServer);
|
|
157330
|
+
const marked = is;
|
|
157331
|
+
if (marked[imWrapMarker]) {
|
|
157332
|
+
return;
|
|
157333
|
+
}
|
|
157334
|
+
const original = is.onNewExchange.bind(is);
|
|
157335
|
+
is.onNewExchange = (exchange, message) => {
|
|
157336
|
+
const session = exchange.session;
|
|
157337
|
+
if (session) {
|
|
157338
|
+
this.lastImRequestAt.set(session, Date.now());
|
|
157339
|
+
}
|
|
157340
|
+
return original(exchange, message);
|
|
157341
|
+
};
|
|
157342
|
+
marked[imWrapMarker] = true;
|
|
157288
157343
|
} catch {
|
|
157289
157344
|
}
|
|
157290
157345
|
}
|
|
@@ -157447,6 +157502,7 @@ ${e?.toString()}`);
|
|
|
157447
157502
|
}
|
|
157448
157503
|
this.staleSessionTimers.clear();
|
|
157449
157504
|
this.stopSessionRotation();
|
|
157505
|
+
this.stopWedgeWatchdog();
|
|
157450
157506
|
this.stopMdnsAddressWatch();
|
|
157451
157507
|
this.sessionStartedAt.clear();
|
|
157452
157508
|
}
|
|
@@ -157512,6 +157568,72 @@ ${e?.toString()}`);
|
|
|
157512
157568
|
this.rotationTimer = null;
|
|
157513
157569
|
}
|
|
157514
157570
|
}
|
|
157571
|
+
// Opt-in wedge watchdog: reuse the rotation check cadence (5min) to look for
|
|
157572
|
+
// the one session wedged on "Updating" and rotate just that one.
|
|
157573
|
+
startWedgeWatchdog() {
|
|
157574
|
+
this.stopWedgeWatchdog();
|
|
157575
|
+
if (!this.dataProvider.featureFlags?.wedgeWatchdog) {
|
|
157576
|
+
return;
|
|
157577
|
+
}
|
|
157578
|
+
this.wedgeWatchdogTimer = setInterval(
|
|
157579
|
+
() => this.runWedgeWatchdogCheck(),
|
|
157580
|
+
ROTATION_CHECK_INTERVAL_MS
|
|
157581
|
+
);
|
|
157582
|
+
this.log.info(
|
|
157583
|
+
`Wedge watchdog: checking every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
|
|
157584
|
+
);
|
|
157585
|
+
}
|
|
157586
|
+
stopWedgeWatchdog() {
|
|
157587
|
+
if (this.wedgeWatchdogTimer) {
|
|
157588
|
+
clearInterval(this.wedgeWatchdogTimer);
|
|
157589
|
+
this.wedgeWatchdogTimer = null;
|
|
157590
|
+
}
|
|
157591
|
+
}
|
|
157592
|
+
// Rotate exactly the sessions the pure rule flags as wedged. Closing is the
|
|
157593
|
+
// same graceful-then-force path age rotation uses, so a false positive just
|
|
157594
|
+
// re-CASEs the controller.
|
|
157595
|
+
runWedgeWatchdogCheck() {
|
|
157596
|
+
try {
|
|
157597
|
+
const sessionManager = this.server.env.get(SessionManager);
|
|
157598
|
+
const now = Date.now();
|
|
157599
|
+
const closes = [];
|
|
157600
|
+
for (const s of [...sessionManager.sessions]) {
|
|
157601
|
+
if (s.isClosing) continue;
|
|
157602
|
+
const startedAt = this.sessionStartedAt.get(s.id);
|
|
157603
|
+
const sessionAgeMs = startedAt != null ? now - startedAt : 0;
|
|
157604
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
157605
|
+
const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
|
|
157606
|
+
const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
|
|
157607
|
+
const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
|
|
157608
|
+
if (!decideWedgeRotation({
|
|
157609
|
+
subscriptionCount: s.subscriptions.size,
|
|
157610
|
+
sessionAgeMs,
|
|
157611
|
+
lastImRequestMsAgo,
|
|
157612
|
+
lastRotatedMsAgo
|
|
157613
|
+
})) {
|
|
157614
|
+
continue;
|
|
157615
|
+
}
|
|
157616
|
+
const silenceMin = Math.round(
|
|
157617
|
+
(lastImRequestMsAgo ?? sessionAgeMs) / 6e4
|
|
157618
|
+
);
|
|
157619
|
+
this.log.info(
|
|
157620
|
+
`Wedge watchdog: rotating session ${s.id}, no inbound interaction for ${silenceMin}min`
|
|
157621
|
+
);
|
|
157622
|
+
this.wedgeLastRotatedAt.set(s, now);
|
|
157623
|
+
closes.push(
|
|
157624
|
+
s.initiateClose().catch(() => {
|
|
157625
|
+
return s.initiateForceClose({
|
|
157626
|
+
cause: new Error("wedge watchdog, forcing")
|
|
157627
|
+
});
|
|
157628
|
+
})
|
|
157629
|
+
);
|
|
157630
|
+
}
|
|
157631
|
+
if (closes.length > 0) {
|
|
157632
|
+
Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
|
|
157633
|
+
}
|
|
157634
|
+
} catch {
|
|
157635
|
+
}
|
|
157636
|
+
}
|
|
157515
157637
|
// Poll the interface addresses mDNS advertises and re-announce when they
|
|
157516
157638
|
// change. matter.js caches the operational records at first announcement, so
|
|
157517
157639
|
// a dynamic ISP IPv6 prefix change keeps advertising the dead global address
|
|
@@ -157643,6 +157765,7 @@ ${e?.toString()}`);
|
|
|
157643
157765
|
if (this.status.code === BridgeStatus.Running) {
|
|
157644
157766
|
this.startAutoForceSyncIfEnabled();
|
|
157645
157767
|
this.startSessionRotation();
|
|
157768
|
+
this.startWedgeWatchdog();
|
|
157646
157769
|
}
|
|
157647
157770
|
} catch (e) {
|
|
157648
157771
|
const reason = "Failed to update bridge due to error:";
|
|
@@ -169579,20 +169702,22 @@ function resolveCleanAreaIds(selectedAreas, cleanAreaRooms) {
|
|
|
169579
169702
|
}
|
|
169580
169703
|
return haAreaIds;
|
|
169581
169704
|
}
|
|
169705
|
+
var cleaningStates = [
|
|
169706
|
+
VacuumState.cleaning,
|
|
169707
|
+
VacuumState.segment_cleaning,
|
|
169708
|
+
VacuumState.zone_cleaning,
|
|
169709
|
+
VacuumState.spot_cleaning,
|
|
169710
|
+
VacuumState.mop_cleaning,
|
|
169711
|
+
VacuumState.paused
|
|
169712
|
+
];
|
|
169713
|
+
function vacuumIsCleaning(state) {
|
|
169714
|
+
return state != null && cleaningStates.includes(state);
|
|
169715
|
+
}
|
|
169582
169716
|
var vacuumRvcRunModeConfig = {
|
|
169583
169717
|
getCurrentMode: (entity) => {
|
|
169584
|
-
const
|
|
169585
|
-
const cleaningStates = [
|
|
169586
|
-
VacuumState.cleaning,
|
|
169587
|
-
VacuumState.segment_cleaning,
|
|
169588
|
-
VacuumState.zone_cleaning,
|
|
169589
|
-
VacuumState.spot_cleaning,
|
|
169590
|
-
VacuumState.mop_cleaning,
|
|
169591
|
-
VacuumState.paused
|
|
169592
|
-
];
|
|
169593
|
-
const isCleaning = cleaningStates.includes(state);
|
|
169718
|
+
const isCleaning = vacuumIsCleaning(entity.state);
|
|
169594
169719
|
logger235.debug(
|
|
169595
|
-
`Vacuum state: "${state}", isCleaning: ${isCleaning}, currentMode: ${isCleaning ? "Cleaning" : "Idle"}`
|
|
169720
|
+
`Vacuum state: "${entity.state}", isCleaning: ${isCleaning}, currentMode: ${isCleaning ? "Cleaning" : "Idle"}`
|
|
169596
169721
|
);
|
|
169597
169722
|
return isCleaning ? 1 /* Cleaning */ : 0 /* Idle */;
|
|
169598
169723
|
},
|
|
@@ -169926,11 +170051,12 @@ function createCleanAreaRvcRunModeServer(cleanAreaRooms) {
|
|
|
169926
170051
|
currentMode: 0 /* Idle */
|
|
169927
170052
|
});
|
|
169928
170053
|
}
|
|
169929
|
-
var VacuumRvcRunModeServer = RvcRunModeServer2(vacuumRvcRunModeConfig);
|
|
169930
170054
|
|
|
169931
170055
|
// src/matter/endpoints/legacy/vacuum/behaviors/vacuum-on-off-server.ts
|
|
169932
170056
|
var VacuumOnOffServer = OnOffServer2({
|
|
169933
|
-
|
|
170057
|
+
// Derive from the entity directly. Reading the sibling rvcRunMode behavior
|
|
170058
|
+
// crashed with a class mismatch and would lag one tick behind anyway (#428).
|
|
170059
|
+
isOn: (entity) => vacuumIsCleaning(entity.state),
|
|
169934
170060
|
turnOn: () => ({ action: "vacuum.start" }),
|
|
169935
170061
|
turnOff: () => ({ action: "vacuum.return_to_base" })
|
|
169936
170062
|
}).with();
|
|
@@ -170743,7 +170869,7 @@ function isDockedCharging(entity, batteryPercent) {
|
|
|
170743
170869
|
}
|
|
170744
170870
|
function mapVacuumOperationalState(entity, batteryPercent = batteryFromAttributes(entity.attributes), chargingState = null) {
|
|
170745
170871
|
const state = entity.state;
|
|
170746
|
-
const
|
|
170872
|
+
const cleaningStates2 = [
|
|
170747
170873
|
VacuumState.cleaning,
|
|
170748
170874
|
VacuumState.segment_cleaning,
|
|
170749
170875
|
VacuumState.zone_cleaning,
|
|
@@ -170756,7 +170882,7 @@ function mapVacuumOperationalState(entity, batteryPercent = batteryFromAttribute
|
|
|
170756
170882
|
operationalState = charging ? RvcOperationalState4.OperationalState.Charging : RvcOperationalState4.OperationalState.Docked;
|
|
170757
170883
|
} else if (state === VacuumState.returning) {
|
|
170758
170884
|
operationalState = RvcOperationalState4.OperationalState.SeekingCharger;
|
|
170759
|
-
} else if (
|
|
170885
|
+
} else if (cleaningStates2.includes(state)) {
|
|
170760
170886
|
operationalState = RvcOperationalState4.OperationalState.Running;
|
|
170761
170887
|
} else if (state === VacuumState.paused) {
|
|
170762
170888
|
operationalState = RvcOperationalState4.OperationalState.Paused;
|
|
@@ -174375,6 +174501,7 @@ init_dist();
|
|
|
174375
174501
|
init_esm7();
|
|
174376
174502
|
import * as os10 from "node:os";
|
|
174377
174503
|
init_diagnostic_event_bus();
|
|
174504
|
+
var imWrapMarker2 = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
|
|
174378
174505
|
var AUTO_FORCE_SYNC_INTERVAL_MS2 = 9e4;
|
|
174379
174506
|
var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS2 = 2500;
|
|
174380
174507
|
var MDNS_ADDRESS_CHECK_INTERVAL_MS2 = 6e4;
|
|
@@ -174407,6 +174534,12 @@ var ServerModeBridge = class {
|
|
|
174407
174534
|
sessionStartedAt = /* @__PURE__ */ new Map();
|
|
174408
174535
|
rotationTimer = null;
|
|
174409
174536
|
maxSessionAgeMs = 0;
|
|
174537
|
+
// Wedge watchdog: last inbound Interaction Model request time per session and
|
|
174538
|
+
// last time the watchdog rotated it, both keyed by the long-lived session
|
|
174539
|
+
// object so they clear when the session goes away.
|
|
174540
|
+
lastImRequestAt = /* @__PURE__ */ new WeakMap();
|
|
174541
|
+
wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
|
|
174542
|
+
wedgeWatchdogTimer = null;
|
|
174410
174543
|
// Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
|
|
174411
174544
|
// change forces a fresh operational announcement (#415).
|
|
174412
174545
|
mdnsAddressTimer = null;
|
|
@@ -174483,6 +174616,8 @@ var ServerModeBridge = class {
|
|
|
174483
174616
|
const nowMs = Date.now();
|
|
174484
174617
|
const lastActiveMsAgo = typeof s.activeTimestamp === "number" && s.activeTimestamp > 0 ? nowMs - s.activeTimestamp : null;
|
|
174485
174618
|
const lastAnyActivityMsAgo = typeof s.timestamp === "number" ? nowMs - s.timestamp : null;
|
|
174619
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
174620
|
+
const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
|
|
174486
174621
|
const startedAt = this.sessionStartedAt.get(s.id);
|
|
174487
174622
|
return {
|
|
174488
174623
|
id: s.id,
|
|
@@ -174491,6 +174626,7 @@ var ServerModeBridge = class {
|
|
|
174491
174626
|
subscriptionCount: subCount,
|
|
174492
174627
|
lastActiveMsAgo,
|
|
174493
174628
|
lastAnyActivityMsAgo,
|
|
174629
|
+
lastImRequestMsAgo,
|
|
174494
174630
|
isPeerActive: Boolean(s.isPeerActive),
|
|
174495
174631
|
ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
|
|
174496
174632
|
};
|
|
@@ -174554,6 +174690,7 @@ var ServerModeBridge = class {
|
|
|
174554
174690
|
this.wireSessionDiagnostics();
|
|
174555
174691
|
this.wireFabricWarnings();
|
|
174556
174692
|
this.startSessionRotation();
|
|
174693
|
+
this.startWedgeWatchdog();
|
|
174557
174694
|
this.startMdnsAddressWatch();
|
|
174558
174695
|
this.scheduleWarmStart();
|
|
174559
174696
|
logMemoryUsage(this.log, "server mode bridge running");
|
|
@@ -174571,6 +174708,7 @@ ${e?.toString()}`);
|
|
|
174571
174708
|
}
|
|
174572
174709
|
async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
|
|
174573
174710
|
this.stopSessionRotation();
|
|
174711
|
+
this.stopWedgeWatchdog();
|
|
174574
174712
|
this.stopMdnsAddressWatch();
|
|
174575
174713
|
this.unwireSessionDiagnostics();
|
|
174576
174714
|
this.unwireFabricWarnings();
|
|
@@ -174603,6 +174741,7 @@ ${e?.toString()}`);
|
|
|
174603
174741
|
if (this.status.code === BridgeStatus.Running) {
|
|
174604
174742
|
this.startAutoForceSyncIfEnabled();
|
|
174605
174743
|
this.startSessionRotation();
|
|
174744
|
+
this.startWedgeWatchdog();
|
|
174606
174745
|
}
|
|
174607
174746
|
} catch (e) {
|
|
174608
174747
|
const reason = "Failed to update server mode bridge due to error:";
|
|
@@ -174787,6 +174926,29 @@ ${e?.toString()}`);
|
|
|
174787
174926
|
sessionManager.sessions.added.on(this.sessionAddedHandler);
|
|
174788
174927
|
sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
|
|
174789
174928
|
seedExistingSessionStarts(this.sessionStartedAt, sessionManager.sessions);
|
|
174929
|
+
this.wireImRequestTracking();
|
|
174930
|
+
} catch {
|
|
174931
|
+
}
|
|
174932
|
+
}
|
|
174933
|
+
// Stamp the time of every inbound Interaction Model request per session by
|
|
174934
|
+
// wrapping InteractionServer.onNewExchange. The wedge watchdog reads these to
|
|
174935
|
+
// tell a live-but-consuming controller from one that only keeps acking.
|
|
174936
|
+
wireImRequestTracking() {
|
|
174937
|
+
try {
|
|
174938
|
+
const is = this.server.env.get(InteractionServer);
|
|
174939
|
+
const marked = is;
|
|
174940
|
+
if (marked[imWrapMarker2]) {
|
|
174941
|
+
return;
|
|
174942
|
+
}
|
|
174943
|
+
const original = is.onNewExchange.bind(is);
|
|
174944
|
+
is.onNewExchange = (exchange, message) => {
|
|
174945
|
+
const session = exchange.session;
|
|
174946
|
+
if (session) {
|
|
174947
|
+
this.lastImRequestAt.set(session, Date.now());
|
|
174948
|
+
}
|
|
174949
|
+
return original(exchange, message);
|
|
174950
|
+
};
|
|
174951
|
+
marked[imWrapMarker2] = true;
|
|
174790
174952
|
} catch {
|
|
174791
174953
|
}
|
|
174792
174954
|
}
|
|
@@ -175004,6 +175166,72 @@ ${e?.toString()}`);
|
|
|
175004
175166
|
this.rotationTimer = null;
|
|
175005
175167
|
}
|
|
175006
175168
|
}
|
|
175169
|
+
// Opt-in wedge watchdog: reuse the rotation check cadence (5min) to look for
|
|
175170
|
+
// the one session wedged on "Updating" and rotate just that one.
|
|
175171
|
+
startWedgeWatchdog() {
|
|
175172
|
+
this.stopWedgeWatchdog();
|
|
175173
|
+
if (!this.dataProvider.featureFlags?.wedgeWatchdog) {
|
|
175174
|
+
return;
|
|
175175
|
+
}
|
|
175176
|
+
this.wedgeWatchdogTimer = setInterval(
|
|
175177
|
+
() => this.runWedgeWatchdogCheck(),
|
|
175178
|
+
ROTATION_CHECK_INTERVAL_MS
|
|
175179
|
+
);
|
|
175180
|
+
this.log.info(
|
|
175181
|
+
`Wedge watchdog: checking every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
|
|
175182
|
+
);
|
|
175183
|
+
}
|
|
175184
|
+
stopWedgeWatchdog() {
|
|
175185
|
+
if (this.wedgeWatchdogTimer) {
|
|
175186
|
+
clearInterval(this.wedgeWatchdogTimer);
|
|
175187
|
+
this.wedgeWatchdogTimer = null;
|
|
175188
|
+
}
|
|
175189
|
+
}
|
|
175190
|
+
// Rotate exactly the sessions the pure rule flags as wedged. Closing is the
|
|
175191
|
+
// same graceful-then-force path age rotation uses, so a false positive just
|
|
175192
|
+
// re-CASEs the controller.
|
|
175193
|
+
runWedgeWatchdogCheck() {
|
|
175194
|
+
try {
|
|
175195
|
+
const sessionManager = this.server.env.get(SessionManager);
|
|
175196
|
+
const now = Date.now();
|
|
175197
|
+
const closes = [];
|
|
175198
|
+
for (const s of [...sessionManager.sessions]) {
|
|
175199
|
+
if (s.isClosing) continue;
|
|
175200
|
+
const startedAt = this.sessionStartedAt.get(s.id);
|
|
175201
|
+
const sessionAgeMs = startedAt != null ? now - startedAt : 0;
|
|
175202
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
175203
|
+
const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
|
|
175204
|
+
const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
|
|
175205
|
+
const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
|
|
175206
|
+
if (!decideWedgeRotation({
|
|
175207
|
+
subscriptionCount: s.subscriptions.size,
|
|
175208
|
+
sessionAgeMs,
|
|
175209
|
+
lastImRequestMsAgo,
|
|
175210
|
+
lastRotatedMsAgo
|
|
175211
|
+
})) {
|
|
175212
|
+
continue;
|
|
175213
|
+
}
|
|
175214
|
+
const silenceMin = Math.round(
|
|
175215
|
+
(lastImRequestMsAgo ?? sessionAgeMs) / 6e4
|
|
175216
|
+
);
|
|
175217
|
+
this.log.info(
|
|
175218
|
+
`Wedge watchdog: rotating session ${s.id}, no inbound interaction for ${silenceMin}min`
|
|
175219
|
+
);
|
|
175220
|
+
this.wedgeLastRotatedAt.set(s, now);
|
|
175221
|
+
closes.push(
|
|
175222
|
+
s.initiateClose().catch(() => {
|
|
175223
|
+
return s.initiateForceClose({
|
|
175224
|
+
cause: new Error("wedge watchdog, forcing")
|
|
175225
|
+
});
|
|
175226
|
+
})
|
|
175227
|
+
);
|
|
175228
|
+
}
|
|
175229
|
+
if (closes.length > 0) {
|
|
175230
|
+
Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
|
|
175231
|
+
}
|
|
175232
|
+
} catch {
|
|
175233
|
+
}
|
|
175234
|
+
}
|
|
175007
175235
|
// Poll the interface addresses mDNS advertises and re-announce when they
|
|
175008
175236
|
// change. matter.js caches the operational records at first announcement, so
|
|
175009
175237
|
// a dynamic ISP IPv6 prefix change keeps advertising the dead global address
|