@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.d.cts
CHANGED
|
@@ -169,6 +169,8 @@ declare class TypedEmitter<Events extends Record<string, unknown[]>> {
|
|
|
169
169
|
once<K extends keyof Events & string>(event: K, listener: (...args: Events[K]) => void): this;
|
|
170
170
|
off<K extends keyof Events & string>(event: K, listener: (...args: Events[K]) => void): this;
|
|
171
171
|
emit<K extends keyof Events & string>(event: K, ...args: Events[K]): boolean;
|
|
172
|
+
/** Number of listeners registered for `event`. */
|
|
173
|
+
listenerCount<K extends keyof Events & string>(event: K): number;
|
|
172
174
|
removeAllListeners(): this;
|
|
173
175
|
}
|
|
174
176
|
|
|
@@ -179,6 +181,14 @@ interface PropertyChange {
|
|
|
179
181
|
piid: number;
|
|
180
182
|
value: unknown;
|
|
181
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* True when `err` (or an error in its `cause` chain) is an MQTT CONNACK
|
|
186
|
+
* auth-refusal. mqtt.js raises an `ErrorWithReasonCode` carrying a numeric
|
|
187
|
+
* `code`: MQTT 3.1.1 uses 4 (bad user/pass) / 5 (not authorized); MQTT 5 uses
|
|
188
|
+
* 134 / 135. We also match the human-readable message as a belt-and-braces
|
|
189
|
+
* fallback in case the code is not surfaced through a wrapper.
|
|
190
|
+
*/
|
|
191
|
+
declare function isAuthRefusedError(err: unknown): boolean;
|
|
182
192
|
|
|
183
193
|
/**
|
|
184
194
|
* A read-only set of capability tokens a device model supports. Phase 2 ships
|
|
@@ -294,7 +304,7 @@ interface PushLike {
|
|
|
294
304
|
}
|
|
295
305
|
/** Injectable collaborators — defaults wire the real transport + command layer. */
|
|
296
306
|
interface BaseDeviceDeps {
|
|
297
|
-
createPush(device: DreameDevice, session: DreameSession, region: DreameRegion): PushLike;
|
|
307
|
+
createPush(device: DreameDevice, session: DreameSession, region: DreameRegion, onAuthFailure?: () => Promise<DreameSession>): PushLike;
|
|
298
308
|
getProperties(base: CommonInput, props: MiotProp[]): Promise<PropertyResult[]>;
|
|
299
309
|
/** Read the cloud-cached (shadow) values WITHOUT waking the device. */
|
|
300
310
|
getCachedProperties(base: CommonInput, props: MiotProp[]): Promise<PropertyResult[]>;
|
|
@@ -316,6 +326,12 @@ interface BaseDeviceInput {
|
|
|
316
326
|
region: DreameRegion;
|
|
317
327
|
/** Always reads the LATEST session — the facade owns the variable. */
|
|
318
328
|
sessionRef: () => DreameSession;
|
|
329
|
+
/**
|
|
330
|
+
* Force-refresh callback handed to the underlying push so it can self-heal a
|
|
331
|
+
* broker CONNACK auth-refusal (stale token) by minting a fresh session.
|
|
332
|
+
* Wired by the facade to `Nodreame.reauthenticate()`.
|
|
333
|
+
*/
|
|
334
|
+
onAuthFailure?: () => Promise<DreameSession>;
|
|
319
335
|
deps?: BaseDeviceDeps;
|
|
320
336
|
/** Eager-seed the cache on `start()`. Default true. */
|
|
321
337
|
fetchInitialValues?: boolean;
|
|
@@ -376,6 +392,12 @@ interface CreateDeviceArgs {
|
|
|
376
392
|
device: DreameDevice;
|
|
377
393
|
region: DreameRegion;
|
|
378
394
|
sessionRef: () => DreameSession;
|
|
395
|
+
/**
|
|
396
|
+
* Force-refresh callback handed down to the device's MQTT push so it can
|
|
397
|
+
* self-heal a broker CONNACK auth-refusal (stale token). Bound by the facade
|
|
398
|
+
* to {@link Nodreame.reauthenticate}.
|
|
399
|
+
*/
|
|
400
|
+
onAuthFailure?: () => Promise<DreameSession>;
|
|
379
401
|
}
|
|
380
402
|
/** Injectable collaborators — defaults wire the real P1 modules. */
|
|
381
403
|
interface NodreameDeps {
|
|
@@ -419,6 +441,15 @@ declare class Nodreame extends TypedEmitter<NodreameEvents> {
|
|
|
419
441
|
login(): Promise<DreameSession>;
|
|
420
442
|
/** Return a valid session, refreshing proactively within the leeway window. */
|
|
421
443
|
ensureSession(): Promise<DreameSession>;
|
|
444
|
+
/**
|
|
445
|
+
* Force a session refresh REGARDLESS of the current expiry, then propagate the
|
|
446
|
+
* new token to every live device push. Wired as each push's `onAuthFailure`:
|
|
447
|
+
* when a broker refuses a CONNECT because it considers the token stale (even
|
|
448
|
+
* though {@link ensureSession} still thinks it valid — e.g. server-side early
|
|
449
|
+
* revocation or clock skew), this mints a genuinely fresh token instead of
|
|
450
|
+
* replaying the rejected one.
|
|
451
|
+
*/
|
|
452
|
+
reauthenticate(): Promise<DreameSession>;
|
|
422
453
|
/** Discover devices and build a live handle per device. */
|
|
423
454
|
discoverDevices(): Promise<readonly BaseDevice[]>;
|
|
424
455
|
/** Tear everything down: close every device push and clear timers. */
|
|
@@ -2598,4 +2629,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
|
|
|
2598
2629
|
*/
|
|
2599
2630
|
declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
|
|
2600
2631
|
|
|
2601
|
-
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, MOWER_TASK_SUBSTATES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerFaultSeverity, type MowerHeartbeat, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerMowingProgress, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerTaskSubState, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, mowerConsumableIndex, mowerFaultSeverity, parseMowerConsumables, parseMowerHeartbeat, parseMowingProgress, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
|
2632
|
+
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, MOWER_TASK_SUBSTATES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerFaultSeverity, type MowerHeartbeat, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerMowingProgress, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerTaskSubState, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isAuthRefusedError, isDreameConsumableKey, mowerConsumableIndex, mowerFaultSeverity, parseMowerConsumables, parseMowerHeartbeat, parseMowingProgress, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
package/dist/index.d.ts
CHANGED
|
@@ -169,6 +169,8 @@ declare class TypedEmitter<Events extends Record<string, unknown[]>> {
|
|
|
169
169
|
once<K extends keyof Events & string>(event: K, listener: (...args: Events[K]) => void): this;
|
|
170
170
|
off<K extends keyof Events & string>(event: K, listener: (...args: Events[K]) => void): this;
|
|
171
171
|
emit<K extends keyof Events & string>(event: K, ...args: Events[K]): boolean;
|
|
172
|
+
/** Number of listeners registered for `event`. */
|
|
173
|
+
listenerCount<K extends keyof Events & string>(event: K): number;
|
|
172
174
|
removeAllListeners(): this;
|
|
173
175
|
}
|
|
174
176
|
|
|
@@ -179,6 +181,14 @@ interface PropertyChange {
|
|
|
179
181
|
piid: number;
|
|
180
182
|
value: unknown;
|
|
181
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* True when `err` (or an error in its `cause` chain) is an MQTT CONNACK
|
|
186
|
+
* auth-refusal. mqtt.js raises an `ErrorWithReasonCode` carrying a numeric
|
|
187
|
+
* `code`: MQTT 3.1.1 uses 4 (bad user/pass) / 5 (not authorized); MQTT 5 uses
|
|
188
|
+
* 134 / 135. We also match the human-readable message as a belt-and-braces
|
|
189
|
+
* fallback in case the code is not surfaced through a wrapper.
|
|
190
|
+
*/
|
|
191
|
+
declare function isAuthRefusedError(err: unknown): boolean;
|
|
182
192
|
|
|
183
193
|
/**
|
|
184
194
|
* A read-only set of capability tokens a device model supports. Phase 2 ships
|
|
@@ -294,7 +304,7 @@ interface PushLike {
|
|
|
294
304
|
}
|
|
295
305
|
/** Injectable collaborators — defaults wire the real transport + command layer. */
|
|
296
306
|
interface BaseDeviceDeps {
|
|
297
|
-
createPush(device: DreameDevice, session: DreameSession, region: DreameRegion): PushLike;
|
|
307
|
+
createPush(device: DreameDevice, session: DreameSession, region: DreameRegion, onAuthFailure?: () => Promise<DreameSession>): PushLike;
|
|
298
308
|
getProperties(base: CommonInput, props: MiotProp[]): Promise<PropertyResult[]>;
|
|
299
309
|
/** Read the cloud-cached (shadow) values WITHOUT waking the device. */
|
|
300
310
|
getCachedProperties(base: CommonInput, props: MiotProp[]): Promise<PropertyResult[]>;
|
|
@@ -316,6 +326,12 @@ interface BaseDeviceInput {
|
|
|
316
326
|
region: DreameRegion;
|
|
317
327
|
/** Always reads the LATEST session — the facade owns the variable. */
|
|
318
328
|
sessionRef: () => DreameSession;
|
|
329
|
+
/**
|
|
330
|
+
* Force-refresh callback handed to the underlying push so it can self-heal a
|
|
331
|
+
* broker CONNACK auth-refusal (stale token) by minting a fresh session.
|
|
332
|
+
* Wired by the facade to `Nodreame.reauthenticate()`.
|
|
333
|
+
*/
|
|
334
|
+
onAuthFailure?: () => Promise<DreameSession>;
|
|
319
335
|
deps?: BaseDeviceDeps;
|
|
320
336
|
/** Eager-seed the cache on `start()`. Default true. */
|
|
321
337
|
fetchInitialValues?: boolean;
|
|
@@ -376,6 +392,12 @@ interface CreateDeviceArgs {
|
|
|
376
392
|
device: DreameDevice;
|
|
377
393
|
region: DreameRegion;
|
|
378
394
|
sessionRef: () => DreameSession;
|
|
395
|
+
/**
|
|
396
|
+
* Force-refresh callback handed down to the device's MQTT push so it can
|
|
397
|
+
* self-heal a broker CONNACK auth-refusal (stale token). Bound by the facade
|
|
398
|
+
* to {@link Nodreame.reauthenticate}.
|
|
399
|
+
*/
|
|
400
|
+
onAuthFailure?: () => Promise<DreameSession>;
|
|
379
401
|
}
|
|
380
402
|
/** Injectable collaborators — defaults wire the real P1 modules. */
|
|
381
403
|
interface NodreameDeps {
|
|
@@ -419,6 +441,15 @@ declare class Nodreame extends TypedEmitter<NodreameEvents> {
|
|
|
419
441
|
login(): Promise<DreameSession>;
|
|
420
442
|
/** Return a valid session, refreshing proactively within the leeway window. */
|
|
421
443
|
ensureSession(): Promise<DreameSession>;
|
|
444
|
+
/**
|
|
445
|
+
* Force a session refresh REGARDLESS of the current expiry, then propagate the
|
|
446
|
+
* new token to every live device push. Wired as each push's `onAuthFailure`:
|
|
447
|
+
* when a broker refuses a CONNECT because it considers the token stale (even
|
|
448
|
+
* though {@link ensureSession} still thinks it valid — e.g. server-side early
|
|
449
|
+
* revocation or clock skew), this mints a genuinely fresh token instead of
|
|
450
|
+
* replaying the rejected one.
|
|
451
|
+
*/
|
|
452
|
+
reauthenticate(): Promise<DreameSession>;
|
|
422
453
|
/** Discover devices and build a live handle per device. */
|
|
423
454
|
discoverDevices(): Promise<readonly BaseDevice[]>;
|
|
424
455
|
/** Tear everything down: close every device push and clear timers. */
|
|
@@ -2598,4 +2629,4 @@ declare function createDumper(target: DumperDevice, options?: DumperOptions): Du
|
|
|
2598
2629
|
*/
|
|
2599
2630
|
declare function createClientDumper(client: Nodreame, options?: DumperOptions): Dumper[];
|
|
2600
2631
|
|
|
2601
|
-
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, MOWER_TASK_SUBSTATES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerFaultSeverity, type MowerHeartbeat, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerMowingProgress, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerTaskSubState, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isDreameConsumableKey, mowerConsumableIndex, mowerFaultSeverity, parseMowerConsumables, parseMowerHeartbeat, parseMowingProgress, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
|
2632
|
+
export { AI_FEATURE_BIT, AI_FEATURE_JSON_KEY, AUTO_SWITCH_JSON_KEY, type AiDetectionRaw, type AutoSwitchKey, type AutoSwitchRaw, BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type ConsumableReading, type ConsumableSpec, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceDump, type DeviceEvent, type DreameAiFeature, DreameApiError, DreameAuthError, type DreameCloudState, type DreameConsumableKey, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, type DumperOptions, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, MOWER_TASK_SUBSTATES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapColorScheme, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerConsumableKey, type MowerConsumableReading, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, MowerFault, type MowerFaultSeverity, type MowerHeartbeat, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerMowingProgress, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerTaskSubState, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, VACUUM_CONSUMABLES, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, consumableSpec, createClientDumper, createDumper, decodeAiFeature, decodeAutoSwitch, decodeAutoSwitchAll, encodeAiFeatureWrite, encodeAutoSwitchWrite, extractMowerConsumableValues, getMowerCapabilities, getVacuumCapabilities, isAuthRefusedError, isDreameConsumableKey, mowerConsumableIndex, mowerFaultSeverity, parseMowerConsumables, parseMowerHeartbeat, parseMowingProgress, renderMowerSvg, renderVacuumPng, resolveCapabilities, supportedAutoSwitchKeys };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/support/version.ts
|
|
2
2
|
var LIBRARY_NAME = "nodedreame";
|
|
3
|
-
var LIBRARY_VERSION = "1.11.
|
|
3
|
+
var LIBRARY_VERSION = "1.11.10";
|
|
4
4
|
|
|
5
5
|
// src/transport/errors.ts
|
|
6
6
|
var DreameError = class extends Error {
|
|
@@ -629,6 +629,10 @@ var TypedEmitter = class {
|
|
|
629
629
|
emit(event, ...args) {
|
|
630
630
|
return this.#emitter.emit(event, ...args);
|
|
631
631
|
}
|
|
632
|
+
/** Number of listeners registered for `event`. */
|
|
633
|
+
listenerCount(event) {
|
|
634
|
+
return this.#emitter.listenerCount(event);
|
|
635
|
+
}
|
|
632
636
|
removeAllListeners() {
|
|
633
637
|
this.#emitter.removeAllListeners();
|
|
634
638
|
return this;
|
|
@@ -641,6 +645,24 @@ function defaultConnect(url, opts) {
|
|
|
641
645
|
const typed = client;
|
|
642
646
|
return typed;
|
|
643
647
|
}
|
|
648
|
+
function isAuthRefusedError(err) {
|
|
649
|
+
let cur = err;
|
|
650
|
+
for (let depth = 0; cur != null && depth < 6; depth += 1) {
|
|
651
|
+
if (typeof cur !== "object") {
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
const code = cur.code;
|
|
655
|
+
if (code === 4 || code === 5 || code === 134 || code === 135) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
const message = cur.message;
|
|
659
|
+
if (typeof message === "string" && /not authorized|bad user ?name|bad username or password|bad password/i.test(message)) {
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
cur = cur.cause;
|
|
663
|
+
}
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
644
666
|
var DreamePush = class extends TypedEmitter {
|
|
645
667
|
#device;
|
|
646
668
|
#session;
|
|
@@ -649,11 +671,15 @@ var DreamePush = class extends TypedEmitter {
|
|
|
649
671
|
#backoffMs;
|
|
650
672
|
#keepaliveSeconds;
|
|
651
673
|
#rejectUnauthorized;
|
|
674
|
+
#onAuthFailure;
|
|
675
|
+
#maxAuthRetries;
|
|
652
676
|
#topic;
|
|
653
677
|
#client = null;
|
|
654
678
|
#closed = false;
|
|
655
679
|
#tearingDown = false;
|
|
656
680
|
#reconnectTimer = null;
|
|
681
|
+
/** Consecutive auth-refused re-auths since the last successful connect. */
|
|
682
|
+
#authFailureCount = 0;
|
|
657
683
|
/**
|
|
658
684
|
* Per-instance bound close handler. Stored so we can pass the EXACT same
|
|
659
685
|
* function reference to both `client.on('close', …)` and
|
|
@@ -670,6 +696,8 @@ var DreamePush = class extends TypedEmitter {
|
|
|
670
696
|
this.#backoffMs = input.reconnectBackoffMs ?? 5e3;
|
|
671
697
|
this.#keepaliveSeconds = input.keepaliveSeconds ?? 60;
|
|
672
698
|
this.#rejectUnauthorized = input.rejectUnauthorized ?? false;
|
|
699
|
+
this.#onAuthFailure = input.onAuthFailure ?? null;
|
|
700
|
+
this.#maxAuthRetries = input.maxAuthRetries ?? 3;
|
|
673
701
|
this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
|
|
674
702
|
this.#onClose = () => {
|
|
675
703
|
if (this.#tearingDown) {
|
|
@@ -706,7 +734,11 @@ var DreamePush = class extends TypedEmitter {
|
|
|
706
734
|
this.#reconnectTimer = null;
|
|
707
735
|
}
|
|
708
736
|
await this.#teardownClient();
|
|
709
|
-
|
|
737
|
+
try {
|
|
738
|
+
await this.#connectAndSubscribe();
|
|
739
|
+
} catch {
|
|
740
|
+
this.#scheduleReconnect();
|
|
741
|
+
}
|
|
710
742
|
}
|
|
711
743
|
/** Tear down permanently. Closed subscriptions cannot be reopened. */
|
|
712
744
|
async close() {
|
|
@@ -743,6 +775,7 @@ var DreamePush = class extends TypedEmitter {
|
|
|
743
775
|
reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
|
|
744
776
|
return;
|
|
745
777
|
}
|
|
778
|
+
this.#authFailureCount = 0;
|
|
746
779
|
this.emit("connect");
|
|
747
780
|
resolve();
|
|
748
781
|
});
|
|
@@ -776,12 +809,55 @@ var DreamePush = class extends TypedEmitter {
|
|
|
776
809
|
if (this.#closed || this.#client) {
|
|
777
810
|
return;
|
|
778
811
|
}
|
|
779
|
-
void this.#
|
|
780
|
-
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
781
|
-
this.#scheduleReconnect();
|
|
782
|
-
});
|
|
812
|
+
void this.#attemptReconnect();
|
|
783
813
|
}, this.#backoffMs);
|
|
784
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* One reconnect attempt with reactive auth self-heal. If the broker refuses
|
|
817
|
+
* the CONNECT with an auth error (a stale access token) and an
|
|
818
|
+
* `onAuthFailure` provider is wired and the re-auth budget is not exhausted,
|
|
819
|
+
* mint a fresh session and retry with the new token instead of replaying the
|
|
820
|
+
* old one. Any other failure (or an exhausted budget) falls back to the plain
|
|
821
|
+
* backoff reconnect so bad credentials cannot hot-loop re-login.
|
|
822
|
+
*/
|
|
823
|
+
async #attemptReconnect() {
|
|
824
|
+
try {
|
|
825
|
+
await this.#connectAndSubscribe();
|
|
826
|
+
} catch (err) {
|
|
827
|
+
if (this.#closed) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (this.#onAuthFailure && isAuthRefusedError(err) && this.#authFailureCount < this.#maxAuthRetries) {
|
|
831
|
+
await this.#reauthenticateAndReconnect();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
835
|
+
this.#scheduleReconnect();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/** Mint a fresh session via `onAuthFailure`, adopt its token, and reconnect. */
|
|
839
|
+
async #reauthenticateAndReconnect() {
|
|
840
|
+
this.#authFailureCount += 1;
|
|
841
|
+
let session;
|
|
842
|
+
try {
|
|
843
|
+
session = await this.#onAuthFailure();
|
|
844
|
+
} catch (err) {
|
|
845
|
+
this.emit("error", err instanceof Error ? err : new DreameTransportError(String(err)));
|
|
846
|
+
if (!this.#client) {
|
|
847
|
+
this.#scheduleReconnect();
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (this.#closed) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
this.#session = session;
|
|
855
|
+
this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
|
|
856
|
+
if (this.#client) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
void this.#attemptReconnect();
|
|
860
|
+
}
|
|
785
861
|
async #teardownClient() {
|
|
786
862
|
const client = this.#client;
|
|
787
863
|
this.#client = null;
|
|
@@ -957,7 +1033,12 @@ function resolveCapabilities(model) {
|
|
|
957
1033
|
// src/device/base-device.ts
|
|
958
1034
|
function defaultBaseDeviceDeps() {
|
|
959
1035
|
return {
|
|
960
|
-
createPush: (device, session, region) => new DreamePush({
|
|
1036
|
+
createPush: (device, session, region, onAuthFailure) => new DreamePush({
|
|
1037
|
+
device,
|
|
1038
|
+
session,
|
|
1039
|
+
region,
|
|
1040
|
+
...onAuthFailure !== void 0 ? { onAuthFailure } : {}
|
|
1041
|
+
}),
|
|
961
1042
|
getProperties: (base, props) => getProperties(base, props),
|
|
962
1043
|
getCachedProperties: (base, props) => getCachedProperties(base, props),
|
|
963
1044
|
setProperties: (base, writes) => setProperties(base, writes),
|
|
@@ -969,6 +1050,7 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
969
1050
|
#device;
|
|
970
1051
|
#region;
|
|
971
1052
|
#sessionRef;
|
|
1053
|
+
#onAuthFailure;
|
|
972
1054
|
#deps;
|
|
973
1055
|
#fetchInitial;
|
|
974
1056
|
#initialProps;
|
|
@@ -984,6 +1066,7 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
984
1066
|
this.#device = input.device;
|
|
985
1067
|
this.#region = input.region;
|
|
986
1068
|
this.#sessionRef = input.sessionRef;
|
|
1069
|
+
this.#onAuthFailure = input.onAuthFailure;
|
|
987
1070
|
this.#deps = input.deps ?? defaultBaseDeviceDeps();
|
|
988
1071
|
this.#fetchInitial = input.fetchInitialValues ?? true;
|
|
989
1072
|
this.#initialProps = input.initialProps ?? [];
|
|
@@ -1023,7 +1106,12 @@ var BaseDevice = class extends TypedEmitter {
|
|
|
1023
1106
|
}
|
|
1024
1107
|
/** Open the push, wire events, optionally seed the cache. */
|
|
1025
1108
|
async start() {
|
|
1026
|
-
const push = this.#deps.createPush(
|
|
1109
|
+
const push = this.#deps.createPush(
|
|
1110
|
+
this.#device,
|
|
1111
|
+
this.#sessionRef(),
|
|
1112
|
+
this.#region,
|
|
1113
|
+
this.#onAuthFailure
|
|
1114
|
+
);
|
|
1027
1115
|
this.#push = push;
|
|
1028
1116
|
push.on("properties", (changes) => this.#onProperties(changes));
|
|
1029
1117
|
push.on(
|
|
@@ -4863,11 +4951,15 @@ var DEFAULT_HEIGHT = 1200;
|
|
|
4863
4951
|
var DEFAULT_PADDING = 50;
|
|
4864
4952
|
var BACKGROUND = "#f5f5f0";
|
|
4865
4953
|
var MAP_BOUNDARY = "#006400";
|
|
4866
|
-
var MOWING_PATH = "#
|
|
4954
|
+
var MOWING_PATH = "#32cd32";
|
|
4867
4955
|
var NAV_PATH = "#b4b4b4";
|
|
4868
4956
|
var OBSTACLE_STROKE = "#ff4d00";
|
|
4869
4957
|
var OBSTACLE_FILL = "#ff4d0065";
|
|
4870
4958
|
var TEXT_COLOR = "#000000";
|
|
4959
|
+
var ZONE_LABEL_COLOR = "#3c3c3c";
|
|
4960
|
+
function escapeXml(s) {
|
|
4961
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4962
|
+
}
|
|
4871
4963
|
var ZONE_COLORS = [
|
|
4872
4964
|
["#a4d291c8", "#86be73"],
|
|
4873
4965
|
["#a0c8dcc8", "#82aac8"],
|
|
@@ -4990,7 +5082,6 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
4990
5082
|
return lines.join("\n");
|
|
4991
5083
|
}
|
|
4992
5084
|
const bounds = calculateBounds(allPoints);
|
|
4993
|
-
const multiZone = map.zones.length > 1;
|
|
4994
5085
|
for (const navPath of map.paths) {
|
|
4995
5086
|
const dashed = svgPathFromSegments(
|
|
4996
5087
|
[navPath.path],
|
|
@@ -5007,9 +5098,6 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
5007
5098
|
}
|
|
5008
5099
|
}
|
|
5009
5100
|
map.zones.forEach((zone, i) => {
|
|
5010
|
-
if (!multiZone) {
|
|
5011
|
-
return;
|
|
5012
|
-
}
|
|
5013
5101
|
const palette = ZONE_COLORS[i % ZONE_COLORS.length];
|
|
5014
5102
|
const [fill, outline] = palette ?? ZONE_COLORS[0] ?? ["#cccccc", "#888888"];
|
|
5015
5103
|
const poly = svgPolygon(zone.path, bounds, width, height, padding, fill, outline);
|
|
@@ -5018,7 +5106,7 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
5018
5106
|
}
|
|
5019
5107
|
});
|
|
5020
5108
|
map.zones.forEach((zone, i) => {
|
|
5021
|
-
const outline =
|
|
5109
|
+
const outline = ZONE_COLORS[i % ZONE_COLORS.length]?.[1] ?? MAP_BOUNDARY;
|
|
5022
5110
|
const boundary = svgPathFromSegments(
|
|
5023
5111
|
[zone.path],
|
|
5024
5112
|
bounds,
|
|
@@ -5062,6 +5150,22 @@ function renderMowerSvg(map, opts = {}) {
|
|
|
5062
5150
|
lines.push(poly);
|
|
5063
5151
|
}
|
|
5064
5152
|
}
|
|
5153
|
+
for (const zone of map.zones) {
|
|
5154
|
+
if (zone.name.length === 0 || zone.path.length === 0) {
|
|
5155
|
+
continue;
|
|
5156
|
+
}
|
|
5157
|
+
let sumX = 0;
|
|
5158
|
+
let sumY = 0;
|
|
5159
|
+
for (const p of zone.path) {
|
|
5160
|
+
sumX += p.x;
|
|
5161
|
+
sumY += p.y;
|
|
5162
|
+
}
|
|
5163
|
+
const centroid = { x: sumX / zone.path.length, y: sumY / zone.path.length };
|
|
5164
|
+
const { px, py } = coordToPixel(centroid, bounds, width, height, padding);
|
|
5165
|
+
lines.push(
|
|
5166
|
+
`<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>`
|
|
5167
|
+
);
|
|
5168
|
+
}
|
|
5065
5169
|
lines.push("</svg>");
|
|
5066
5170
|
return lines.join("\n");
|
|
5067
5171
|
}
|
|
@@ -5421,6 +5525,7 @@ function defaultDeps(opts) {
|
|
|
5421
5525
|
device: args.device,
|
|
5422
5526
|
region: args.region,
|
|
5423
5527
|
sessionRef: args.sessionRef,
|
|
5528
|
+
...args.onAuthFailure !== void 0 ? { onAuthFailure: args.onAuthFailure } : {},
|
|
5424
5529
|
...opts.fetchInitialValues !== void 0 ? { fetchInitialValues: opts.fetchInitialValues } : {},
|
|
5425
5530
|
...opts.pollIntervalMs !== void 0 ? { pollIntervalMs: opts.pollIntervalMs } : {}
|
|
5426
5531
|
});
|
|
@@ -5434,6 +5539,15 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5434
5539
|
#session = null;
|
|
5435
5540
|
#devices = [];
|
|
5436
5541
|
#closed = false;
|
|
5542
|
+
/** Background timer that proactively refreshes before the token expires. */
|
|
5543
|
+
#refreshTimer = null;
|
|
5544
|
+
/** Guard against overlapping proactive-refresh runs. */
|
|
5545
|
+
#refreshInFlight = false;
|
|
5546
|
+
/**
|
|
5547
|
+
* The single in-flight refresh, shared by concurrent callers so the proactive
|
|
5548
|
+
* timer and a manual `ensureSession()`/`reauthenticate()` never double-refresh.
|
|
5549
|
+
*/
|
|
5550
|
+
#inFlightRefresh = null;
|
|
5437
5551
|
constructor(opts, deps) {
|
|
5438
5552
|
super();
|
|
5439
5553
|
if (!opts.username || !opts.password) {
|
|
@@ -5462,6 +5576,7 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5462
5576
|
...this.#opts.lang !== void 0 ? { lang: this.#opts.lang } : {},
|
|
5463
5577
|
...this.#opts.fetchImpl !== void 0 ? { fetchImpl: this.#opts.fetchImpl } : {}
|
|
5464
5578
|
});
|
|
5579
|
+
this.#scheduleProactiveRefresh();
|
|
5465
5580
|
return this.#session;
|
|
5466
5581
|
}
|
|
5467
5582
|
/** Return a valid session, refreshing proactively within the leeway window. */
|
|
@@ -5473,18 +5588,61 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5473
5588
|
if (Date.now() < current.expiresAt - this.#leewayMs) {
|
|
5474
5589
|
return current;
|
|
5475
5590
|
}
|
|
5591
|
+
return this.#refreshNow(current);
|
|
5592
|
+
}
|
|
5593
|
+
/**
|
|
5594
|
+
* Force a session refresh REGARDLESS of the current expiry, then propagate the
|
|
5595
|
+
* new token to every live device push. Wired as each push's `onAuthFailure`:
|
|
5596
|
+
* when a broker refuses a CONNECT because it considers the token stale (even
|
|
5597
|
+
* though {@link ensureSession} still thinks it valid — e.g. server-side early
|
|
5598
|
+
* revocation or clock skew), this mints a genuinely fresh token instead of
|
|
5599
|
+
* replaying the rejected one.
|
|
5600
|
+
*/
|
|
5601
|
+
async reauthenticate() {
|
|
5602
|
+
if (this.#closed) {
|
|
5603
|
+
throw new DreameAuthError("client is closed");
|
|
5604
|
+
}
|
|
5605
|
+
const current = this.#session;
|
|
5606
|
+
if (!current) {
|
|
5607
|
+
return this.login();
|
|
5608
|
+
}
|
|
5609
|
+
return this.#refreshNow(current);
|
|
5610
|
+
}
|
|
5611
|
+
/**
|
|
5612
|
+
* Refresh via the refresh-token (with a full re-login fallback) and propagate
|
|
5613
|
+
* the new token to every device push. Shared by {@link ensureSession} (within
|
|
5614
|
+
* the leeway window) and {@link reauthenticate} (unconditional).
|
|
5615
|
+
*/
|
|
5616
|
+
async #refreshNow(current) {
|
|
5617
|
+
const existing = this.#inFlightRefresh;
|
|
5618
|
+
if (existing) {
|
|
5619
|
+
return existing;
|
|
5620
|
+
}
|
|
5621
|
+
const run = this.#doRefresh(current);
|
|
5622
|
+
this.#inFlightRefresh = run;
|
|
5623
|
+
try {
|
|
5624
|
+
return await run;
|
|
5625
|
+
} finally {
|
|
5626
|
+
this.#inFlightRefresh = null;
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
async #doRefresh(current) {
|
|
5476
5630
|
if (current.refreshToken) {
|
|
5631
|
+
let next = null;
|
|
5477
5632
|
try {
|
|
5478
|
-
|
|
5633
|
+
next = await this.#deps.refresh({
|
|
5479
5634
|
refreshToken: current.refreshToken,
|
|
5480
5635
|
region: this.#opts.region,
|
|
5481
5636
|
...this.#opts.country !== void 0 ? { country: this.#opts.country } : {},
|
|
5482
5637
|
...this.#opts.lang !== void 0 ? { lang: this.#opts.lang } : {},
|
|
5483
5638
|
...this.#opts.fetchImpl !== void 0 ? { fetchImpl: this.#opts.fetchImpl } : {}
|
|
5484
5639
|
});
|
|
5640
|
+
} catch {
|
|
5641
|
+
next = null;
|
|
5642
|
+
}
|
|
5643
|
+
if (next) {
|
|
5485
5644
|
await this.#adoptSession(next);
|
|
5486
5645
|
return next;
|
|
5487
|
-
} catch {
|
|
5488
5646
|
}
|
|
5489
5647
|
}
|
|
5490
5648
|
const fresh = await this.login();
|
|
@@ -5506,23 +5664,30 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5506
5664
|
(device) => this.#deps.createDevice({
|
|
5507
5665
|
device,
|
|
5508
5666
|
region: this.#opts.region,
|
|
5509
|
-
sessionRef: () => this.#requireSession()
|
|
5667
|
+
sessionRef: () => this.#requireSession(),
|
|
5668
|
+
onAuthFailure: () => this.reauthenticate()
|
|
5510
5669
|
})
|
|
5511
5670
|
);
|
|
5512
5671
|
for (const h of handles) {
|
|
5513
5672
|
h.on("stateChanged", (e) => this.emit("stateChanged", e));
|
|
5514
5673
|
h.on("event", (e) => this.emit("event", e));
|
|
5515
|
-
h.on("error", (err) =>
|
|
5674
|
+
h.on("error", (err) => {
|
|
5675
|
+
if (this.listenerCount("error") > 0) {
|
|
5676
|
+
this.emit("error", err);
|
|
5677
|
+
}
|
|
5678
|
+
});
|
|
5516
5679
|
await h.start();
|
|
5517
5680
|
}
|
|
5518
5681
|
const previous = this.#devices;
|
|
5519
5682
|
this.#devices = [...handles];
|
|
5520
5683
|
await Promise.all(previous.map((d) => d.close()));
|
|
5684
|
+
this.#scheduleProactiveRefresh();
|
|
5521
5685
|
return this.#devices;
|
|
5522
5686
|
}
|
|
5523
5687
|
/** Tear everything down: close every device push and clear timers. */
|
|
5524
5688
|
async close() {
|
|
5525
5689
|
this.#closed = true;
|
|
5690
|
+
this.#clearRefreshTimer();
|
|
5526
5691
|
const devices = this.#devices;
|
|
5527
5692
|
this.#devices = [];
|
|
5528
5693
|
await Promise.all(devices.map((d) => d.close()));
|
|
@@ -5537,13 +5702,65 @@ var Nodreame = class extends TypedEmitter {
|
|
|
5537
5702
|
async #adoptSession(session) {
|
|
5538
5703
|
this.#session = session;
|
|
5539
5704
|
await this.#propagateSession(session);
|
|
5705
|
+
this.#scheduleProactiveRefresh();
|
|
5706
|
+
}
|
|
5707
|
+
#clearRefreshTimer() {
|
|
5708
|
+
if (this.#refreshTimer) {
|
|
5709
|
+
clearTimeout(this.#refreshTimer);
|
|
5710
|
+
this.#refreshTimer = null;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
/** Arm the proactive-refresh timer `delayMs` from now (clamped to ≥0). */
|
|
5714
|
+
#armRefreshTimer(delayMs) {
|
|
5715
|
+
this.#clearRefreshTimer();
|
|
5716
|
+
if (this.#closed) {
|
|
5717
|
+
return;
|
|
5718
|
+
}
|
|
5719
|
+
this.#refreshTimer = setTimeout(() => {
|
|
5720
|
+
this.#refreshTimer = null;
|
|
5721
|
+
void this.#runProactiveRefresh();
|
|
5722
|
+
}, Math.max(0, delayMs));
|
|
5723
|
+
}
|
|
5724
|
+
/** (Re)arm the proactive-refresh timer against the current session's expiry. */
|
|
5725
|
+
#scheduleProactiveRefresh() {
|
|
5726
|
+
const session = this.#session;
|
|
5727
|
+
if (!session) {
|
|
5728
|
+
this.#clearRefreshTimer();
|
|
5729
|
+
return;
|
|
5730
|
+
}
|
|
5731
|
+
this.#armRefreshTimer(session.expiresAt - this.#leewayMs - Date.now());
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Timer body: refresh the session before it expires, then reschedule against
|
|
5735
|
+
* the new expiry. Because {@link ensureSession} propagates the refreshed token
|
|
5736
|
+
* to every device push, each MQTT connection is re-keyed BEFORE the broker
|
|
5737
|
+
* would reject the old one — the reactive re-auth loop never even starts.
|
|
5738
|
+
*/
|
|
5739
|
+
async #runProactiveRefresh() {
|
|
5740
|
+
if (this.#closed || this.#refreshInFlight) {
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
this.#refreshInFlight = true;
|
|
5744
|
+
try {
|
|
5745
|
+
await this.ensureSession();
|
|
5746
|
+
this.#refreshInFlight = false;
|
|
5747
|
+
this.#scheduleProactiveRefresh();
|
|
5748
|
+
} catch (err) {
|
|
5749
|
+
this.#refreshInFlight = false;
|
|
5750
|
+
if (this.listenerCount("error") > 0) {
|
|
5751
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
5752
|
+
}
|
|
5753
|
+
if (!this.#closed) {
|
|
5754
|
+
this.#armRefreshTimer(this.#leewayMs);
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5540
5757
|
}
|
|
5541
5758
|
/** Push the refreshed token to every live device push. */
|
|
5542
5759
|
async #propagateSession(session) {
|
|
5543
5760
|
if (this.#closed) {
|
|
5544
5761
|
return;
|
|
5545
5762
|
}
|
|
5546
|
-
await Promise.
|
|
5763
|
+
await Promise.allSettled(this.#devices.map((d) => d.applySession(session)));
|
|
5547
5764
|
}
|
|
5548
5765
|
};
|
|
5549
5766
|
|
|
@@ -6045,6 +6262,7 @@ export {
|
|
|
6045
6262
|
extractMowerConsumableValues,
|
|
6046
6263
|
getMowerCapabilities,
|
|
6047
6264
|
getVacuumCapabilities,
|
|
6265
|
+
isAuthRefusedError,
|
|
6048
6266
|
isDreameConsumableKey,
|
|
6049
6267
|
mowerConsumableIndex,
|
|
6050
6268
|
mowerFaultSeverity,
|