@enyo-energy/energy-app-sdk 0.0.113 → 0.0.115
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/implementations/retry/backoff.cjs +58 -0
- package/dist/cjs/implementations/retry/backoff.d.cts +19 -0
- package/dist/cjs/implementations/retry/retry-errors.cjs +28 -0
- package/dist/cjs/implementations/retry/retry-errors.d.cts +18 -0
- package/dist/cjs/implementations/retry/retry-manager.cjs +182 -0
- package/dist/cjs/implementations/retry/retry-manager.d.cts +97 -0
- package/dist/cjs/index.cjs +6 -0
- package/dist/cjs/index.d.cts +6 -0
- package/dist/cjs/packages/energy-app-onboarding.d.cts +17 -1
- package/dist/cjs/types/enyo-appliance.cjs +6 -3
- package/dist/cjs/types/enyo-appliance.d.cts +25 -7
- package/dist/cjs/types/enyo-authentication.d.cts +10 -0
- package/dist/cjs/types/enyo-charge.d.cts +5 -0
- package/dist/cjs/types/enyo-data-bus-value.d.cts +6 -4
- package/dist/cjs/types/enyo-onboarding.cjs +15 -1
- package/dist/cjs/types/enyo-onboarding.d.cts +19 -0
- package/dist/cjs/types/enyo-retry-manager.cjs +19 -0
- package/dist/cjs/types/enyo-retry-manager.d.cts +72 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/retry/backoff.d.ts +19 -0
- package/dist/implementations/retry/backoff.js +55 -0
- package/dist/implementations/retry/retry-errors.d.ts +18 -0
- package/dist/implementations/retry/retry-errors.js +24 -0
- package/dist/implementations/retry/retry-manager.d.ts +97 -0
- package/dist/implementations/retry/retry-manager.js +178 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/packages/energy-app-onboarding.d.ts +17 -1
- package/dist/types/enyo-appliance.d.ts +25 -7
- package/dist/types/enyo-appliance.js +6 -3
- package/dist/types/enyo-authentication.d.ts +10 -0
- package/dist/types/enyo-charge.d.ts +5 -0
- package/dist/types/enyo-data-bus-value.d.ts +6 -4
- package/dist/types/enyo-onboarding.d.ts +19 -0
- package/dist/types/enyo-onboarding.js +14 -0
- package/dist/types/enyo-retry-manager.d.ts +72 -0
- package/dist/types/enyo-retry-manager.js +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RetryState = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle state of a single retryable instance tracked by {@link RetryManager}.
|
|
6
|
+
*/
|
|
7
|
+
var RetryState;
|
|
8
|
+
(function (RetryState) {
|
|
9
|
+
/** Registered but no attempt has run yet (or the instance was reset). */
|
|
10
|
+
RetryState["Idle"] = "idle";
|
|
11
|
+
/** An attempt is currently executing. */
|
|
12
|
+
RetryState["Running"] = "running";
|
|
13
|
+
/** The previous attempt failed and the manager is sleeping before the next one. */
|
|
14
|
+
RetryState["Waiting"] = "waiting";
|
|
15
|
+
/** The most recent attempt succeeded. */
|
|
16
|
+
RetryState["Succeeded"] = "succeeded";
|
|
17
|
+
/** All attempts have been used (or the error was non-retryable). */
|
|
18
|
+
RetryState["Exhausted"] = "exhausted";
|
|
19
|
+
})(RetryState || (exports.RetryState = RetryState = {}));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle state of a single retryable instance tracked by {@link RetryManager}.
|
|
3
|
+
*/
|
|
4
|
+
export declare enum RetryState {
|
|
5
|
+
/** Registered but no attempt has run yet (or the instance was reset). */
|
|
6
|
+
Idle = "idle",
|
|
7
|
+
/** An attempt is currently executing. */
|
|
8
|
+
Running = "running",
|
|
9
|
+
/** The previous attempt failed and the manager is sleeping before the next one. */
|
|
10
|
+
Waiting = "waiting",
|
|
11
|
+
/** The most recent attempt succeeded. */
|
|
12
|
+
Succeeded = "succeeded",
|
|
13
|
+
/** All attempts have been used (or the error was non-retryable). */
|
|
14
|
+
Exhausted = "exhausted"
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Backoff curve used to compute the delay between retry attempts.
|
|
18
|
+
*
|
|
19
|
+
* The same shape covers fixed, linear and exponential strategies so callers
|
|
20
|
+
* configure backoff with plain data instead of constructing strategy classes.
|
|
21
|
+
*/
|
|
22
|
+
export interface BackoffConfig {
|
|
23
|
+
/** Backoff curve. `exponential` is the typical default for network I/O. */
|
|
24
|
+
type: 'fixed' | 'linear' | 'exponential';
|
|
25
|
+
/** Base delay for the first retry, in milliseconds. Must be `>= 0`. */
|
|
26
|
+
initialMs: number;
|
|
27
|
+
/** Optional upper bound (ms) applied after the curve and before jitter. */
|
|
28
|
+
maxMs?: number;
|
|
29
|
+
/** Growth factor for `exponential`. Defaults to `2`. Must be `>= 1`. */
|
|
30
|
+
factor?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Random jitter ratio in `[0, 1]`. `0.2` means the final delay is uniformly
|
|
33
|
+
* distributed in `[delay * 0.8, delay * 1.2]`. Defaults to `0`.
|
|
34
|
+
*/
|
|
35
|
+
jitter?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Per-instance retry policy. Registered with {@link RetryManager.register}.
|
|
39
|
+
*/
|
|
40
|
+
export interface RetryPolicy {
|
|
41
|
+
/** Total attempts allowed including the first call. Use `Infinity` for endless retry. */
|
|
42
|
+
maxAttempts: number;
|
|
43
|
+
/** Backoff curve applied between attempts. */
|
|
44
|
+
backoff: BackoffConfig;
|
|
45
|
+
/**
|
|
46
|
+
* Predicate consulted after each failure. Returning `false` short-circuits
|
|
47
|
+
* to {@link RetryState.Exhausted} without further retries (e.g. for auth or 4xx errors).
|
|
48
|
+
* Defaults to "always retryable".
|
|
49
|
+
*/
|
|
50
|
+
isRetryable?: (error: unknown) => boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Snapshot of a single instance's current retry state.
|
|
54
|
+
*/
|
|
55
|
+
export interface RetryStatus {
|
|
56
|
+
/** Identifier the instance was registered under. */
|
|
57
|
+
id: string;
|
|
58
|
+
/** Current lifecycle state. */
|
|
59
|
+
state: RetryState;
|
|
60
|
+
/** Number of attempts that have started so far in the current cycle. */
|
|
61
|
+
attempts: number;
|
|
62
|
+
/** Error from the most recent failed attempt, if any. */
|
|
63
|
+
lastError?: unknown;
|
|
64
|
+
/** Delay (ms) the manager is sleeping for, set only while `state === Waiting`. */
|
|
65
|
+
nextDelayMs?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Listener invoked on every state transition for any registered instance.
|
|
69
|
+
*
|
|
70
|
+
* @param status Snapshot of the instance immediately after the transition.
|
|
71
|
+
*/
|
|
72
|
+
export type RetryStateListener = (status: RetryStatus) => void;
|
package/dist/cjs/version.cjs
CHANGED
|
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
|
|
|
9
9
|
/**
|
|
10
10
|
* Current version of the enyo Energy App SDK.
|
|
11
11
|
*/
|
|
12
|
-
exports.SDK_VERSION = '0.0.
|
|
12
|
+
exports.SDK_VERSION = '0.0.115';
|
|
13
13
|
/**
|
|
14
14
|
* Gets the current SDK version.
|
|
15
15
|
* @returns The semantic version string of the SDK
|
package/dist/cjs/version.d.cts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BackoffConfig } from '../../types/enyo-retry-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Computes the delay (ms) the {@link RetryManager} should wait before retry attempt `attempt`.
|
|
4
|
+
*
|
|
5
|
+
* Pure function — no timers, no global state — so it can be called directly by
|
|
6
|
+
* callers that drive their own retry loops.
|
|
7
|
+
*
|
|
8
|
+
* @param config Backoff curve and bounds.
|
|
9
|
+
* @param attempt 1-based attempt number; `1` is the first retry (the call right after the first failure).
|
|
10
|
+
* @param rng Source of randomness for jitter, in `[0, 1)`. Injectable for deterministic tests.
|
|
11
|
+
* @returns Non-negative delay in milliseconds.
|
|
12
|
+
* @throws RangeError on invalid configuration or non-positive `attempt`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* computeBackoffDelay({ type: 'exponential', initialMs: 100, maxMs: 5000 }, 4); // 800
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeBackoffDelay(config: BackoffConfig, attempt: number, rng?: () => number): number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes the delay (ms) the {@link RetryManager} should wait before retry attempt `attempt`.
|
|
3
|
+
*
|
|
4
|
+
* Pure function — no timers, no global state — so it can be called directly by
|
|
5
|
+
* callers that drive their own retry loops.
|
|
6
|
+
*
|
|
7
|
+
* @param config Backoff curve and bounds.
|
|
8
|
+
* @param attempt 1-based attempt number; `1` is the first retry (the call right after the first failure).
|
|
9
|
+
* @param rng Source of randomness for jitter, in `[0, 1)`. Injectable for deterministic tests.
|
|
10
|
+
* @returns Non-negative delay in milliseconds.
|
|
11
|
+
* @throws RangeError on invalid configuration or non-positive `attempt`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* computeBackoffDelay({ type: 'exponential', initialMs: 100, maxMs: 5000 }, 4); // 800
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function computeBackoffDelay(config, attempt, rng = Math.random) {
|
|
19
|
+
if (!Number.isFinite(attempt) || attempt < 1) {
|
|
20
|
+
throw new RangeError(`attempt must be a positive integer, got ${attempt}`);
|
|
21
|
+
}
|
|
22
|
+
if (config.initialMs < 0) {
|
|
23
|
+
throw new RangeError(`initialMs must be >= 0, got ${config.initialMs}`);
|
|
24
|
+
}
|
|
25
|
+
if (config.maxMs !== undefined && config.maxMs < 0) {
|
|
26
|
+
throw new RangeError(`maxMs must be >= 0, got ${config.maxMs}`);
|
|
27
|
+
}
|
|
28
|
+
const factor = config.factor ?? 2;
|
|
29
|
+
if (config.type === 'exponential' && factor < 1) {
|
|
30
|
+
throw new RangeError(`factor must be >= 1, got ${factor}`);
|
|
31
|
+
}
|
|
32
|
+
const jitter = config.jitter ?? 0;
|
|
33
|
+
if (jitter < 0 || jitter > 1) {
|
|
34
|
+
throw new RangeError(`jitter must be in [0, 1], got ${jitter}`);
|
|
35
|
+
}
|
|
36
|
+
let delay;
|
|
37
|
+
switch (config.type) {
|
|
38
|
+
case 'fixed':
|
|
39
|
+
delay = config.initialMs;
|
|
40
|
+
break;
|
|
41
|
+
case 'linear':
|
|
42
|
+
delay = config.initialMs * attempt;
|
|
43
|
+
break;
|
|
44
|
+
case 'exponential':
|
|
45
|
+
delay = config.initialMs * factor ** (attempt - 1);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (config.maxMs !== undefined && delay > config.maxMs) {
|
|
49
|
+
delay = config.maxMs;
|
|
50
|
+
}
|
|
51
|
+
if (jitter > 0) {
|
|
52
|
+
delay = delay * (1 + (rng() * 2 - 1) * jitter);
|
|
53
|
+
}
|
|
54
|
+
return delay < 0 ? 0 : delay;
|
|
55
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link RetryManager.execute} when all attempts have been used or
|
|
3
|
+
* the policy's `isRetryable` predicate rejected the last error.
|
|
4
|
+
*
|
|
5
|
+
* The original failure is preserved on {@link RetryExhaustedError.cause} so
|
|
6
|
+
* callers can inspect or rethrow it.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RetryExhaustedError extends Error {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly attempts: number;
|
|
11
|
+
readonly cause: unknown;
|
|
12
|
+
/**
|
|
13
|
+
* @param id Identifier of the retry instance that gave up.
|
|
14
|
+
* @param attempts Number of attempts that ran before giving up.
|
|
15
|
+
* @param cause The error from the final failing attempt.
|
|
16
|
+
*/
|
|
17
|
+
constructor(id: string, attempts: number, cause: unknown);
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link RetryManager.execute} when all attempts have been used or
|
|
3
|
+
* the policy's `isRetryable` predicate rejected the last error.
|
|
4
|
+
*
|
|
5
|
+
* The original failure is preserved on {@link RetryExhaustedError.cause} so
|
|
6
|
+
* callers can inspect or rethrow it.
|
|
7
|
+
*/
|
|
8
|
+
export class RetryExhaustedError extends Error {
|
|
9
|
+
id;
|
|
10
|
+
attempts;
|
|
11
|
+
cause;
|
|
12
|
+
/**
|
|
13
|
+
* @param id Identifier of the retry instance that gave up.
|
|
14
|
+
* @param attempts Number of attempts that ran before giving up.
|
|
15
|
+
* @param cause The error from the final failing attempt.
|
|
16
|
+
*/
|
|
17
|
+
constructor(id, attempts, cause) {
|
|
18
|
+
super(`Retry exhausted for "${id}" after ${attempts} attempt(s)`);
|
|
19
|
+
this.id = id;
|
|
20
|
+
this.attempts = attempts;
|
|
21
|
+
this.cause = cause;
|
|
22
|
+
this.name = 'RetryExhaustedError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type RetryPolicy, type RetryStateListener, type RetryStatus } from '../../types/enyo-retry-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options controlling how a {@link RetryManager} interacts with the outside world.
|
|
4
|
+
*
|
|
5
|
+
* Both fields exist purely so unit tests can inject deterministic substitutes;
|
|
6
|
+
* production code should use the defaults.
|
|
7
|
+
*/
|
|
8
|
+
export interface RetryManagerOptions {
|
|
9
|
+
/** Replacement for `setTimeout`-based sleeping. Defaults to a `setTimeout` wrapper. */
|
|
10
|
+
sleep?: (ms: number) => Promise<void>;
|
|
11
|
+
/** Source of randomness for backoff jitter. Defaults to `Math.random`. */
|
|
12
|
+
rng?: () => number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Protocol-agnostic retry coordinator.
|
|
16
|
+
*
|
|
17
|
+
* Each *instance* (a Modbus connection, an MQTT client, a REST endpoint, …) is
|
|
18
|
+
* registered under a string `id` together with its own {@link RetryPolicy}.
|
|
19
|
+
* Callers then run an operation through {@link RetryManager.execute}; the
|
|
20
|
+
* manager handles attempt counting, backoff, and state transitions and reports
|
|
21
|
+
* progress through {@link RetryManager.onStateChange}.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const retries = new RetryManager();
|
|
26
|
+
* retries.register('modbus-inverter-1', {
|
|
27
|
+
* maxAttempts: 5,
|
|
28
|
+
* backoff: { type: 'exponential', initialMs: 200, maxMs: 5000, jitter: 0.2 },
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* await retries.execute('modbus-inverter-1', () => client.connect());
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class RetryManager {
|
|
35
|
+
private readonly instances;
|
|
36
|
+
private readonly listeners;
|
|
37
|
+
private readonly sleep;
|
|
38
|
+
private readonly rng;
|
|
39
|
+
/**
|
|
40
|
+
* @param options Test hooks for sleep and randomness. Production callers can omit this argument.
|
|
41
|
+
*/
|
|
42
|
+
constructor(options?: RetryManagerOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Registers a new retry instance.
|
|
45
|
+
*
|
|
46
|
+
* @param id Identifier used for all subsequent operations and reported in {@link RetryStatus}.
|
|
47
|
+
* @param policy Retry policy applied to this instance.
|
|
48
|
+
* @throws Error if `id` is already registered.
|
|
49
|
+
*/
|
|
50
|
+
register(id: string, policy: RetryPolicy): void;
|
|
51
|
+
/**
|
|
52
|
+
* Removes the instance from the manager. No-op if `id` is not registered.
|
|
53
|
+
*/
|
|
54
|
+
unregister(id: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Returns whether `id` is registered.
|
|
57
|
+
*/
|
|
58
|
+
has(id: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Returns a snapshot of `id`'s current state, or `undefined` if not registered.
|
|
61
|
+
*/
|
|
62
|
+
status(id: string): RetryStatus | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Returns snapshots for every registered instance.
|
|
65
|
+
*/
|
|
66
|
+
statuses(): RetryStatus[];
|
|
67
|
+
/**
|
|
68
|
+
* Resets `id` to {@link RetryState.Idle} with zero attempts. The policy is preserved.
|
|
69
|
+
*
|
|
70
|
+
* @throws Error if `id` is not registered.
|
|
71
|
+
*/
|
|
72
|
+
reset(id: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Subscribes to state-change events for every registered instance.
|
|
75
|
+
*
|
|
76
|
+
* @param listener Called once per transition with the post-transition snapshot.
|
|
77
|
+
* @returns Unsubscribe function.
|
|
78
|
+
*/
|
|
79
|
+
onStateChange(listener: RetryStateListener): () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Runs `fn` under the policy registered for `id`.
|
|
82
|
+
*
|
|
83
|
+
* Each invocation starts a fresh attempt cycle: counters and `lastError` are
|
|
84
|
+
* cleared before the first attempt so the same instance can be re-used after
|
|
85
|
+
* a previous success or exhaustion.
|
|
86
|
+
*
|
|
87
|
+
* @param id Identifier registered via {@link RetryManager.register}.
|
|
88
|
+
* @param fn Operation to execute. Rejecting causes a retry (subject to the policy).
|
|
89
|
+
* @returns Whatever `fn` resolves to.
|
|
90
|
+
* @throws RetryExhaustedError when all attempts have been used or `isRetryable` returns `false`.
|
|
91
|
+
* @throws Error when `id` is not registered.
|
|
92
|
+
*/
|
|
93
|
+
execute<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
94
|
+
private requireInstance;
|
|
95
|
+
private transition;
|
|
96
|
+
private emit;
|
|
97
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { RetryState, } from '../../types/enyo-retry-manager.js';
|
|
2
|
+
import { computeBackoffDelay } from './backoff.js';
|
|
3
|
+
import { RetryExhaustedError } from './retry-errors.js';
|
|
4
|
+
const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
5
|
+
/**
|
|
6
|
+
* Protocol-agnostic retry coordinator.
|
|
7
|
+
*
|
|
8
|
+
* Each *instance* (a Modbus connection, an MQTT client, a REST endpoint, …) is
|
|
9
|
+
* registered under a string `id` together with its own {@link RetryPolicy}.
|
|
10
|
+
* Callers then run an operation through {@link RetryManager.execute}; the
|
|
11
|
+
* manager handles attempt counting, backoff, and state transitions and reports
|
|
12
|
+
* progress through {@link RetryManager.onStateChange}.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const retries = new RetryManager();
|
|
17
|
+
* retries.register('modbus-inverter-1', {
|
|
18
|
+
* maxAttempts: 5,
|
|
19
|
+
* backoff: { type: 'exponential', initialMs: 200, maxMs: 5000, jitter: 0.2 },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* await retries.execute('modbus-inverter-1', () => client.connect());
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class RetryManager {
|
|
26
|
+
instances = new Map();
|
|
27
|
+
listeners = new Set();
|
|
28
|
+
sleep;
|
|
29
|
+
rng;
|
|
30
|
+
/**
|
|
31
|
+
* @param options Test hooks for sleep and randomness. Production callers can omit this argument.
|
|
32
|
+
*/
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.sleep = options.sleep ?? defaultSleep;
|
|
35
|
+
this.rng = options.rng ?? Math.random;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Registers a new retry instance.
|
|
39
|
+
*
|
|
40
|
+
* @param id Identifier used for all subsequent operations and reported in {@link RetryStatus}.
|
|
41
|
+
* @param policy Retry policy applied to this instance.
|
|
42
|
+
* @throws Error if `id` is already registered.
|
|
43
|
+
*/
|
|
44
|
+
register(id, policy) {
|
|
45
|
+
if (this.instances.has(id)) {
|
|
46
|
+
throw new Error(`Retry instance "${id}" is already registered`);
|
|
47
|
+
}
|
|
48
|
+
if (policy.maxAttempts < 1) {
|
|
49
|
+
throw new RangeError(`maxAttempts must be >= 1, got ${policy.maxAttempts}`);
|
|
50
|
+
}
|
|
51
|
+
this.instances.set(id, { id, policy, state: RetryState.Idle, attempts: 0 });
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Removes the instance from the manager. No-op if `id` is not registered.
|
|
55
|
+
*/
|
|
56
|
+
unregister(id) {
|
|
57
|
+
this.instances.delete(id);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns whether `id` is registered.
|
|
61
|
+
*/
|
|
62
|
+
has(id) {
|
|
63
|
+
return this.instances.has(id);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns a snapshot of `id`'s current state, or `undefined` if not registered.
|
|
67
|
+
*/
|
|
68
|
+
status(id) {
|
|
69
|
+
const instance = this.instances.get(id);
|
|
70
|
+
return instance ? toStatus(instance) : undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Returns snapshots for every registered instance.
|
|
74
|
+
*/
|
|
75
|
+
statuses() {
|
|
76
|
+
return Array.from(this.instances.values(), toStatus);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resets `id` to {@link RetryState.Idle} with zero attempts. The policy is preserved.
|
|
80
|
+
*
|
|
81
|
+
* @throws Error if `id` is not registered.
|
|
82
|
+
*/
|
|
83
|
+
reset(id) {
|
|
84
|
+
const instance = this.requireInstance(id);
|
|
85
|
+
instance.state = RetryState.Idle;
|
|
86
|
+
instance.attempts = 0;
|
|
87
|
+
instance.lastError = undefined;
|
|
88
|
+
instance.nextDelayMs = undefined;
|
|
89
|
+
this.emit(instance);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Subscribes to state-change events for every registered instance.
|
|
93
|
+
*
|
|
94
|
+
* @param listener Called once per transition with the post-transition snapshot.
|
|
95
|
+
* @returns Unsubscribe function.
|
|
96
|
+
*/
|
|
97
|
+
onStateChange(listener) {
|
|
98
|
+
this.listeners.add(listener);
|
|
99
|
+
return () => {
|
|
100
|
+
this.listeners.delete(listener);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Runs `fn` under the policy registered for `id`.
|
|
105
|
+
*
|
|
106
|
+
* Each invocation starts a fresh attempt cycle: counters and `lastError` are
|
|
107
|
+
* cleared before the first attempt so the same instance can be re-used after
|
|
108
|
+
* a previous success or exhaustion.
|
|
109
|
+
*
|
|
110
|
+
* @param id Identifier registered via {@link RetryManager.register}.
|
|
111
|
+
* @param fn Operation to execute. Rejecting causes a retry (subject to the policy).
|
|
112
|
+
* @returns Whatever `fn` resolves to.
|
|
113
|
+
* @throws RetryExhaustedError when all attempts have been used or `isRetryable` returns `false`.
|
|
114
|
+
* @throws Error when `id` is not registered.
|
|
115
|
+
*/
|
|
116
|
+
async execute(id, fn) {
|
|
117
|
+
const instance = this.requireInstance(id);
|
|
118
|
+
instance.attempts = 0;
|
|
119
|
+
instance.lastError = undefined;
|
|
120
|
+
instance.nextDelayMs = undefined;
|
|
121
|
+
const { maxAttempts, backoff, isRetryable } = instance.policy;
|
|
122
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
123
|
+
instance.attempts = attempt;
|
|
124
|
+
instance.nextDelayMs = undefined;
|
|
125
|
+
this.transition(instance, RetryState.Running);
|
|
126
|
+
try {
|
|
127
|
+
const result = await fn();
|
|
128
|
+
instance.lastError = undefined;
|
|
129
|
+
this.transition(instance, RetryState.Succeeded);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
instance.lastError = error;
|
|
134
|
+
const retryable = isRetryable ? isRetryable(error) : true;
|
|
135
|
+
const hasMore = attempt < maxAttempts;
|
|
136
|
+
if (!retryable || !hasMore) {
|
|
137
|
+
this.transition(instance, RetryState.Exhausted);
|
|
138
|
+
throw new RetryExhaustedError(id, attempt, error);
|
|
139
|
+
}
|
|
140
|
+
const delay = computeBackoffDelay(backoff, attempt, this.rng);
|
|
141
|
+
instance.nextDelayMs = delay;
|
|
142
|
+
this.transition(instance, RetryState.Waiting);
|
|
143
|
+
await this.sleep(delay);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Unreachable: the loop either returns, throws, or completes by entering
|
|
147
|
+
// the catch branch on the final attempt (which throws Exhausted above).
|
|
148
|
+
throw new RetryExhaustedError(id, instance.attempts, instance.lastError);
|
|
149
|
+
}
|
|
150
|
+
requireInstance(id) {
|
|
151
|
+
const instance = this.instances.get(id);
|
|
152
|
+
if (!instance) {
|
|
153
|
+
throw new Error(`Retry instance "${id}" is not registered`);
|
|
154
|
+
}
|
|
155
|
+
return instance;
|
|
156
|
+
}
|
|
157
|
+
transition(instance, state) {
|
|
158
|
+
instance.state = state;
|
|
159
|
+
this.emit(instance);
|
|
160
|
+
}
|
|
161
|
+
emit(instance) {
|
|
162
|
+
if (this.listeners.size === 0)
|
|
163
|
+
return;
|
|
164
|
+
const snapshot = toStatus(instance);
|
|
165
|
+
for (const listener of this.listeners) {
|
|
166
|
+
listener(snapshot);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function toStatus(instance) {
|
|
171
|
+
return {
|
|
172
|
+
id: instance.id,
|
|
173
|
+
state: instance.state,
|
|
174
|
+
attempts: instance.attempts,
|
|
175
|
+
lastError: instance.lastError,
|
|
176
|
+
nextDelayMs: instance.nextDelayMs,
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export * from './packages/energy-app-diagnostics.js';
|
|
|
40
40
|
export * from './types/enyo-learning-phase.js';
|
|
41
41
|
export * from './packages/energy-app-learning-phase.js';
|
|
42
42
|
export * from './types/enyo-air-conditioning-appliance.js';
|
|
43
|
+
export * from './types/enyo-onboarding.js';
|
|
44
|
+
export * from './packages/energy-app-onboarding.js';
|
|
45
|
+
export * from './types/enyo-retry-manager.js';
|
|
46
|
+
export * from './implementations/retry/backoff.js';
|
|
47
|
+
export * from './implementations/retry/retry-manager.js';
|
|
48
|
+
export * from './implementations/retry/retry-errors.js';
|
|
43
49
|
export * from './integrations/integration-types.js';
|
|
44
50
|
export * from './integrations/integration-energy-app.js';
|
|
45
51
|
export * from './integrations/heatpump-integration-energy-app.js';
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,12 @@ export * from './packages/energy-app-diagnostics.js';
|
|
|
40
40
|
export * from './types/enyo-learning-phase.js';
|
|
41
41
|
export * from './packages/energy-app-learning-phase.js';
|
|
42
42
|
export * from './types/enyo-air-conditioning-appliance.js';
|
|
43
|
+
export * from './types/enyo-onboarding.js';
|
|
44
|
+
export * from './packages/energy-app-onboarding.js';
|
|
45
|
+
export * from './types/enyo-retry-manager.js';
|
|
46
|
+
export * from './implementations/retry/backoff.js';
|
|
47
|
+
export * from './implementations/retry/retry-manager.js';
|
|
48
|
+
export * from './implementations/retry/retry-errors.js';
|
|
43
49
|
export * from './integrations/integration-types.js';
|
|
44
50
|
export * from './integrations/integration-energy-app.js';
|
|
45
51
|
export * from './integrations/heatpump-integration-energy-app.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EnyoOnboardingGuide, EnyoOnboardingStep, EnyoOnboardingStepListener, EnyoOnboardingStepResponse } from "../types/enyo-onboarding.js";
|
|
1
|
+
import { EnyoOnboardingGuide, EnyoOnboardingGuideCategory, EnyoOnboardingStep, EnyoOnboardingStepListener, EnyoOnboardingStepResponse } from "../types/enyo-onboarding.js";
|
|
2
2
|
/**
|
|
3
3
|
* Interface for managing onboarding guides within Energy App packages.
|
|
4
4
|
* Provides methods to create, manage, and navigate through onboarding flows
|
|
@@ -40,6 +40,22 @@ export interface EnergyAppOnboarding {
|
|
|
40
40
|
* @returns Promise that resolves to an array of all active onboarding guides
|
|
41
41
|
*/
|
|
42
42
|
getAllOnboardingGuides(): Promise<EnyoOnboardingGuide[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Gets all currently active onboarding guides that belong to the given category.
|
|
45
|
+
* Guides without an explicit category are excluded — use {@link getAllOnboardingGuides}
|
|
46
|
+
* if you need uncategorized guides as well.
|
|
47
|
+
*
|
|
48
|
+
* @param category - The lifecycle category to filter guides by
|
|
49
|
+
* @returns Promise resolving to all guides whose `category` matches
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const reconnectGuides = await onboarding.getGuidesByCategory(
|
|
54
|
+
* EnyoOnboardingGuideCategory.ReconnectDevice
|
|
55
|
+
* );
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
getGuidesByCategory(category: EnyoOnboardingGuideCategory): Promise<EnyoOnboardingGuide[]>;
|
|
43
59
|
/**
|
|
44
60
|
* Gets the current step being displayed in the onboarding flow for a specific guide.
|
|
45
61
|
* Returns null if no onboarding is active or if the guide is complete.
|
|
@@ -28,16 +28,26 @@ export declare enum EnyoApplianceStateEnum {
|
|
|
28
28
|
/**
|
|
29
29
|
* Health status of an appliance. Orthogonal to {@link EnyoApplianceStateEnum},
|
|
30
30
|
* which describes connectivity. `Healthy` means the appliance is operating
|
|
31
|
-
* normally; `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* normally; `Warning` means a non-blocking issue has been reported (the
|
|
32
|
+
* appliance is still functional but should be inspected); `Faulted` means it
|
|
33
|
+
* has reported an internal error and may need attention. Vendor- or
|
|
34
|
+
* protocol-specific details should be conveyed via accompanying error codes.
|
|
34
35
|
*/
|
|
35
36
|
export declare enum EnyoApplianceStatusEnum {
|
|
36
37
|
/** Appliance is operating normally */
|
|
37
38
|
Healthy = "healthy",
|
|
39
|
+
/** Appliance is operating but has reported a non-blocking issue that should be inspected */
|
|
40
|
+
Warning = "warning",
|
|
38
41
|
/** Appliance has reported an internal fault */
|
|
39
42
|
Faulted = "faulted"
|
|
40
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Severity classification for an {@link EnyoApplianceErrorCode}.
|
|
46
|
+
* Producers should mark non-blocking issues as `'warning'` and blocking
|
|
47
|
+
* faults as `'error'`. When omitted on an error code, consumers should
|
|
48
|
+
* treat it as `'error'` for backwards compatibility.
|
|
49
|
+
*/
|
|
50
|
+
export type EnyoApplianceErrorSeverity = 'error' | 'warning';
|
|
41
51
|
/**
|
|
42
52
|
* Translated, human-readable message for an appliance error code.
|
|
43
53
|
* Producers should emit at most one entry per supported language.
|
|
@@ -49,18 +59,26 @@ export interface EnyoApplianceErrorMessage {
|
|
|
49
59
|
message: string;
|
|
50
60
|
}
|
|
51
61
|
/**
|
|
52
|
-
* Vendor- or protocol-specific error reported by an appliance,
|
|
53
|
-
* accompanied by translated human-readable messages. The `code` is
|
|
54
|
-
* machine-readable identifier (stable, non-localized); `messages` is an
|
|
62
|
+
* Vendor- or protocol-specific error or warning reported by an appliance,
|
|
63
|
+
* optionally accompanied by translated human-readable messages. The `code` is
|
|
64
|
+
* the machine-readable identifier (stable, non-localized); `messages` is an
|
|
55
65
|
* optional set of pre-translated descriptions intended for UI display.
|
|
56
66
|
* Consumers should fall back to rendering `code` when no `messages` entry
|
|
57
|
-
* matches their locale.
|
|
67
|
+
* matches their locale. Use `severity` to distinguish a blocking error from
|
|
68
|
+
* a non-blocking warning; when omitted, consumers should treat the entry as
|
|
69
|
+
* an error for backwards compatibility.
|
|
58
70
|
*/
|
|
59
71
|
export interface EnyoApplianceErrorCode {
|
|
60
72
|
/** Machine-readable, vendor- or protocol-specific error code */
|
|
61
73
|
code: string;
|
|
62
74
|
/** Optional translated messages explaining the error */
|
|
63
75
|
messages?: EnyoApplianceErrorMessage[];
|
|
76
|
+
/**
|
|
77
|
+
* Optional severity of this entry. Defaults to `'error'` semantics when
|
|
78
|
+
* omitted. Use `'warning'` to indicate a non-blocking issue that should
|
|
79
|
+
* be surfaced but does not put the appliance into a faulted state.
|
|
80
|
+
*/
|
|
81
|
+
severity?: EnyoApplianceErrorSeverity;
|
|
64
82
|
}
|
|
65
83
|
export interface EnyoApplianceNetworkMetadata {
|
|
66
84
|
/** If the appliance is connected via cellular network, you can put the imsi here*/
|
|
@@ -18,14 +18,17 @@ export var EnyoApplianceStateEnum;
|
|
|
18
18
|
/**
|
|
19
19
|
* Health status of an appliance. Orthogonal to {@link EnyoApplianceStateEnum},
|
|
20
20
|
* which describes connectivity. `Healthy` means the appliance is operating
|
|
21
|
-
* normally; `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* normally; `Warning` means a non-blocking issue has been reported (the
|
|
22
|
+
* appliance is still functional but should be inspected); `Faulted` means it
|
|
23
|
+
* has reported an internal error and may need attention. Vendor- or
|
|
24
|
+
* protocol-specific details should be conveyed via accompanying error codes.
|
|
24
25
|
*/
|
|
25
26
|
export var EnyoApplianceStatusEnum;
|
|
26
27
|
(function (EnyoApplianceStatusEnum) {
|
|
27
28
|
/** Appliance is operating normally */
|
|
28
29
|
EnyoApplianceStatusEnum["Healthy"] = "healthy";
|
|
30
|
+
/** Appliance is operating but has reported a non-blocking issue that should be inspected */
|
|
31
|
+
EnyoApplianceStatusEnum["Warning"] = "warning";
|
|
29
32
|
/** Appliance has reported an internal fault */
|
|
30
33
|
EnyoApplianceStatusEnum["Faulted"] = "faulted";
|
|
31
34
|
})(EnyoApplianceStatusEnum || (EnyoApplianceStatusEnum = {}));
|