@apocaliss92/nodedreame 1.11.9 → 1.11.11
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/index.cjs +238 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +237 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -73,6 +73,7 @@ __export(index_exports, {
|
|
|
73
73
|
extractMowerConsumableValues: () => extractMowerConsumableValues,
|
|
74
74
|
getMowerCapabilities: () => getMowerCapabilities,
|
|
75
75
|
getVacuumCapabilities: () => getVacuumCapabilities,
|
|
76
|
+
isAuthRefusedError: () => isAuthRefusedError,
|
|
76
77
|
isDreameConsumableKey: () => isDreameConsumableKey,
|
|
77
78
|
mowerConsumableIndex: () => mowerConsumableIndex,
|
|
78
79
|
mowerFaultSeverity: () => mowerFaultSeverity,
|
|
@@ -88,7 +89,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
88
89
|
|
|
89
90
|
// src/support/version.ts
|
|
90
91
|
var LIBRARY_NAME = "nodedreame";
|
|
91
|
-
var LIBRARY_VERSION = "1.11.
|
|
92
|
+
var LIBRARY_VERSION = "1.11.10";
|
|
92
93
|
|
|
93
94
|
// src/transport/errors.ts
|
|
94
95
|
var DreameError = class extends Error {
|
|
@@ -717,6 +718,10 @@ var TypedEmitter = class {
|
|
|
717
718
|
emit(event, ...args) {
|
|
718
719
|
return this.#emitter.emit(event, ...args);
|
|
719
720
|
}
|
|
721
|
+
/** Number of listeners registered for `event`. */
|
|
722
|
+
listenerCount(event) {
|
|
723
|
+
return this.#emitter.listenerCount(event);
|
|
724
|
+
}
|
|
720
725
|
removeAllListeners() {
|
|
721
726
|
this.#emitter.removeAllListeners();
|
|
722
727
|
return this;
|
|
@@ -729,6 +734,24 @@ function defaultConnect(url, opts) {
|
|
|
729
734
|
const typed = client;
|
|
730
735
|
return typed;
|
|
731
736
|
}
|
|
737
|
+
function isAuthRefusedError(err) {
|
|
738
|
+
let cur = err;
|
|
739
|
+
for (let depth = 0; cur != null && depth < 6; depth += 1) {
|
|
740
|
+
if (typeof cur !== "object") {
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
const code = cur.code;
|
|
744
|
+
if (code === 4 || code === 5 || code === 134 || code === 135) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
const message = cur.message;
|
|
748
|
+
if (typeof message === "string" && /not authorized|bad user ?name|bad username or password|bad password/i.test(message)) {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
cur = cur.cause;
|
|
752
|
+
}
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
732
755
|
var DreamePush = class extends TypedEmitter {
|
|
733
756
|
#device;
|
|
734
757
|
#session;
|
|
@@ -737,11 +760,15 @@ var DreamePush = class extends TypedEmitter {
|
|
|
737
760
|
#backoffMs;
|
|
738
761
|
#keepaliveSeconds;
|
|
739
762
|
#rejectUnauthorized;
|
|
763
|
+
#onAuthFailure;
|
|
764
|
+
#maxAuthRetries;
|
|
740
765
|
#topic;
|
|
741
766
|
#client = null;
|
|
742
767
|
#closed = false;
|
|
743
768
|
#tearingDown = false;
|
|
744
769
|
#reconnectTimer = null;
|
|
770
|
+
/** Consecutive auth-refused re-auths since the last successful connect. */
|
|
771
|
+
#authFailureCount = 0;
|
|
745
772
|
/**
|
|
746
773
|
* Per-instance bound close handler. Stored so we can pass the EXACT same
|
|
747
774
|
* function reference to both `client.on('close', …)` and
|
|
@@ -758,6 +785,8 @@ var DreamePush = class extends TypedEmitter {
|
|
|
758
785
|
this.#backoffMs = input.reconnectBackoffMs ?? 5e3;
|
|
759
786
|
this.#keepaliveSeconds = input.keepaliveSeconds ?? 60;
|
|
760
787
|
this.#rejectUnauthorized = input.rejectUnauthorized ?? false;
|
|
788
|
+
this.#onAuthFailure = input.onAuthFailure ?? null;
|
|
789
|
+
this.#maxAuthRetries = input.maxAuthRetries ?? 3;
|
|
761
790
|
this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
|
|
762
791
|
this.#onClose = () => {
|
|
763
792
|
if (this.#tearingDown) {
|
|
@@ -794,7 +823,11 @@ var DreamePush = class extends TypedEmitter {
|
|
|
794
823
|
this.#reconnectTimer = null;
|
|
795
824
|
}
|
|
796
825
|
await this.#teardownClient();
|
|
797
|
-
|
|
826
|
+
try {
|
|
827
|
+
await this.#connectAndSubscribe();
|
|
828
|
+
} catch {
|
|
829
|
+
this.#scheduleReconnect();
|
|
830
|
+
}
|
|
798
831
|
}
|
|
799
832
|
/** Tear down permanently. Closed subscriptions cannot be reopened. */
|
|
800
833
|
async close() {
|
|
@@ -831,6 +864,7 @@ var DreamePush = class extends TypedEmitter {
|
|
|
831
864
|
reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
|
|
832
865
|
return;
|
|
833
866
|
}
|
|
867
|
+
this.#authFailureCount = 0;
|
|
834
868
|
this.emit("connect");
|
|
835
869
|
resolve();
|
|
836
870
|
});
|
|
@@ -864,12 +898,55 @@ var DreamePush = class extends TypedEmitter {
|
|
|
864
898
|
if (this.#closed || this.#client) {
|
|
865
899
|
return;
|
|
866
900
|
}
|
|
867
|
-
void this.#
|
|
868
|
-
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
869
|
-
this.#scheduleReconnect();
|
|
870
|
-
});
|
|
901
|
+
void this.#attemptReconnect();
|
|
871
902
|
}, this.#backoffMs);
|
|
872
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* One reconnect attempt with reactive auth self-heal. If the broker refuses
|
|
906
|
+
* the CONNECT with an auth error (a stale access token) and an
|
|
907
|
+
* `onAuthFailure` provider is wired and the re-auth budget is not exhausted,
|
|
908
|
+
* mint a fresh session and retry with the new token instead of replaying the
|
|
909
|
+
* old one. Any other failure (or an exhausted budget) falls back to the plain
|
|
910
|
+
* backoff reconnect so bad credentials cannot hot-loop re-login.
|
|
911
|
+
*/
|
|
912
|
+
async #attemptReconnect() {
|
|
913
|
+
try {
|
|
914
|
+
await this.#connectAndSubscribe();
|
|
915
|
+
} catch (err) {
|
|
916
|
+
if (this.#closed) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (this.#onAuthFailure && isAuthRefusedError(err) && this.#authFailureCount < this.#maxAuthRetries) {
|
|
920
|
+
await this.#reauthenticateAndReconnect();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
924
|
+
this.#scheduleReconnect();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/** Mint a fresh session via `onAuthFailure`, adopt its token, and reconnect. */
|
|
928
|
+
async #reauthenticateAndReconnect() {
|
|
929
|
+
this.#authFailureCount += 1;
|
|
930
|
+
let session;
|
|
931
|
+
try {
|
|
932
|
+
session = await this.#onAuthFailure();
|
|
933
|
+
} catch (err) {
|
|
934
|
+
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
935
|
+
if (!this.#client) {
|
|
936
|
+
this.#scheduleReconnect();
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (this.#closed) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
this.#session = session;
|
|
944
|
+
this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
|
|
945
|
+
if (this.#client) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
void this.#attemptReconnect();
|
|
949
|
+
}
|
|
873
950
|
async #teardownClient() {
|
|
874
951
|
const client = this.#client;
|
|
875
952
|
this.#client = null;
|
|
@@ -1045,7 +1122,12 @@ function resolveCapabilities(model) {
|
|
|
1045
1122
|
// src/device/base-device.ts
|
|
1046
1123
|
function defaultBaseDeviceDeps() {
|
|
1047
1124
|
return {
|
|
1048
|
-
createPush: (device, session, region) => new DreamePush({
|
|
1125
|
+
createPush: (device, session, region, onAuthFailure) => new DreamePush({
|
|
1126
|
+
device,
|
|
1127
|
+
session,
|
|
1128
|
+
region,
|
|
1129
|
+
...onAuthFailure !== void 0 ? { onAuthFailure } : {}
|
|
1130
|
+
}),
|
|
1049
1131
|
getProperties: (base, props) => getProperties(base, props),
|
|
1050
1132
|
getCachedProperties: (base, props) => getCachedProperties(base, props),
|
|
1051
1133
|
setProperties: (base, writes) => setProperties(base, writes),
|
|
@@ -1057,6 +1139,7 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
1057
1139
|
#device;
|
|
1058
1140
|
#region;
|
|
1059
1141
|
#sessionRef;
|
|
1142
|
+
#onAuthFailure;
|
|
1060
1143
|
#deps;
|
|
1061
1144
|
#fetchInitial;
|
|
1062
1145
|
#initialProps;
|
|
@@ -1072,6 +1155,7 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
1072
1155
|
this.#device = input.device;
|
|
1073
1156
|
this.#region = input.region;
|
|
1074
1157
|
this.#sessionRef = input.sessionRef;
|
|
1158
|
+
this.#onAuthFailure = input.onAuthFailure;
|
|
1075
1159
|
this.#deps = input.deps ?? defaultBaseDeviceDeps();
|
|
1076
1160
|
this.#fetchInitial = input.fetchInitialValues ?? true;
|
|
1077
1161
|
this.#initialProps = input.initialProps ?? [];
|
|
@@ -1111,7 +1195,12 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
1111
1195
|
}
|
|
1112
1196
|
/** Open the push, wire events, optionally seed the cache. */
|
|
1113
1197
|
async start() {
|
|
1114
|
-
const push = this.#deps.createPush(
|
|
1198
|
+
const push = this.#deps.createPush(
|
|
1199
|
+
this.#device,
|
|
1200
|
+
this.#sessionRef(),
|
|
1201
|
+
this.#region,
|
|
1202
|
+
this.#onAuthFailure
|
|
1203
|
+
);
|
|
1115
1204
|
this.#push = push;
|
|
1116
1205
|
push.on("properties", (changes) => this.#onProperties(changes));
|
|
1117
1206
|
push.on(
|
|
@@ -4951,11 +5040,15 @@ var DEFAULT_HEIGHT = 1200;
|
|
|
4951
5040
|
var DEFAULT_PADDING = 50;
|
|
4952
5041
|
var BACKGROUND = "#f5f5f0";
|
|
4953
5042
|
var MAP_BOUNDARY = "#006400";
|
|
4954
|
-
var MOWING_PATH = "#
|
|
5043
|
+
var MOWING_PATH = "#32cd32";
|
|
4955
5044
|
var NAV_PATH = "#b4b4b4";
|
|
4956
5045
|
var OBSTACLE_STROKE = "#ff4d00";
|
|
4957
5046
|
var OBSTACLE_FILL = "#ff4d0065";
|
|
4958
5047
|
var TEXT_COLOR = "#000000";
|
|
5048
|
+
var ZONE_LABEL_COLOR = "#3c3c3c";
|
|
5049
|
+
function escapeXml(s) {
|
|
5050
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5051
|
+
}
|
|
4959
5052
|
var ZONE_COLORS = [
|
|
4960
5053
|
["#a4d291c8", "#86be73"],
|
|
4961
5054
|
["#a0c8dcc8", "#82aac8"],
|
|
@@ -5078,7 +5171,6 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
5078
5171
|
return lines.join("\n");
|
|
5079
5172
|
}
|
|
5080
5173
|
const bounds = calculateBounds(allPoints);
|
|
5081
|
-
const multiZone = map.zones.length > 1;
|
|
5082
5174
|
for (const navPath of map.paths) {
|
|
5083
5175
|
const dashed = svgPathFromSegments(
|
|
5084
5176
|
[navPath.path],
|
|
@@ -5095,9 +5187,6 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
5095
5187
|
}
|
|
5096
5188
|
}
|
|
5097
5189
|
map.zones.forEach((zone, i) => {
|
|
5098
|
-
if (!multiZone) {
|
|
5099
|
-
return;
|
|
5100
|
-
}
|
|
5101
5190
|
const palette = ZONE_COLORS[i % ZONE_COLORS.length];
|
|
5102
5191
|
const [fill, outline] = palette ?? ZONE_COLORS[0] ?? ["#cccccc", "#888888"];
|
|
5103
5192
|
const poly = svgPolygon(zone.path, bounds, width, height, padding, fill, outline);
|
|
@@ -5106,7 +5195,7 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
5106
5195
|
}
|
|
5107
5196
|
});
|
|
5108
5197
|
map.zones.forEach((zone, i) => {
|
|
5109
|
-
const outline =
|
|
5198
|
+
const outline = ZONE_COLORS[i % ZONE_COLORS.length]?.[1] ?? MAP_BOUNDARY;
|
|
5110
5199
|
const boundary = svgPathFromSegments(
|
|
5111
5200
|
[zone.path],
|
|
5112
5201
|
bounds,
|
|
@@ -5150,6 +5239,22 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
5150
5239
|
lines.push(poly);
|
|
5151
5240
|
}
|
|
5152
5241
|
}
|
|
5242
|
+
for (const zone of map.zones) {
|
|
5243
|
+
if (zone.name.length === 0 || zone.path.length === 0) {
|
|
5244
|
+
continue;
|
|
5245
|
+
}
|
|
5246
|
+
let sumX = 0;
|
|
5247
|
+
let sumY = 0;
|
|
5248
|
+
for (const p of zone.path) {
|
|
5249
|
+
sumX += p.x;
|
|
5250
|
+
sumY += p.y;
|
|
5251
|
+
}
|
|
5252
|
+
const centroid = { x: sumX / zone.path.length, y: sumY / zone.path.length };
|
|
5253
|
+
const { px, py } = coordToPixel(centroid, bounds, width, height, padding);
|
|
5254
|
+
lines.push(
|
|
5255
|
+
`<text x="${px}" y="${py}" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="${ZONE_LABEL_COLOR}" text-anchor="middle" dominant-baseline="middle">${escapeXml(zone.name)}</text>`
|
|
5256
|
+
);
|
|
5257
|
+
}
|
|
5153
5258
|
lines.push("</svg>");
|
|
5154
5259
|
return lines.join("\n");
|
|
5155
5260
|
}
|
|
@@ -5509,6 +5614,7 @@ function defaultDeps(opts) {
|
|
|
5509
5614
|
device: args.device,
|
|
5510
5615
|
region: args.region,
|
|
5511
5616
|
sessionRef: args.sessionRef,
|
|
5617
|
+
...args.onAuthFailure !== void 0 ? { onAuthFailure: args.onAuthFailure } : {},
|
|
5512
5618
|
...opts.fetchInitialValues !== void 0 ? { fetchInitialValues: opts.fetchInitialValues } : {},
|
|
5513
5619
|
...opts.pollIntervalMs !== void 0 ? { pollIntervalMs: opts.pollIntervalMs } : {}
|
|
5514
5620
|
});
|
|
@@ -5522,6 +5628,15 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5522
5628
|
#session = null;
|
|
5523
5629
|
#devices = [];
|
|
5524
5630
|
#closed = false;
|
|
5631
|
+
/** Background timer that proactively refreshes before the token expires. */
|
|
5632
|
+
#refreshTimer = null;
|
|
5633
|
+
/** Guard against overlapping proactive-refresh runs. */
|
|
5634
|
+
#refreshInFlight = false;
|
|
5635
|
+
/**
|
|
5636
|
+
* The single in-flight refresh, shared by concurrent callers so the proactive
|
|
5637
|
+
* timer and a manual `ensureSession()`/`reauthenticate()` never double-refresh.
|
|
5638
|
+
*/
|
|
5639
|
+
#inFlightRefresh = null;
|
|
5525
5640
|
constructor(opts, deps) {
|
|
5526
5641
|
super();
|
|
5527
5642
|
if (!opts.username || !opts.password) {
|
|
@@ -5550,6 +5665,7 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5550
5665
|
...this.#opts.lang !== void 0 ? { lang: this.#opts.lang } : {},
|
|
5551
5666
|
...this.#opts.fetchImpl !== void 0 ? { fetchImpl: this.#opts.fetchImpl } : {}
|
|
5552
5667
|
});
|
|
5668
|
+
this.#scheduleProactiveRefresh();
|
|
5553
5669
|
return this.#session;
|
|
5554
5670
|
}
|
|
5555
5671
|
/** Return a valid session, refreshing proactively within the leeway window. */
|
|
@@ -5561,18 +5677,61 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5561
5677
|
if (Date.now() < current.expiresAt - this.#leewayMs) {
|
|
5562
5678
|
return current;
|
|
5563
5679
|
}
|
|
5680
|
+
return this.#refreshNow(current);
|
|
5681
|
+
}
|
|
5682
|
+
/**
|
|
5683
|
+
* Force a session refresh REGARDLESS of the current expiry, then propagate the
|
|
5684
|
+
* new token to every live device push. Wired as each push's `onAuthFailure`:
|
|
5685
|
+
* when a broker refuses a CONNECT because it considers the token stale (even
|
|
5686
|
+
* though {@link ensureSession} still thinks it valid — e.g. server-side early
|
|
5687
|
+
* revocation or clock skew), this mints a genuinely fresh token instead of
|
|
5688
|
+
* replaying the rejected one.
|
|
5689
|
+
*/
|
|
5690
|
+
async reauthenticate() {
|
|
5691
|
+
if (this.#closed) {
|
|
5692
|
+
throw new DreameAuthError("client is closed");
|
|
5693
|
+
}
|
|
5694
|
+
const current = this.#session;
|
|
5695
|
+
if (!current) {
|
|
5696
|
+
return this.login();
|
|
5697
|
+
}
|
|
5698
|
+
return this.#refreshNow(current);
|
|
5699
|
+
}
|
|
5700
|
+
/**
|
|
5701
|
+
* Refresh via the refresh-token (with a full re-login fallback) and propagate
|
|
5702
|
+
* the new token to every device push. Shared by {@link ensureSession} (within
|
|
5703
|
+
* the leeway window) and {@link reauthenticate} (unconditional).
|
|
5704
|
+
*/
|
|
5705
|
+
async #refreshNow(current) {
|
|
5706
|
+
const existing = this.#inFlightRefresh;
|
|
5707
|
+
if (existing) {
|
|
5708
|
+
return existing;
|
|
5709
|
+
}
|
|
5710
|
+
const run = this.#doRefresh(current);
|
|
5711
|
+
this.#inFlightRefresh = run;
|
|
5712
|
+
try {
|
|
5713
|
+
return await run;
|
|
5714
|
+
} finally {
|
|
5715
|
+
this.#inFlightRefresh = null;
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
async #doRefresh(current) {
|
|
5564
5719
|
if (current.refreshToken) {
|
|
5720
|
+
let next = null;
|
|
5565
5721
|
try {
|
|
5566
|
-
|
|
5722
|
+
next = await this.#deps.refresh({
|
|
5567
5723
|
refreshToken: current.refreshToken,
|
|
5568
5724
|
region: this.#opts.region,
|
|
5569
5725
|
...this.#opts.country !== void 0 ? { country: this.#opts.country } : {},
|
|
5570
5726
|
...this.#opts.lang !== void 0 ? { lang: this.#opts.lang } : {},
|
|
5571
5727
|
...this.#opts.fetchImpl !== void 0 ? { fetchImpl: this.#opts.fetchImpl } : {}
|
|
5572
5728
|
});
|
|
5729
|
+
} catch {
|
|
5730
|
+
next = null;
|
|
5731
|
+
}
|
|
5732
|
+
if (next) {
|
|
5573
5733
|
await this.#adoptSession(next);
|
|
5574
5734
|
return next;
|
|
5575
|
-
} catch {
|
|
5576
5735
|
}
|
|
5577
5736
|
}
|
|
5578
5737
|
const fresh = await this.login();
|
|
@@ -5594,23 +5753,30 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5594
5753
|
(device) => this.#deps.createDevice({
|
|
5595
5754
|
device,
|
|
5596
5755
|
region: this.#opts.region,
|
|
5597
|
-
sessionRef: () => this.#requireSession()
|
|
5756
|
+
sessionRef: () => this.#requireSession(),
|
|
5757
|
+
onAuthFailure: () => this.reauthenticate()
|
|
5598
5758
|
})
|
|
5599
5759
|
);
|
|
5600
5760
|
for (const h of handles) {
|
|
5601
5761
|
h.on("stateChanged", (e) => this.emit("stateChanged", e));
|
|
5602
5762
|
h.on("event", (e) => this.emit("event", e));
|
|
5603
|
-
h.on("error", (err) =>
|
|
5763
|
+
h.on("error", (err) => {
|
|
5764
|
+
if (this.listenerCount("error") > 0) {
|
|
5765
|
+
this.emit("error", err);
|
|
5766
|
+
}
|
|
5767
|
+
});
|
|
5604
5768
|
await h.start();
|
|
5605
5769
|
}
|
|
5606
5770
|
const previous = this.#devices;
|
|
5607
5771
|
this.#devices = [...handles];
|
|
5608
5772
|
await Promise.all(previous.map((d) => d.close()));
|
|
5773
|
+
this.#scheduleProactiveRefresh();
|
|
5609
5774
|
return this.#devices;
|
|
5610
5775
|
}
|
|
5611
5776
|
/** Tear everything down: close every device push and clear timers. */
|
|
5612
5777
|
async close() {
|
|
5613
5778
|
this.#closed = true;
|
|
5779
|
+
this.#clearRefreshTimer();
|
|
5614
5780
|
const devices = this.#devices;
|
|
5615
5781
|
this.#devices = [];
|
|
5616
5782
|
await Promise.all(devices.map((d) => d.close()));
|
|
@@ -5625,13 +5791,65 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5625
5791
|
async #adoptSession(session) {
|
|
5626
5792
|
this.#session = session;
|
|
5627
5793
|
await this.#propagateSession(session);
|
|
5794
|
+
this.#scheduleProactiveRefresh();
|
|
5795
|
+
}
|
|
5796
|
+
#clearRefreshTimer() {
|
|
5797
|
+
if (this.#refreshTimer) {
|
|
5798
|
+
clearTimeout(this.#refreshTimer);
|
|
5799
|
+
this.#refreshTimer = null;
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
/** Arm the proactive-refresh timer `delayMs` from now (clamped to ≥0). */
|
|
5803
|
+
#armRefreshTimer(delayMs) {
|
|
5804
|
+
this.#clearRefreshTimer();
|
|
5805
|
+
if (this.#closed) {
|
|
5806
|
+
return;
|
|
5807
|
+
}
|
|
5808
|
+
this.#refreshTimer = setTimeout(() => {
|
|
5809
|
+
this.#refreshTimer = null;
|
|
5810
|
+
void this.#runProactiveRefresh();
|
|
5811
|
+
}, Math.max(0, delayMs));
|
|
5812
|
+
}
|
|
5813
|
+
/** (Re)arm the proactive-refresh timer against the current session's expiry. */
|
|
5814
|
+
#scheduleProactiveRefresh() {
|
|
5815
|
+
const session = this.#session;
|
|
5816
|
+
if (!session) {
|
|
5817
|
+
this.#clearRefreshTimer();
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
this.#armRefreshTimer(session.expiresAt - this.#leewayMs - Date.now());
|
|
5821
|
+
}
|
|
5822
|
+
/**
|
|
5823
|
+
* Timer body: refresh the session before it expires, then reschedule against
|
|
5824
|
+
* the new expiry. Because {@link ensureSession} propagates the refreshed token
|
|
5825
|
+
* to every device push, each MQTT connection is re-keyed BEFORE the broker
|
|
5826
|
+
* would reject the old one — the reactive re-auth loop never even starts.
|
|
5827
|
+
*/
|
|
5828
|
+
async #runProactiveRefresh() {
|
|
5829
|
+
if (this.#closed || this.#refreshInFlight) {
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
this.#refreshInFlight = true;
|
|
5833
|
+
try {
|
|
5834
|
+
await this.ensureSession();
|
|
5835
|
+
this.#refreshInFlight = false;
|
|
5836
|
+
this.#scheduleProactiveRefresh();
|
|
5837
|
+
} catch (err) {
|
|
5838
|
+
this.#refreshInFlight = false;
|
|
5839
|
+
if (this.listenerCount("error") > 0) {
|
|
5840
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
5841
|
+
}
|
|
5842
|
+
if (!this.#closed) {
|
|
5843
|
+
this.#armRefreshTimer(this.#leewayMs);
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5628
5846
|
}
|
|
5629
5847
|
/** Push the refreshed token to every live device push. */
|
|
5630
5848
|
async #propagateSession(session) {
|
|
5631
5849
|
if (this.#closed) {
|
|
5632
5850
|
return;
|
|
5633
5851
|
}
|
|
5634
|
-
await Promise.
|
|
5852
|
+
await Promise.allSettled(this.#devices.map((d) => d.applySession(session)));
|
|
5635
5853
|
}
|
|
5636
5854
|
};
|
|
5637
5855
|
|
|
@@ -6134,6 +6352,7 @@ function createClientDumper(client, options) {
|
|
|
6134
6352
|
extractMowerConsumableValues,
|
|
6135
6353
|
getMowerCapabilities,
|
|
6136
6354
|
getVacuumCapabilities,
|
|
6355
|
+
isAuthRefusedError,
|
|
6137
6356
|
isDreameConsumableKey,
|
|
6138
6357
|
mowerConsumableIndex,
|
|
6139
6358
|
mowerFaultSeverity,
|