tesla-fleet-api 0.4.0 → 0.6.0
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/README.md +269 -1
- package/dist/_virtual/_rolldown/runtime.cjs +10 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +96 -0
- package/dist/bluetooth/constants.d.cts +68 -0
- package/dist/bluetooth/constants.d.mts +68 -0
- package/dist/bluetooth/constants.mjs +70 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +165 -0
- package/dist/bluetooth/homey.d.cts +84 -0
- package/dist/bluetooth/homey.d.mts +84 -0
- package/dist/bluetooth/homey.mjs +159 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +97 -0
- package/dist/bluetooth/transport.d.mts +97 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +52 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/commands.cjs +1080 -85
- package/dist/commands.d.cts +585 -54
- package/dist/commands.d.mts +583 -51
- package/dist/commands.mjs +1084 -93
- package/dist/const.cjs +22 -0
- package/dist/const.d.cts +21 -0
- package/dist/const.d.mts +21 -0
- package/dist/const.mjs +20 -1
- package/dist/energy.cjs +24 -19
- package/dist/energy.d.cts +21 -15
- package/dist/energy.d.mts +21 -15
- package/dist/energy.mjs +24 -19
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +375 -17
- package/dist/energyspecific.d.cts +199 -19
- package/dist/energyspecific.d.mts +197 -16
- package/dist/energyspecific.mjs +370 -18
- package/dist/exceptions.cjs +3 -2
- package/dist/exceptions.d.cts +3 -2
- package/dist/exceptions.d.mts +3 -2
- package/dist/exceptions.mjs +3 -2
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +22 -8
- package/dist/index.d.mts +22 -8
- package/dist/index.mjs +19 -6
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -1
- package/dist/router.d.mts +3 -1
- package/dist/router.mjs +3 -1
- package/dist/signing/crypto.cjs +28 -0
- package/dist/signing/crypto.mjs +26 -1
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +104 -30
- package/dist/signing/session.d.cts +60 -23
- package/dist/signing/session.d.mts +60 -23
- package/dist/signing/session.mjs +105 -31
- package/dist/tariff.cjs +27 -0
- package/dist/tariff.d.cts +11 -0
- package/dist/tariff.d.mts +11 -0
- package/dist/tariff.mjs +27 -1
- package/dist/teslafleetapi.cjs +256 -55
- package/dist/teslafleetapi.d.cts +110 -14
- package/dist/teslafleetapi.d.mts +110 -14
- package/dist/teslafleetapi.mjs +258 -57
- package/dist/teslemetry.cjs +152 -5
- package/dist/teslemetry.d.cts +95 -5
- package/dist/teslemetry.d.mts +93 -3
- package/dist/teslemetry.mjs +151 -6
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +42 -5
- package/dist/tessie.d.cts +51 -5
- package/dist/tessie.d.mts +49 -3
- package/dist/tessie.mjs +42 -5
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/live_status.d.cts +16 -16
- package/dist/types/live_status.d.mts +16 -16
- package/dist/types/responses.d.cts +5 -1
- package/dist/types/responses.d.mts +5 -1
- package/dist/types/vehicle.d.cts +7 -1
- package/dist/types/vehicle.d.mts +7 -1
- package/dist/types.d.cts +3 -3
- package/dist/types.d.mts +3 -3
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +79 -36
- package/dist/vehicle.d.cts +42 -26
- package/dist/vehicle.d.mts +42 -26
- package/dist/vehicle.mjs +80 -37
- package/dist/vehiclesigned.cjs +20 -5
- package/dist/vehiclesigned.d.cts +14 -5
- package/dist/vehiclesigned.d.mts +14 -5
- package/dist/vehiclesigned.mjs +20 -5
- package/dist/vehiclespecific.cjs +126 -15
- package/dist/vehiclespecific.d.cts +82 -18
- package/dist/vehiclespecific.d.mts +82 -18
- package/dist/vehiclespecific.mjs +126 -15
- package/package.json +56 -3
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const require_exceptions = require("../exceptions.cjs");
|
|
2
|
+
// src/router/base.ts
|
|
3
|
+
/**
|
|
4
|
+
* Failures in one backend's own path, before the device judged the command, so
|
|
5
|
+
* the next backend (a different path to the same device) may still succeed.
|
|
6
|
+
* Anything else, typically the device answering and refusing, propagates: the
|
|
7
|
+
* next backend reaches the same device and would get the same answer.
|
|
8
|
+
*/
|
|
9
|
+
const FAILOVER_SAFE_ERRORS = [
|
|
10
|
+
require_exceptions.TeslaFleetConnectionError,
|
|
11
|
+
require_exceptions.BluetoothTransportError,
|
|
12
|
+
require_exceptions.BluetoothTimeout,
|
|
13
|
+
require_exceptions.BluetoothCommandFailed,
|
|
14
|
+
require_exceptions.BluetoothCloudUnavailableError,
|
|
15
|
+
require_exceptions.VehicleOffline,
|
|
16
|
+
require_exceptions.RateLimited,
|
|
17
|
+
require_exceptions.InternalServerError,
|
|
18
|
+
require_exceptions.BadGateway,
|
|
19
|
+
require_exceptions.ServiceUnavailable,
|
|
20
|
+
require_exceptions.GatewayTimeout,
|
|
21
|
+
...require_exceptions.KEY_REJECTED_FAULTS,
|
|
22
|
+
require_exceptions.SessionInfoAuthenticationFault,
|
|
23
|
+
require_exceptions.SigningDisabled,
|
|
24
|
+
require_exceptions.SignedCommandRequired,
|
|
25
|
+
require_exceptions.TeslaFleetMessageFaultFieldExceedsMTU,
|
|
26
|
+
require_exceptions.TeslaFleetMessageFaultResponseSizeExceedsMTU,
|
|
27
|
+
require_exceptions.InvalidToken,
|
|
28
|
+
require_exceptions.MissingToken,
|
|
29
|
+
require_exceptions.OAuthExpired,
|
|
30
|
+
require_exceptions.OAuthInvalid,
|
|
31
|
+
require_exceptions.LoginRequired,
|
|
32
|
+
require_exceptions.PaymentRequired,
|
|
33
|
+
require_exceptions.SubscriptionRequired,
|
|
34
|
+
require_exceptions.VehicleSubscriptionRequired,
|
|
35
|
+
require_exceptions.InsufficientCredits,
|
|
36
|
+
require_exceptions.Forbidden,
|
|
37
|
+
require_exceptions.InvalidScope
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Whether a command that threw `error` may already have executed. The field
|
|
41
|
+
* check is the one that matters across the dual-package hazard: a process that
|
|
42
|
+
* loads this package through both `import` and `require` has two copies of the
|
|
43
|
+
* class, and `instanceof` fails across them.
|
|
44
|
+
*/
|
|
45
|
+
function is_unconfirmed(error) {
|
|
46
|
+
return error instanceof require_exceptions.BluetoothUnconfirmedCommand || error?.unconfirmed === true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a call that threw `error` may be retried on the next backend. Accepts
|
|
50
|
+
* any thrown value. A boolean `failover_safe` on the value decides for itself,
|
|
51
|
+
* in both directions, which lets a backend the router cannot import mark its own
|
|
52
|
+
* errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
|
|
53
|
+
*
|
|
54
|
+
* An unconfirmed command is never safe, whatever it is marked. Python's
|
|
55
|
+
* `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
|
|
56
|
+
* relies on the router checking first; here the predicate is safe to use alone.
|
|
57
|
+
*/
|
|
58
|
+
function is_failover_safe(error) {
|
|
59
|
+
if (is_unconfirmed(error)) return false;
|
|
60
|
+
const marker = error?.failover_safe;
|
|
61
|
+
if (typeof marker === "boolean") return marker;
|
|
62
|
+
return FAILOVER_SAFE_ERRORS.some((cls) => error instanceof cls);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The names `backend` routes: plain function properties, own or inherited, up
|
|
66
|
+
* to `Object.prototype`. The most-derived property of a name decides, so a
|
|
67
|
+
* subclass that shadows a method with a getter or a value stops it routing.
|
|
68
|
+
* Getters are never invoked, and symbol keys (`Symbol.asyncDispose`) are not
|
|
69
|
+
* seen, so a context manager is not proxied.
|
|
70
|
+
*/
|
|
71
|
+
function methodNames(backend) {
|
|
72
|
+
const names = /* @__PURE__ */ new Set();
|
|
73
|
+
const seen = /* @__PURE__ */ new Set();
|
|
74
|
+
for (let target = backend; target !== null && target !== Object.prototype; target = Object.getPrototypeOf(target)) for (const name of Object.getOwnPropertyNames(target)) {
|
|
75
|
+
if (seen.has(name)) continue;
|
|
76
|
+
seen.add(name);
|
|
77
|
+
if (name === "constructor" || name === "then" || name.startsWith("_")) continue;
|
|
78
|
+
if (typeof Object.getOwnPropertyDescriptor(target, name)?.value === "function") names.add(name);
|
|
79
|
+
}
|
|
80
|
+
return names;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A `TeslaFleetError`'s message is its class's, so it is safe to log. Anything
|
|
84
|
+
* else may hold a secret, so only its type is logged (see `src/log.ts`).
|
|
85
|
+
*/
|
|
86
|
+
function errorText(error) {
|
|
87
|
+
if (error instanceof require_exceptions.TeslaFleetError) return `${error.name}: ${error.message}`;
|
|
88
|
+
return error instanceof Error ? error.name : typeof error;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Routes calls across an ordered list of backends, trying each in order.
|
|
92
|
+
*
|
|
93
|
+
* Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
|
|
94
|
+
* methods. Methods are derived from the backends' prototypes at construction, so
|
|
95
|
+
* a new backend method is routed with no change here. A name on no backend does
|
|
96
|
+
* not exist. A backend without the method is skipped. A method added to a backend
|
|
97
|
+
* after construction is not routed.
|
|
98
|
+
*
|
|
99
|
+
* Each call is attempted on the backends that have the method. If one throws a
|
|
100
|
+
* value `is_failover_safe` accepts, the same call is retried on the next with
|
|
101
|
+
* the same arguments. Anything else propagates at once. When every backend fails
|
|
102
|
+
* over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
|
|
103
|
+
*
|
|
104
|
+
* **Warning:** a failed call is replayed on the next backend, so a non-idempotent
|
|
105
|
+
* command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
|
|
106
|
+
* mid-flight, after a backend may have partly applied it, can be double-executed,
|
|
107
|
+
* or executed more than once across a longer chain. This is the accepted tradeoff
|
|
108
|
+
* of per-command failover. Callers that need exactly-once semantics gate dispatch
|
|
109
|
+
* with a health check or call a backend directly.
|
|
110
|
+
*
|
|
111
|
+
* `BluetoothUnconfirmedCommand` is the exception. The command may already have
|
|
112
|
+
* executed, so it propagates at once on any backend in the chain, is never
|
|
113
|
+
* replayed, and never reaches `on_result`.
|
|
114
|
+
*
|
|
115
|
+
* The health check gates the first backend only: when it is false the primary is
|
|
116
|
+
* skipped and the call routes to the rest. There is deliberately no per-backend
|
|
117
|
+
* health matrix.
|
|
118
|
+
*
|
|
119
|
+
* Plain attributes are not routed; read them from `primary` or `backends`. A
|
|
120
|
+
* backend's connection lifecycle is not proxied either.
|
|
121
|
+
*
|
|
122
|
+
* TS `private` methods exist at run time, so they are installed too: the types
|
|
123
|
+
* hide them. `#private` members are unreachable, which keeps a signing key off
|
|
124
|
+
* the router as it is off the backend.
|
|
125
|
+
*/
|
|
126
|
+
var Router = class {
|
|
127
|
+
backends;
|
|
128
|
+
#health;
|
|
129
|
+
#on_result;
|
|
130
|
+
#logger;
|
|
131
|
+
constructor(backends, options = {}) {
|
|
132
|
+
if (backends.length < 2) throw new TypeError("A router needs a primary and a secondary backend.");
|
|
133
|
+
this.backends = Object.freeze([...backends]);
|
|
134
|
+
this.#health = options.health;
|
|
135
|
+
this.#on_result = options.on_result;
|
|
136
|
+
this.#logger = options.logger;
|
|
137
|
+
const targets = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const backend of backends) for (const name of methodNames(backend)) targets.set(name, [...targets.get(name) ?? [], backend]);
|
|
139
|
+
for (const [name, having] of targets) {
|
|
140
|
+
if (name in this) continue;
|
|
141
|
+
Object.defineProperty(this, name, { value: (...args) => this.#dispatch(name, having, args) });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** The first backend, tried first when healthy. */
|
|
145
|
+
get primary() {
|
|
146
|
+
return this.backends[0];
|
|
147
|
+
}
|
|
148
|
+
/** The second backend, tried when the primary is unhealthy or fails. */
|
|
149
|
+
get secondary() {
|
|
150
|
+
return this.backends[1];
|
|
151
|
+
}
|
|
152
|
+
/** Resolve the health check. With none configured this reports `true`. */
|
|
153
|
+
async is_healthy() {
|
|
154
|
+
const health = this.#health;
|
|
155
|
+
if (health == null) return true;
|
|
156
|
+
if (typeof health === "boolean") return health;
|
|
157
|
+
return Boolean(await health());
|
|
158
|
+
}
|
|
159
|
+
/** The `key=value` log field naming a backend. */
|
|
160
|
+
_backend_field(backend) {
|
|
161
|
+
return `backend=${backend.constructor?.name ?? "Object"}`;
|
|
162
|
+
}
|
|
163
|
+
/** `name` on the first backend that has it, for a plain attribute getter. */
|
|
164
|
+
_first_attribute(name) {
|
|
165
|
+
return this.backends.find((candidate) => name in candidate)?.[name];
|
|
166
|
+
}
|
|
167
|
+
async #dispatch(name, targets, args) {
|
|
168
|
+
let start = 0;
|
|
169
|
+
if (this.#health != null && targets.length > 1 && targets[0] === this.backends[0] && !await this.is_healthy()) start = 1;
|
|
170
|
+
let last;
|
|
171
|
+
for (const backend of targets.slice(start)) {
|
|
172
|
+
const field = this._backend_field(backend);
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await backend[name](...args);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (is_unconfirmed(error)) {
|
|
178
|
+
this.#logger?.debug(`command=${name} ${field} result=unconfirmed error=${errorText(error)}`);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
last = error;
|
|
182
|
+
this.#logger?.debug(`command=${name} ${field} result=error error=${errorText(error)}`);
|
|
183
|
+
if (!(this.#on_result ? await this.#on_result(error, backend, name) : is_failover_safe(error))) throw error;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
this.#logger?.debug(`command=${name} ${field} result=success`);
|
|
187
|
+
await this.#on_result?.(null, backend, name);
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
throw last;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var VehicleRouterBase = class extends Router {
|
|
194
|
+
/** The VIN of the first backend that has one. */
|
|
195
|
+
get vin() {
|
|
196
|
+
return this._first_attribute("vin");
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const VehicleRouter = VehicleRouterBase;
|
|
200
|
+
exports.FAILOVER_SAFE_ERRORS = FAILOVER_SAFE_ERRORS;
|
|
201
|
+
exports.Router = Router;
|
|
202
|
+
exports.VehicleRouter = VehicleRouter;
|
|
203
|
+
exports.is_failover_safe = is_failover_safe;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Logger } from "../log.cjs";
|
|
2
|
+
type MethodKeys<B> = { [K in keyof B]-?: K extends `_${string}` | "then" ? never : B[K] extends ((...args: never[]) => unknown) ? K : never; }[keyof B];
|
|
3
|
+
type AllMethodKeys<B> = B extends unknown ? MethodKeys<B> : never;
|
|
4
|
+
export type MemberOf<B, K extends PropertyKey> = B extends { [P in K]: infer F; } ? F : never;
|
|
5
|
+
type Asynced<F> = F extends ((...args: infer A) => infer R) ? (...args: A) => Promise<Awaited<R>> : never;
|
|
6
|
+
/** Every method of the union `B`. A name on several members must satisfy all their signatures. */
|
|
7
|
+
export type Routed<B> = { [K in AllMethodKeys<B>]: Asynced<MemberOf<B, K>>; };
|
|
8
|
+
/** A primary, a secondary, and any number of further backends, tried in order. */
|
|
9
|
+
export type Backends = readonly [object, object, ...object[]];
|
|
10
|
+
/**
|
|
11
|
+
* Gates the primary. Omit it to attempt the primary directly and fail over on a
|
|
12
|
+
* failover-safe error, with no up-front probe.
|
|
13
|
+
*/
|
|
14
|
+
export type HealthCheck = boolean | (() => boolean | Promise<boolean>);
|
|
15
|
+
/**
|
|
16
|
+
* Called after every dispatched call. On failure, `error` is the thrown value
|
|
17
|
+
* and the return decides whether failover continues, replacing
|
|
18
|
+
* `is_failover_safe` in both directions; return `is_failover_safe(error)` to keep
|
|
19
|
+
* the default policy. On success, `error` is `null` and the return is ignored.
|
|
20
|
+
* A handler that throws propagates. `BluetoothUnconfirmedCommand` never gets here.
|
|
21
|
+
*/
|
|
22
|
+
export type ResultHandler = (error: unknown, backend: object, method: string) => boolean | Promise<boolean>;
|
|
23
|
+
export interface RouterOptions {
|
|
24
|
+
health?: HealthCheck | null;
|
|
25
|
+
on_result?: ResultHandler | null;
|
|
26
|
+
/** Receives one `command= backend= result=` line per attempted backend. Silent by default. */
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Failures in one backend's own path, before the device judged the command, so
|
|
31
|
+
* the next backend (a different path to the same device) may still succeed.
|
|
32
|
+
* Anything else, typically the device answering and refusing, propagates: the
|
|
33
|
+
* next backend reaches the same device and would get the same answer.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FAILOVER_SAFE_ERRORS: readonly (abstract new (...args: never[]) => unknown)[];
|
|
36
|
+
/**
|
|
37
|
+
* Whether a call that threw `error` may be retried on the next backend. Accepts
|
|
38
|
+
* any thrown value. A boolean `failover_safe` on the value decides for itself,
|
|
39
|
+
* in both directions, which lets a backend the router cannot import mark its own
|
|
40
|
+
* errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
|
|
41
|
+
*
|
|
42
|
+
* An unconfirmed command is never safe, whatever it is marked. Python's
|
|
43
|
+
* `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
|
|
44
|
+
* relies on the router checking first; here the predicate is safe to use alone.
|
|
45
|
+
*/
|
|
46
|
+
export declare function is_failover_safe(error: unknown): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Routes calls across an ordered list of backends, trying each in order.
|
|
49
|
+
*
|
|
50
|
+
* Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
|
|
51
|
+
* methods. Methods are derived from the backends' prototypes at construction, so
|
|
52
|
+
* a new backend method is routed with no change here. A name on no backend does
|
|
53
|
+
* not exist. A backend without the method is skipped. A method added to a backend
|
|
54
|
+
* after construction is not routed.
|
|
55
|
+
*
|
|
56
|
+
* Each call is attempted on the backends that have the method. If one throws a
|
|
57
|
+
* value `is_failover_safe` accepts, the same call is retried on the next with
|
|
58
|
+
* the same arguments. Anything else propagates at once. When every backend fails
|
|
59
|
+
* over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
|
|
60
|
+
*
|
|
61
|
+
* **Warning:** a failed call is replayed on the next backend, so a non-idempotent
|
|
62
|
+
* command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
|
|
63
|
+
* mid-flight, after a backend may have partly applied it, can be double-executed,
|
|
64
|
+
* or executed more than once across a longer chain. This is the accepted tradeoff
|
|
65
|
+
* of per-command failover. Callers that need exactly-once semantics gate dispatch
|
|
66
|
+
* with a health check or call a backend directly.
|
|
67
|
+
*
|
|
68
|
+
* `BluetoothUnconfirmedCommand` is the exception. The command may already have
|
|
69
|
+
* executed, so it propagates at once on any backend in the chain, is never
|
|
70
|
+
* replayed, and never reaches `on_result`.
|
|
71
|
+
*
|
|
72
|
+
* The health check gates the first backend only: when it is false the primary is
|
|
73
|
+
* skipped and the call routes to the rest. There is deliberately no per-backend
|
|
74
|
+
* health matrix.
|
|
75
|
+
*
|
|
76
|
+
* Plain attributes are not routed; read them from `primary` or `backends`. A
|
|
77
|
+
* backend's connection lifecycle is not proxied either.
|
|
78
|
+
*
|
|
79
|
+
* TS `private` methods exist at run time, so they are installed too: the types
|
|
80
|
+
* hide them. `#private` members are unreachable, which keeps a signing key off
|
|
81
|
+
* the router as it is off the backend.
|
|
82
|
+
*/
|
|
83
|
+
export declare class Router<Bs extends Backends> {
|
|
84
|
+
#private;
|
|
85
|
+
readonly backends: Bs;
|
|
86
|
+
constructor(backends: Bs, options?: RouterOptions);
|
|
87
|
+
/** The first backend, tried first when healthy. */
|
|
88
|
+
get primary(): Bs[0];
|
|
89
|
+
/** The second backend, tried when the primary is unhealthy or fails. */
|
|
90
|
+
get secondary(): Bs[1];
|
|
91
|
+
/** Resolve the health check. With none configured this reports `true`. */
|
|
92
|
+
is_healthy(): Promise<boolean>;
|
|
93
|
+
/** The `key=value` log field naming a backend. */
|
|
94
|
+
protected _backend_field(backend: object): string;
|
|
95
|
+
/** `name` on the first backend that has it, for a plain attribute getter. */
|
|
96
|
+
protected _first_attribute<K extends string>(name: K): MemberOf<Bs[number], K>;
|
|
97
|
+
}
|
|
98
|
+
declare class VehicleRouterBase<Bs extends Backends> extends Router<Bs> {
|
|
99
|
+
/** The VIN of the first backend that has one. */
|
|
100
|
+
get vin(): MemberOf<Bs[number], "vin">;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A router over vehicles, typically a Bluetooth primary and a cloud fallback.
|
|
104
|
+
* See {@link Router} for dispatch, failover and the replay warning.
|
|
105
|
+
*
|
|
106
|
+
* A same-named method must accept the caller's arguments on every backend that
|
|
107
|
+
* has it, or the call does not compile: Python only fails at run time. Where two
|
|
108
|
+
* backends legitimately differ, call `router.primary.x()`.
|
|
109
|
+
*/
|
|
110
|
+
export type VehicleRouter<Bs extends Backends> = VehicleRouterBase<Bs> & Routed<Bs[number]>;
|
|
111
|
+
export declare const VehicleRouter: new <const Bs extends Backends>(backends: Bs, options?: RouterOptions) => VehicleRouter<Bs>;
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Logger } from "../log.mjs";
|
|
2
|
+
type MethodKeys<B> = { [K in keyof B]-?: K extends `_${string}` | "then" ? never : B[K] extends ((...args: never[]) => unknown) ? K : never; }[keyof B];
|
|
3
|
+
type AllMethodKeys<B> = B extends unknown ? MethodKeys<B> : never;
|
|
4
|
+
export type MemberOf<B, K extends PropertyKey> = B extends { [P in K]: infer F; } ? F : never;
|
|
5
|
+
type Asynced<F> = F extends ((...args: infer A) => infer R) ? (...args: A) => Promise<Awaited<R>> : never;
|
|
6
|
+
/** Every method of the union `B`. A name on several members must satisfy all their signatures. */
|
|
7
|
+
export type Routed<B> = { [K in AllMethodKeys<B>]: Asynced<MemberOf<B, K>>; };
|
|
8
|
+
/** A primary, a secondary, and any number of further backends, tried in order. */
|
|
9
|
+
export type Backends = readonly [object, object, ...object[]];
|
|
10
|
+
/**
|
|
11
|
+
* Gates the primary. Omit it to attempt the primary directly and fail over on a
|
|
12
|
+
* failover-safe error, with no up-front probe.
|
|
13
|
+
*/
|
|
14
|
+
export type HealthCheck = boolean | (() => boolean | Promise<boolean>);
|
|
15
|
+
/**
|
|
16
|
+
* Called after every dispatched call. On failure, `error` is the thrown value
|
|
17
|
+
* and the return decides whether failover continues, replacing
|
|
18
|
+
* `is_failover_safe` in both directions; return `is_failover_safe(error)` to keep
|
|
19
|
+
* the default policy. On success, `error` is `null` and the return is ignored.
|
|
20
|
+
* A handler that throws propagates. `BluetoothUnconfirmedCommand` never gets here.
|
|
21
|
+
*/
|
|
22
|
+
export type ResultHandler = (error: unknown, backend: object, method: string) => boolean | Promise<boolean>;
|
|
23
|
+
export interface RouterOptions {
|
|
24
|
+
health?: HealthCheck | null;
|
|
25
|
+
on_result?: ResultHandler | null;
|
|
26
|
+
/** Receives one `command= backend= result=` line per attempted backend. Silent by default. */
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Failures in one backend's own path, before the device judged the command, so
|
|
31
|
+
* the next backend (a different path to the same device) may still succeed.
|
|
32
|
+
* Anything else, typically the device answering and refusing, propagates: the
|
|
33
|
+
* next backend reaches the same device and would get the same answer.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FAILOVER_SAFE_ERRORS: readonly (abstract new (...args: never[]) => unknown)[];
|
|
36
|
+
/**
|
|
37
|
+
* Whether a call that threw `error` may be retried on the next backend. Accepts
|
|
38
|
+
* any thrown value. A boolean `failover_safe` on the value decides for itself,
|
|
39
|
+
* in both directions, which lets a backend the router cannot import mark its own
|
|
40
|
+
* errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
|
|
41
|
+
*
|
|
42
|
+
* An unconfirmed command is never safe, whatever it is marked. Python's
|
|
43
|
+
* `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
|
|
44
|
+
* relies on the router checking first; here the predicate is safe to use alone.
|
|
45
|
+
*/
|
|
46
|
+
export declare function is_failover_safe(error: unknown): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Routes calls across an ordered list of backends, trying each in order.
|
|
49
|
+
*
|
|
50
|
+
* Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
|
|
51
|
+
* methods. Methods are derived from the backends' prototypes at construction, so
|
|
52
|
+
* a new backend method is routed with no change here. A name on no backend does
|
|
53
|
+
* not exist. A backend without the method is skipped. A method added to a backend
|
|
54
|
+
* after construction is not routed.
|
|
55
|
+
*
|
|
56
|
+
* Each call is attempted on the backends that have the method. If one throws a
|
|
57
|
+
* value `is_failover_safe` accepts, the same call is retried on the next with
|
|
58
|
+
* the same arguments. Anything else propagates at once. When every backend fails
|
|
59
|
+
* over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
|
|
60
|
+
*
|
|
61
|
+
* **Warning:** a failed call is replayed on the next backend, so a non-idempotent
|
|
62
|
+
* command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
|
|
63
|
+
* mid-flight, after a backend may have partly applied it, can be double-executed,
|
|
64
|
+
* or executed more than once across a longer chain. This is the accepted tradeoff
|
|
65
|
+
* of per-command failover. Callers that need exactly-once semantics gate dispatch
|
|
66
|
+
* with a health check or call a backend directly.
|
|
67
|
+
*
|
|
68
|
+
* `BluetoothUnconfirmedCommand` is the exception. The command may already have
|
|
69
|
+
* executed, so it propagates at once on any backend in the chain, is never
|
|
70
|
+
* replayed, and never reaches `on_result`.
|
|
71
|
+
*
|
|
72
|
+
* The health check gates the first backend only: when it is false the primary is
|
|
73
|
+
* skipped and the call routes to the rest. There is deliberately no per-backend
|
|
74
|
+
* health matrix.
|
|
75
|
+
*
|
|
76
|
+
* Plain attributes are not routed; read them from `primary` or `backends`. A
|
|
77
|
+
* backend's connection lifecycle is not proxied either.
|
|
78
|
+
*
|
|
79
|
+
* TS `private` methods exist at run time, so they are installed too: the types
|
|
80
|
+
* hide them. `#private` members are unreachable, which keeps a signing key off
|
|
81
|
+
* the router as it is off the backend.
|
|
82
|
+
*/
|
|
83
|
+
export declare class Router<Bs extends Backends> {
|
|
84
|
+
#private;
|
|
85
|
+
readonly backends: Bs;
|
|
86
|
+
constructor(backends: Bs, options?: RouterOptions);
|
|
87
|
+
/** The first backend, tried first when healthy. */
|
|
88
|
+
get primary(): Bs[0];
|
|
89
|
+
/** The second backend, tried when the primary is unhealthy or fails. */
|
|
90
|
+
get secondary(): Bs[1];
|
|
91
|
+
/** Resolve the health check. With none configured this reports `true`. */
|
|
92
|
+
is_healthy(): Promise<boolean>;
|
|
93
|
+
/** The `key=value` log field naming a backend. */
|
|
94
|
+
protected _backend_field(backend: object): string;
|
|
95
|
+
/** `name` on the first backend that has it, for a plain attribute getter. */
|
|
96
|
+
protected _first_attribute<K extends string>(name: K): MemberOf<Bs[number], K>;
|
|
97
|
+
}
|
|
98
|
+
declare class VehicleRouterBase<Bs extends Backends> extends Router<Bs> {
|
|
99
|
+
/** The VIN of the first backend that has one. */
|
|
100
|
+
get vin(): MemberOf<Bs[number], "vin">;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A router over vehicles, typically a Bluetooth primary and a cloud fallback.
|
|
104
|
+
* See {@link Router} for dispatch, failover and the replay warning.
|
|
105
|
+
*
|
|
106
|
+
* A same-named method must accept the caller's arguments on every backend that
|
|
107
|
+
* has it, or the call does not compile: Python only fails at run time. Where two
|
|
108
|
+
* backends legitimately differ, call `router.primary.x()`.
|
|
109
|
+
*/
|
|
110
|
+
export type VehicleRouter<Bs extends Backends> = VehicleRouterBase<Bs> & Routed<Bs[number]>;
|
|
111
|
+
export declare const VehicleRouter: new <const Bs extends Backends>(backends: Bs, options?: RouterOptions) => VehicleRouter<Bs>;
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { BadGateway, BluetoothCloudUnavailableError, BluetoothCommandFailed, BluetoothTimeout, BluetoothTransportError, BluetoothUnconfirmedCommand, Forbidden, GatewayTimeout, InsufficientCredits, InternalServerError, InvalidScope, InvalidToken, KEY_REJECTED_FAULTS, LoginRequired, MissingToken, OAuthExpired, OAuthInvalid, PaymentRequired, RateLimited, ServiceUnavailable, SessionInfoAuthenticationFault, SignedCommandRequired, SigningDisabled, SubscriptionRequired, TeslaFleetConnectionError, TeslaFleetError, TeslaFleetMessageFaultFieldExceedsMTU, TeslaFleetMessageFaultResponseSizeExceedsMTU, VehicleOffline, VehicleSubscriptionRequired } from "../exceptions.mjs";
|
|
2
|
+
// src/router/base.ts
|
|
3
|
+
/**
|
|
4
|
+
* Failures in one backend's own path, before the device judged the command, so
|
|
5
|
+
* the next backend (a different path to the same device) may still succeed.
|
|
6
|
+
* Anything else, typically the device answering and refusing, propagates: the
|
|
7
|
+
* next backend reaches the same device and would get the same answer.
|
|
8
|
+
*/
|
|
9
|
+
const FAILOVER_SAFE_ERRORS = [
|
|
10
|
+
TeslaFleetConnectionError,
|
|
11
|
+
BluetoothTransportError,
|
|
12
|
+
BluetoothTimeout,
|
|
13
|
+
BluetoothCommandFailed,
|
|
14
|
+
BluetoothCloudUnavailableError,
|
|
15
|
+
VehicleOffline,
|
|
16
|
+
RateLimited,
|
|
17
|
+
InternalServerError,
|
|
18
|
+
BadGateway,
|
|
19
|
+
ServiceUnavailable,
|
|
20
|
+
GatewayTimeout,
|
|
21
|
+
...KEY_REJECTED_FAULTS,
|
|
22
|
+
SessionInfoAuthenticationFault,
|
|
23
|
+
SigningDisabled,
|
|
24
|
+
SignedCommandRequired,
|
|
25
|
+
TeslaFleetMessageFaultFieldExceedsMTU,
|
|
26
|
+
TeslaFleetMessageFaultResponseSizeExceedsMTU,
|
|
27
|
+
InvalidToken,
|
|
28
|
+
MissingToken,
|
|
29
|
+
OAuthExpired,
|
|
30
|
+
OAuthInvalid,
|
|
31
|
+
LoginRequired,
|
|
32
|
+
PaymentRequired,
|
|
33
|
+
SubscriptionRequired,
|
|
34
|
+
VehicleSubscriptionRequired,
|
|
35
|
+
InsufficientCredits,
|
|
36
|
+
Forbidden,
|
|
37
|
+
InvalidScope
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Whether a command that threw `error` may already have executed. The field
|
|
41
|
+
* check is the one that matters across the dual-package hazard: a process that
|
|
42
|
+
* loads this package through both `import` and `require` has two copies of the
|
|
43
|
+
* class, and `instanceof` fails across them.
|
|
44
|
+
*/
|
|
45
|
+
function is_unconfirmed(error) {
|
|
46
|
+
return error instanceof BluetoothUnconfirmedCommand || error?.unconfirmed === true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a call that threw `error` may be retried on the next backend. Accepts
|
|
50
|
+
* any thrown value. A boolean `failover_safe` on the value decides for itself,
|
|
51
|
+
* in both directions, which lets a backend the router cannot import mark its own
|
|
52
|
+
* errors. Otherwise it is safe only as an instance of `FAILOVER_SAFE_ERRORS`.
|
|
53
|
+
*
|
|
54
|
+
* An unconfirmed command is never safe, whatever it is marked. Python's
|
|
55
|
+
* `is_failover_safe` answers true for it (it subclasses `BluetoothTimeout`) and
|
|
56
|
+
* relies on the router checking first; here the predicate is safe to use alone.
|
|
57
|
+
*/
|
|
58
|
+
function is_failover_safe(error) {
|
|
59
|
+
if (is_unconfirmed(error)) return false;
|
|
60
|
+
const marker = error?.failover_safe;
|
|
61
|
+
if (typeof marker === "boolean") return marker;
|
|
62
|
+
return FAILOVER_SAFE_ERRORS.some((cls) => error instanceof cls);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The names `backend` routes: plain function properties, own or inherited, up
|
|
66
|
+
* to `Object.prototype`. The most-derived property of a name decides, so a
|
|
67
|
+
* subclass that shadows a method with a getter or a value stops it routing.
|
|
68
|
+
* Getters are never invoked, and symbol keys (`Symbol.asyncDispose`) are not
|
|
69
|
+
* seen, so a context manager is not proxied.
|
|
70
|
+
*/
|
|
71
|
+
function methodNames(backend) {
|
|
72
|
+
const names = /* @__PURE__ */ new Set();
|
|
73
|
+
const seen = /* @__PURE__ */ new Set();
|
|
74
|
+
for (let target = backend; target !== null && target !== Object.prototype; target = Object.getPrototypeOf(target)) for (const name of Object.getOwnPropertyNames(target)) {
|
|
75
|
+
if (seen.has(name)) continue;
|
|
76
|
+
seen.add(name);
|
|
77
|
+
if (name === "constructor" || name === "then" || name.startsWith("_")) continue;
|
|
78
|
+
if (typeof Object.getOwnPropertyDescriptor(target, name)?.value === "function") names.add(name);
|
|
79
|
+
}
|
|
80
|
+
return names;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A `TeslaFleetError`'s message is its class's, so it is safe to log. Anything
|
|
84
|
+
* else may hold a secret, so only its type is logged (see `src/log.ts`).
|
|
85
|
+
*/
|
|
86
|
+
function errorText(error) {
|
|
87
|
+
if (error instanceof TeslaFleetError) return `${error.name}: ${error.message}`;
|
|
88
|
+
return error instanceof Error ? error.name : typeof error;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Routes calls across an ordered list of backends, trying each in order.
|
|
92
|
+
*
|
|
93
|
+
* Use {@link VehicleRouter} or `EnergySiteRouter`, which type the routed
|
|
94
|
+
* methods. Methods are derived from the backends' prototypes at construction, so
|
|
95
|
+
* a new backend method is routed with no change here. A name on no backend does
|
|
96
|
+
* not exist. A backend without the method is skipped. A method added to a backend
|
|
97
|
+
* after construction is not routed.
|
|
98
|
+
*
|
|
99
|
+
* Each call is attempted on the backends that have the method. If one throws a
|
|
100
|
+
* value `is_failover_safe` accepts, the same call is retried on the next with
|
|
101
|
+
* the same arguments. Anything else propagates at once. When every backend fails
|
|
102
|
+
* over, the last thrown value is rethrown unchanged, even if it is not an `Error`.
|
|
103
|
+
*
|
|
104
|
+
* **Warning:** a failed call is replayed on the next backend, so a non-idempotent
|
|
105
|
+
* command (`honk_horn`, `actuate_trunk`, `door_unlock`, `charge_start`) that fails
|
|
106
|
+
* mid-flight, after a backend may have partly applied it, can be double-executed,
|
|
107
|
+
* or executed more than once across a longer chain. This is the accepted tradeoff
|
|
108
|
+
* of per-command failover. Callers that need exactly-once semantics gate dispatch
|
|
109
|
+
* with a health check or call a backend directly.
|
|
110
|
+
*
|
|
111
|
+
* `BluetoothUnconfirmedCommand` is the exception. The command may already have
|
|
112
|
+
* executed, so it propagates at once on any backend in the chain, is never
|
|
113
|
+
* replayed, and never reaches `on_result`.
|
|
114
|
+
*
|
|
115
|
+
* The health check gates the first backend only: when it is false the primary is
|
|
116
|
+
* skipped and the call routes to the rest. There is deliberately no per-backend
|
|
117
|
+
* health matrix.
|
|
118
|
+
*
|
|
119
|
+
* Plain attributes are not routed; read them from `primary` or `backends`. A
|
|
120
|
+
* backend's connection lifecycle is not proxied either.
|
|
121
|
+
*
|
|
122
|
+
* TS `private` methods exist at run time, so they are installed too: the types
|
|
123
|
+
* hide them. `#private` members are unreachable, which keeps a signing key off
|
|
124
|
+
* the router as it is off the backend.
|
|
125
|
+
*/
|
|
126
|
+
var Router = class {
|
|
127
|
+
backends;
|
|
128
|
+
#health;
|
|
129
|
+
#on_result;
|
|
130
|
+
#logger;
|
|
131
|
+
constructor(backends, options = {}) {
|
|
132
|
+
if (backends.length < 2) throw new TypeError("A router needs a primary and a secondary backend.");
|
|
133
|
+
this.backends = Object.freeze([...backends]);
|
|
134
|
+
this.#health = options.health;
|
|
135
|
+
this.#on_result = options.on_result;
|
|
136
|
+
this.#logger = options.logger;
|
|
137
|
+
const targets = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const backend of backends) for (const name of methodNames(backend)) targets.set(name, [...targets.get(name) ?? [], backend]);
|
|
139
|
+
for (const [name, having] of targets) {
|
|
140
|
+
if (name in this) continue;
|
|
141
|
+
Object.defineProperty(this, name, { value: (...args) => this.#dispatch(name, having, args) });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** The first backend, tried first when healthy. */
|
|
145
|
+
get primary() {
|
|
146
|
+
return this.backends[0];
|
|
147
|
+
}
|
|
148
|
+
/** The second backend, tried when the primary is unhealthy or fails. */
|
|
149
|
+
get secondary() {
|
|
150
|
+
return this.backends[1];
|
|
151
|
+
}
|
|
152
|
+
/** Resolve the health check. With none configured this reports `true`. */
|
|
153
|
+
async is_healthy() {
|
|
154
|
+
const health = this.#health;
|
|
155
|
+
if (health == null) return true;
|
|
156
|
+
if (typeof health === "boolean") return health;
|
|
157
|
+
return Boolean(await health());
|
|
158
|
+
}
|
|
159
|
+
/** The `key=value` log field naming a backend. */
|
|
160
|
+
_backend_field(backend) {
|
|
161
|
+
return `backend=${backend.constructor?.name ?? "Object"}`;
|
|
162
|
+
}
|
|
163
|
+
/** `name` on the first backend that has it, for a plain attribute getter. */
|
|
164
|
+
_first_attribute(name) {
|
|
165
|
+
return this.backends.find((candidate) => name in candidate)?.[name];
|
|
166
|
+
}
|
|
167
|
+
async #dispatch(name, targets, args) {
|
|
168
|
+
let start = 0;
|
|
169
|
+
if (this.#health != null && targets.length > 1 && targets[0] === this.backends[0] && !await this.is_healthy()) start = 1;
|
|
170
|
+
let last;
|
|
171
|
+
for (const backend of targets.slice(start)) {
|
|
172
|
+
const field = this._backend_field(backend);
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await backend[name](...args);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (is_unconfirmed(error)) {
|
|
178
|
+
this.#logger?.debug(`command=${name} ${field} result=unconfirmed error=${errorText(error)}`);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
last = error;
|
|
182
|
+
this.#logger?.debug(`command=${name} ${field} result=error error=${errorText(error)}`);
|
|
183
|
+
if (!(this.#on_result ? await this.#on_result(error, backend, name) : is_failover_safe(error))) throw error;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
this.#logger?.debug(`command=${name} ${field} result=success`);
|
|
187
|
+
await this.#on_result?.(null, backend, name);
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
throw last;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var VehicleRouterBase = class extends Router {
|
|
194
|
+
/** The VIN of the first backend that has one. */
|
|
195
|
+
get vin() {
|
|
196
|
+
return this._first_attribute("vin");
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const VehicleRouter = VehicleRouterBase;
|
|
200
|
+
export { FAILOVER_SAFE_ERRORS, Router, VehicleRouter, is_failover_safe };
|