@homebridge-plugins/homebridge-august 3.1.4 → 3.2.1
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/.github/scripts/compute-version-override.cjs +51 -0
- package/CHANGELOG.md +27 -0
- package/dist/connectivity-manager.d.ts +7 -0
- package/dist/connectivity-manager.d.ts.map +1 -1
- package/dist/connectivity-manager.js +148 -30
- package/dist/connectivity-manager.js.map +1 -1
- package/dist/package.json +2 -2
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
- package/test/connectivity-manager.test.ts +239 -29
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const child = require('node:child_process')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
|
|
6
|
+
const bump = (process.argv[2] || 'patch').toLowerCase()
|
|
7
|
+
const allowed = new Set(['major', 'minor', 'patch'])
|
|
8
|
+
|
|
9
|
+
if (!allowed.has(bump)) {
|
|
10
|
+
console.error(`Invalid bump level '${bump}'. Use major, minor, or patch.`)
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
15
|
+
|
|
16
|
+
function inc(v, level) {
|
|
17
|
+
const parts = v.split('-')[0].split('+')[0].split('.').map(Number)
|
|
18
|
+
if (level === 'major') {
|
|
19
|
+
parts[0] += 1
|
|
20
|
+
parts[1] = 0
|
|
21
|
+
parts[2] = 0
|
|
22
|
+
} else if (level === 'minor') {
|
|
23
|
+
parts[1] += 1
|
|
24
|
+
parts[2] = 0
|
|
25
|
+
} else {
|
|
26
|
+
parts[2] += 1
|
|
27
|
+
}
|
|
28
|
+
return parts.join('.')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function latestStable(name) {
|
|
32
|
+
try {
|
|
33
|
+
const versions = JSON.parse(child.execSync(`npm info ${name} versions --json`).toString('utf8').trim())
|
|
34
|
+
const stable = (Array.isArray(versions) ? versions : [])
|
|
35
|
+
.filter(v => /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/.test(v) && !v.includes('-'))
|
|
36
|
+
stable.sort((a, b) => {
|
|
37
|
+
const ap = a.split('.').map(Number)
|
|
38
|
+
const bp = b.split('.').map(Number)
|
|
39
|
+
for (let i = 0; i < 3; i++) {
|
|
40
|
+
if (ap[i] !== bp[i]) return bp[i] - ap[i]
|
|
41
|
+
}
|
|
42
|
+
return 0
|
|
43
|
+
})
|
|
44
|
+
return stable[0] || ''
|
|
45
|
+
} catch {
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const base = latestStable(pkg.name) || pkg.version
|
|
51
|
+
process.stdout.write(inc(base, bump))
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
# [3.2.0](https://github.com/homebridge-plugins/homebridge-august/compare/v3.1.4...v3.2.0) (2026-04-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **platform:** use august-yale's typed exceptions for error classification ([93219ac](https://github.com/homebridge-plugins/homebridge-august/commit/93219accdf8571330b8f3fefa029d727ddf7a6c3))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [3.1.4](https://github.com/homebridge-plugins/homebridge-august/compare/v3.1.3...v3.1.4) (2026-04-27)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **platform:** destroy August client on session refresh instead of end() ([a12f2b3](https://github.com/homebridge-plugins/homebridge-august/commit/a12f2b328263523eef3b2549383f00ce6dfbf0a1))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **lock:** wire PubNub reconnect signal into ConnectivityManager ([462b22c](https://github.com/homebridge-plugins/homebridge-august/commit/462b22c5b53e125d3ec31eceea538a74360f5760))
|
|
21
|
+
* **platform:** add ConnectivityManager (unwired) ([6377ac4](https://github.com/homebridge-plugins/homebridge-august/commit/6377ac435320aa884e82b32f25f163b2bd9fde6a))
|
|
22
|
+
* **platform:** platform-level serial poller, drop per-lock interval/retry ([aa973a4](https://github.com/homebridge-plugins/homebridge-august/commit/aa973a4e46cd4252f335a5c6988f94148cafefbf))
|
|
23
|
+
* **platform:** route pushChanges through ConnectivityManager ([41080f4](https://github.com/homebridge-plugins/homebridge-august/commit/41080f490cd71b504e22f56a80ae6e1c8c9eb016))
|
|
24
|
+
* **platform:** wire ConnectivityManager into AugustPlatform ([0ce28e3](https://github.com/homebridge-plugins/homebridge-august/commit/0ce28e359a68720ed278e17097f61262ea8705b4))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
1
28
|
## [3.1.2](https://github.com/homebridge-plugins/homebridge-august/compare/v3.1.1...v3.1.2) (2026-04-18)
|
|
2
29
|
|
|
3
30
|
### Bug Fixes
|
|
@@ -27,9 +27,13 @@ export declare class ConnectivityManager {
|
|
|
27
27
|
private state;
|
|
28
28
|
private client?;
|
|
29
29
|
private probeTimer?;
|
|
30
|
+
private heartbeatTimer?;
|
|
30
31
|
private backoffIndex;
|
|
31
32
|
private rebuildInFlight?;
|
|
32
33
|
private listeners;
|
|
34
|
+
private offlineSinceMs?;
|
|
35
|
+
private probeAttempts;
|
|
36
|
+
private lastFailureMessage?;
|
|
33
37
|
constructor(log: Logging, credentialsFactory: () => Promise<credentials>,
|
|
34
38
|
/**
|
|
35
39
|
* Called whenever the manager builds or rebuilds the August client.
|
|
@@ -83,6 +87,9 @@ export declare class ConnectivityManager {
|
|
|
83
87
|
private rebuildClient;
|
|
84
88
|
private classify;
|
|
85
89
|
private transitionTo;
|
|
90
|
+
private startHeartbeat;
|
|
91
|
+
private stopHeartbeat;
|
|
92
|
+
private emitHeartbeat;
|
|
86
93
|
private clearProbeTimer;
|
|
87
94
|
}
|
|
88
95
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectivity-manager.d.ts","sourceRoot":"","sources":["../src/connectivity-manager.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,
|
|
1
|
+
{"version":3,"file":"connectivity-manager.d.ts","sourceRoot":"","sources":["../src/connectivity-manager.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,MAAiE,MAAM,aAAa,CAAA;AAE3F,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,CAAA;AA6BjF,UAAU,cAAc;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,qBAAa,YAAa,SAAQ,KAAK;gBACzB,KAAK,EAAE,MAAM;CAI1B;AAED,qBAAa,mBAAmB;IAiB5B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IACnC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe;IAzBlC,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,MAAM,CAAC,CAAQ;IACvB,OAAO,CAAC,UAAU,CAAC,CAAgB;IACnC,OAAO,CAAC,cAAc,CAAC,CAAgB;IACvC,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,eAAe,CAAC,CAAe;IACvC,OAAO,CAAC,SAAS,CAAwE;IAKzF,OAAO,CAAC,cAAc,CAAC,CAAQ;IAC/B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,kBAAkB,CAAC,CAAQ;gBAGhB,GAAG,EAAE,OAAO,EACZ,kBAAkB,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC;IAC/D;;;;;;OAMG;IACc,eAAe,GAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,KAAK,IAAe;IAG7E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B;;;;;;;;;;;;OAYG;IACG,OAAO,CAAC,CAAC,EACb,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EAClC,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAgCzB;;;;;OAKG;IACH,iBAAiB,IAAI,IAAI;IAYzB,QAAQ,IAAI,iBAAiB;IAI7B;;;;;OAKG;IACH,SAAS,IAAI,MAAM,GAAG,SAAS;IAI/B;;;;OAIG;IACH,aAAa,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxF,QAAQ,IAAI,IAAI;IAWhB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,aAAa;YAQP,QAAQ;YAqER,aAAa;IAkB3B,OAAO,CAAC,QAAQ;IAkDhB,OAAO,CAAC,YAAY;IA8BpB,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,eAAe;CAMxB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import August, { TimeoutError } from 'august-yale';
|
|
1
|
+
import August, { AbortedError, InvalidAuth, NetworkError, TimeoutError } from 'august-yale';
|
|
2
2
|
/**
|
|
3
3
|
* Backoff schedule for probes after the network is detected as broken.
|
|
4
4
|
* Capped at 5 minutes; ±10% jitter is applied at use to avoid synchronized
|
|
@@ -7,16 +7,20 @@ import August, { TimeoutError } from 'august-yale';
|
|
|
7
7
|
const BACKOFF_SCHEDULE_MS = [5_000, 10_000, 20_000, 40_000, 80_000, 160_000, 300_000];
|
|
8
8
|
/** Probe timeout — short on purpose. A healthy August API responds in <1s. */
|
|
9
9
|
const PROBE_TIMEOUT_MS = 8_000;
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
/**
|
|
11
|
+
* How often to emit a heartbeat log line while offline.
|
|
12
|
+
*
|
|
13
|
+
* Without it the log goes silent after the initial healthy → degraded →
|
|
14
|
+
* offline transition, making it impossible to distinguish "state machine
|
|
15
|
+
* is working hard, probes are failing every backoff slot" from "state
|
|
16
|
+
* machine is silently stuck". The heartbeat surfaces probe attempts and
|
|
17
|
+
* the most recent failure so future regressions are debuggable from the
|
|
18
|
+
* log alone.
|
|
19
|
+
*
|
|
20
|
+
* One hour matches the typical "I checked on it later" cadence; not so
|
|
21
|
+
* frequent it becomes log spam during a real ISP outage.
|
|
22
|
+
*/
|
|
23
|
+
const OFFLINE_HEARTBEAT_INTERVAL_MS = 60 * 60 * 1000;
|
|
20
24
|
export class OfflineError extends Error {
|
|
21
25
|
constructor(label) {
|
|
22
26
|
super(`Operation skipped — connectivity is offline: ${label}`);
|
|
@@ -30,9 +34,16 @@ export class ConnectivityManager {
|
|
|
30
34
|
state = 'healthy';
|
|
31
35
|
client;
|
|
32
36
|
probeTimer;
|
|
37
|
+
heartbeatTimer;
|
|
33
38
|
backoffIndex = 0;
|
|
34
39
|
rebuildInFlight;
|
|
35
40
|
listeners = new Set();
|
|
41
|
+
// Diagnostic counters. Reset on transition out of offline. Used by the
|
|
42
|
+
// heartbeat log so an ongoing outage is visible in the log without
|
|
43
|
+
// having to trace per-probe details.
|
|
44
|
+
offlineSinceMs;
|
|
45
|
+
probeAttempts = 0;
|
|
46
|
+
lastFailureMessage;
|
|
36
47
|
constructor(log, credentialsFactory,
|
|
37
48
|
/**
|
|
38
49
|
* Called whenever the manager builds or rebuilds the August client.
|
|
@@ -107,7 +118,10 @@ export class ConnectivityManager {
|
|
|
107
118
|
if (this.state === 'healthy') {
|
|
108
119
|
return;
|
|
109
120
|
}
|
|
110
|
-
|
|
121
|
+
const offlineFor = this.offlineSinceMs
|
|
122
|
+
? ` (offline for ${Math.floor((Date.now() - this.offlineSinceMs) / 1000)}s)`
|
|
123
|
+
: '';
|
|
124
|
+
this.log.info(`Connectivity: PubNub reconnect signal received in ${this.state}${offlineFor} — probing immediately`);
|
|
111
125
|
this.clearProbeTimer();
|
|
112
126
|
void this.runProbe();
|
|
113
127
|
}
|
|
@@ -134,6 +148,7 @@ export class ConnectivityManager {
|
|
|
134
148
|
}
|
|
135
149
|
shutdown() {
|
|
136
150
|
this.clearProbeTimer();
|
|
151
|
+
this.stopHeartbeat();
|
|
137
152
|
this.client?.destroy();
|
|
138
153
|
this.client = undefined;
|
|
139
154
|
this.onClientChanged(undefined);
|
|
@@ -174,25 +189,61 @@ export class ConnectivityManager {
|
|
|
174
189
|
this.probeTimer = setTimeout(() => void this.runProbe(), jittered);
|
|
175
190
|
}
|
|
176
191
|
async runProbe() {
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
this.probeAttempts++;
|
|
193
|
+
let client = this.client;
|
|
194
|
+
if (!client) {
|
|
195
|
+
// No client at all — rebuild from credentials. This branch is
|
|
196
|
+
// hit only at startup if init() hasn't completed; in steady
|
|
197
|
+
// state we always have a client.
|
|
198
|
+
this.log.warn(`Connectivity: probe attempt #${this.probeAttempts} — no client to probe; trying to (re)build`);
|
|
199
|
+
try {
|
|
200
|
+
await this.rebuildClient('probe with no client');
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
this.lastFailureMessage = `probe-build: ${e?.message ?? e}`;
|
|
204
|
+
this.scheduleProbe();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
client = this.client;
|
|
208
|
+
if (!client) {
|
|
209
|
+
// rebuildClient resolved but didn't actually populate this.client.
|
|
210
|
+
// Defensive: don't proceed without a client; reschedule and try
|
|
211
|
+
// again on the next backoff slot.
|
|
212
|
+
this.lastFailureMessage = 'probe-build: rebuildClient produced no client';
|
|
213
|
+
this.scheduleProbe();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
179
216
|
}
|
|
217
|
+
// Reset the transport BEFORE probing.
|
|
218
|
+
//
|
|
219
|
+
// The previous implementation probed against the existing client's
|
|
220
|
+
// socket pool. That pool may hold stale half-open sockets from
|
|
221
|
+
// before the outage; probing against them just keeps failing on
|
|
222
|
+
// dead connections, so the system stayed offline even after the
|
|
223
|
+
// network had recovered. The PubNub-triggered immediate probe
|
|
224
|
+
// hit the same wall.
|
|
225
|
+
//
|
|
226
|
+
// resetTransport() throws away the dispatcher and creates a fresh
|
|
227
|
+
// one without losing auth state, the session token, or
|
|
228
|
+
// configuration. Distinct from destroy() + new August() — that
|
|
229
|
+
// would force a re-auth round-trip on the next call, which itself
|
|
230
|
+
// can fail on a still-flaky network and leave us stuck. With just
|
|
231
|
+
// a transport reset, the next request gets a clean connection
|
|
232
|
+
// pool and reuses the cached session token; if it succeeds, we're
|
|
233
|
+
// healthy in one round-trip.
|
|
234
|
+
client.resetTransport();
|
|
235
|
+
this.log.info(`Connectivity: probe attempt #${this.probeAttempts} (state=${this.state})`);
|
|
180
236
|
try {
|
|
181
|
-
// Cheap authenticated call against the EXISTING client. .locks()
|
|
182
|
-
// returns a small map and is the lightest endpoint. Race against a
|
|
183
|
-
// short timeout — the goal is "is the network working", not
|
|
184
|
-
// "is everything healthy".
|
|
185
237
|
await Promise.race([
|
|
186
|
-
|
|
238
|
+
client.locks(),
|
|
187
239
|
new Promise((_resolve, reject) => setTimeout(() => reject(new TimeoutError('probe timed out')), PROBE_TIMEOUT_MS)),
|
|
188
240
|
]);
|
|
189
|
-
|
|
190
|
-
// accumulated before the outage, then mark healthy.
|
|
191
|
-
await this.rebuildClient('probe succeeded');
|
|
241
|
+
this.log.info(`Connectivity: probe attempt #${this.probeAttempts} succeeded`);
|
|
192
242
|
this.transitionTo('healthy');
|
|
193
243
|
this.backoffIndex = 0;
|
|
194
244
|
}
|
|
195
|
-
catch {
|
|
245
|
+
catch (e) {
|
|
246
|
+
this.lastFailureMessage = `${this.classify(e)}: ${e?.message ?? e}`;
|
|
196
247
|
if (this.state !== 'offline') {
|
|
197
248
|
this.log.warn('Connectivity: probe failed — entering offline state');
|
|
198
249
|
this.transitionTo('offline');
|
|
@@ -220,22 +271,51 @@ export class ConnectivityManager {
|
|
|
220
271
|
}
|
|
221
272
|
}
|
|
222
273
|
classify(e) {
|
|
223
|
-
|
|
224
|
-
|
|
274
|
+
// august-yale wraps every transport-level fetch failure into one of
|
|
275
|
+
// its typed exceptions before the error reaches us. We rely on those
|
|
276
|
+
// types here rather than re-implementing undici's 24-class taxonomy.
|
|
277
|
+
//
|
|
278
|
+
// - NetworkError (and its TimeoutError subclass) covers connect
|
|
279
|
+
// timeouts, socket resets, headers/body timeouts, DNS failures,
|
|
280
|
+
// TLS proxy failures, malformed responses, etc.
|
|
281
|
+
//
|
|
282
|
+
// - AbortedError reflects an in-flight request racing with our own
|
|
283
|
+
// teardown (e.g. during rebuildClient). NOT 'network': the consumer
|
|
284
|
+
// intent was to tear down, so we don't want this to drive the
|
|
285
|
+
// state machine into degraded.
|
|
286
|
+
//
|
|
287
|
+
// - InvalidAuth is august-yale's auth-failure type. Maps to 'auth'.
|
|
288
|
+
//
|
|
289
|
+
// Anything else (including programmer errors and server-answered
|
|
290
|
+
// 4xx/5xx that august-yale leaves unwrapped) is 'transient' — bubble
|
|
291
|
+
// back to the caller, don't change connectivity state.
|
|
292
|
+
if (e instanceof NetworkError) {
|
|
225
293
|
return 'network';
|
|
226
294
|
}
|
|
227
|
-
if (
|
|
228
|
-
return '
|
|
295
|
+
if (e instanceof AbortedError) {
|
|
296
|
+
return 'transient';
|
|
229
297
|
}
|
|
230
|
-
if (
|
|
298
|
+
if (e instanceof TimeoutError) {
|
|
299
|
+
// Defensive: TimeoutError is also a NetworkError so the first
|
|
300
|
+
// check should match. Kept for clarity in case of future
|
|
301
|
+
// exception hierarchy changes upstream.
|
|
231
302
|
return 'network';
|
|
232
303
|
}
|
|
233
|
-
if (
|
|
234
|
-
return '
|
|
304
|
+
if (e instanceof InvalidAuth) {
|
|
305
|
+
return 'auth';
|
|
235
306
|
}
|
|
307
|
+
// Fallback for HTTP-level errors that august-yale leaves unwrapped
|
|
308
|
+
// (e.g. server answered with a non-2xx without bare-401 semantics).
|
|
309
|
+
const err = e;
|
|
236
310
|
if (err?.statusCode === 401) {
|
|
237
311
|
return 'auth';
|
|
238
312
|
}
|
|
313
|
+
if (err?.statusCode === 502 || err?.statusCode === 503 || err?.statusCode === 504) {
|
|
314
|
+
// 5xx responses where the server is reachable but reporting an
|
|
315
|
+
// upstream issue. Treat as network — these are recoverable by
|
|
316
|
+
// waiting and retrying (matches the state-machine semantic).
|
|
317
|
+
return 'network';
|
|
318
|
+
}
|
|
239
319
|
return 'transient';
|
|
240
320
|
}
|
|
241
321
|
transitionTo(next) {
|
|
@@ -245,6 +325,19 @@ export class ConnectivityManager {
|
|
|
245
325
|
}
|
|
246
326
|
this.state = next;
|
|
247
327
|
this.log.info(`Connectivity: ${prev} -> ${next}`);
|
|
328
|
+
// Heartbeat lifecycle: start when entering offline, stop when leaving.
|
|
329
|
+
// offlineSinceMs is set on entry; probe-attempt count is NOT reset
|
|
330
|
+
// here because runProbe() already incremented it for the failure
|
|
331
|
+
// that triggered this transition.
|
|
332
|
+
if (next === 'offline') {
|
|
333
|
+
this.offlineSinceMs = Date.now();
|
|
334
|
+
this.startHeartbeat();
|
|
335
|
+
}
|
|
336
|
+
else if (prev === 'offline') {
|
|
337
|
+
this.stopHeartbeat();
|
|
338
|
+
this.offlineSinceMs = undefined;
|
|
339
|
+
this.probeAttempts = 0;
|
|
340
|
+
}
|
|
248
341
|
for (const l of this.listeners) {
|
|
249
342
|
try {
|
|
250
343
|
l(next, prev);
|
|
@@ -254,6 +347,31 @@ export class ConnectivityManager {
|
|
|
254
347
|
}
|
|
255
348
|
}
|
|
256
349
|
}
|
|
350
|
+
startHeartbeat() {
|
|
351
|
+
this.stopHeartbeat();
|
|
352
|
+
this.heartbeatTimer = setInterval(() => this.emitHeartbeat(), OFFLINE_HEARTBEAT_INTERVAL_MS);
|
|
353
|
+
// Allow Node to exit if this is the only outstanding timer; we
|
|
354
|
+
// don't want the heartbeat to keep the process alive on shutdown.
|
|
355
|
+
if (typeof this.heartbeatTimer.unref === 'function') {
|
|
356
|
+
this.heartbeatTimer.unref();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
stopHeartbeat() {
|
|
360
|
+
if (this.heartbeatTimer) {
|
|
361
|
+
clearInterval(this.heartbeatTimer);
|
|
362
|
+
this.heartbeatTimer = undefined;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
emitHeartbeat() {
|
|
366
|
+
if (this.state !== 'offline' || this.offlineSinceMs === undefined) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const minutes = Math.floor((Date.now() - this.offlineSinceMs) / 60000);
|
|
370
|
+
const lastFailure = this.lastFailureMessage ?? 'unknown';
|
|
371
|
+
this.log.warn(`Connectivity: still offline after ${minutes} min — `
|
|
372
|
+
+ `${this.probeAttempts} probe attempt${this.probeAttempts === 1 ? '' : 's'}, `
|
|
373
|
+
+ `last failure: ${lastFailure}`);
|
|
374
|
+
}
|
|
257
375
|
clearProbeTimer() {
|
|
258
376
|
if (this.probeTimer) {
|
|
259
377
|
clearTimeout(this.probeTimer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectivity-manager.js","sourceRoot":"","sources":["../src/connectivity-manager.ts"],"names":[],"mappings":"AA+CA,OAAO,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"connectivity-manager.js","sourceRoot":"","sources":["../src/connectivity-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-august",
|
|
3
3
|
"displayName": "August",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.1
|
|
5
|
+
"version": "3.2.1",
|
|
6
6
|
"description": "The August plugin allows you to access your August & Yale device(s) from HomeKit.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "donavanbecker",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@homebridge/plugin-ui-utils": "^2.2.3",
|
|
65
|
-
"august-yale": "^1.1
|
|
65
|
+
"august-yale": "^1.2.1",
|
|
66
66
|
"rxjs": "^7.8.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>default | @homebridge-plugins/homebridge-august</title><meta name="description" content="Documentation for @homebridge-plugins/homebridge-august"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@homebridge-plugins/homebridge-august</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">default</a></li></ul><h1>Variable default</h1></div><div class="tsd-signature"><span class="tsd-kind-variable">default</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <span class="tsd-signature-type">void</span></div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/homebridge-plugins/homebridge-august/blob/
|
|
1
|
+
<!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>default | @homebridge-plugins/homebridge-august</title><meta name="description" content="Documentation for @homebridge-plugins/homebridge-august"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@homebridge-plugins/homebridge-august</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">default</a></li></ul><h1>Variable default</h1></div><div class="tsd-signature"><span class="tsd-kind-variable">default</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <span class="tsd-signature-type">void</span></div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/homebridge-plugins/homebridge-august/blob/6ea4f34cb710cbff333e71d84b048bb61d34ea64/src/index.ts#L26">index.ts:26</a></li></ul></aside></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@homebridge-plugins/homebridge-august</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-august",
|
|
3
3
|
"displayName": "August",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.1
|
|
5
|
+
"version": "3.2.1",
|
|
6
6
|
"description": "The August plugin allows you to access your August & Yale device(s) from HomeKit.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "donavanbecker",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@homebridge/plugin-ui-utils": "^2.2.3",
|
|
65
|
-
"august-yale": "^1.1
|
|
65
|
+
"august-yale": "^1.2.1",
|
|
66
66
|
"rxjs": "^7.8.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
@@ -17,16 +17,49 @@ import { ConnectivityManager, OfflineError } from '../src/connectivity-manager.j
|
|
|
17
17
|
|
|
18
18
|
// Mock the august-yale module. We need:
|
|
19
19
|
// - August: a constructor we can instantiate, with details() and destroy()
|
|
20
|
-
// -
|
|
20
|
+
// - The full exception hierarchy that classify() now uses for routing:
|
|
21
|
+
// NetworkError, TimeoutError (subclass), AbortedError, InvalidAuth.
|
|
21
22
|
vi.mock('august-yale', () => {
|
|
22
|
-
class
|
|
23
|
-
|
|
23
|
+
class YaleApiError extends Error {
|
|
24
|
+
public originalError?: Error
|
|
25
|
+
constructor(message?: string, originalError?: Error) {
|
|
24
26
|
super(message)
|
|
27
|
+
this.name = 'YaleApiError'
|
|
28
|
+
this.originalError = originalError
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
class NetworkError extends YaleApiError {
|
|
32
|
+
public code?: string
|
|
33
|
+
constructor(message?: string, originalError?: Error, code?: string) {
|
|
34
|
+
super(message, originalError)
|
|
35
|
+
this.name = 'NetworkError'
|
|
36
|
+
this.code = code
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
class TimeoutError extends NetworkError {
|
|
40
|
+
constructor(message?: string, originalError?: Error, code?: string) {
|
|
41
|
+
super(message, originalError, code)
|
|
25
42
|
this.name = 'TimeoutError'
|
|
26
43
|
}
|
|
27
44
|
}
|
|
45
|
+
class AbortedError extends YaleApiError {
|
|
46
|
+
public code?: string
|
|
47
|
+
constructor(message?: string, originalError?: Error, code?: string) {
|
|
48
|
+
super(message, originalError)
|
|
49
|
+
this.name = 'AbortedError'
|
|
50
|
+
this.code = code
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class InvalidAuth extends YaleApiError {
|
|
54
|
+
constructor(message?: string, originalError?: Error) {
|
|
55
|
+
super(message, originalError)
|
|
56
|
+
this.name = 'InvalidAuth'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
28
59
|
// Each new August() returns a unique object so tests can assert which
|
|
29
|
-
// client is being used (initial vs. rebuilt).
|
|
60
|
+
// client is being used (initial vs. rebuilt). Tests can also override
|
|
61
|
+
// the methods of the NEXT client to be built by setting fields on
|
|
62
|
+
// `nextClientOverride`. After one use the override is cleared.
|
|
30
63
|
let nextId = 0
|
|
31
64
|
// eslint-disable-next-line prefer-arrow-callback
|
|
32
65
|
const MockAugust = vi.fn().mockImplementation(function () {
|
|
@@ -38,12 +71,17 @@ vi.mock('august-yale', () => {
|
|
|
38
71
|
lock: vi.fn().mockResolvedValue(undefined),
|
|
39
72
|
unlock: vi.fn().mockResolvedValue(undefined),
|
|
40
73
|
destroy: vi.fn(),
|
|
74
|
+
resetTransport: vi.fn(),
|
|
41
75
|
end: vi.fn(),
|
|
42
76
|
}
|
|
43
77
|
})
|
|
44
78
|
return {
|
|
45
79
|
default: MockAugust,
|
|
80
|
+
YaleApiError,
|
|
81
|
+
NetworkError,
|
|
46
82
|
TimeoutError,
|
|
83
|
+
AbortedError,
|
|
84
|
+
InvalidAuth,
|
|
47
85
|
}
|
|
48
86
|
})
|
|
49
87
|
|
|
@@ -188,23 +226,57 @@ describe('ConnectivityManager', () => {
|
|
|
188
226
|
expect(m.getState()).toBe('degraded')
|
|
189
227
|
})
|
|
190
228
|
|
|
191
|
-
it('treats
|
|
229
|
+
it('treats NetworkError as network and transitions to degraded', async () => {
|
|
230
|
+
// NetworkError covers all transport-level failures from august-yale —
|
|
231
|
+
// socket reset, DNS failure, malformed response, etc. — without us
|
|
232
|
+
// having to enumerate undici's 24 error classes.
|
|
192
233
|
const m = new ConnectivityManager(makeLog(), fakeCredentials)
|
|
193
234
|
await m.init()
|
|
194
|
-
const
|
|
195
|
-
await m.execute('test', async () => { throw
|
|
235
|
+
const { NetworkError } = await import('august-yale')
|
|
236
|
+
const result = await m.execute('test', async () => { throw new NetworkError('socket hang up', undefined, 'ECONNRESET') })
|
|
237
|
+
expect(result).toBeUndefined()
|
|
196
238
|
expect(m.getState()).toBe('degraded')
|
|
197
239
|
})
|
|
198
240
|
|
|
199
|
-
it('treats
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
241
|
+
it('treats InvalidAuth as auth and rebuilds without changing state', async () => {
|
|
242
|
+
const onClientChanged = vi.fn()
|
|
243
|
+
const m = new ConnectivityManager(makeLog(), fakeCredentials, onClientChanged)
|
|
244
|
+
await m.init()
|
|
245
|
+
expect(onClientChanged).toHaveBeenCalledTimes(1)
|
|
246
|
+
const { InvalidAuth } = await import('august-yale')
|
|
247
|
+
const result = await m.execute('test', async () => { throw new InvalidAuth('session expired') })
|
|
248
|
+
expect(result).toBeUndefined()
|
|
249
|
+
await vi.runAllTimersAsync()
|
|
250
|
+
expect(onClientChanged).toHaveBeenCalledTimes(2)
|
|
251
|
+
expect(m.getState()).toBe('healthy')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('treats AbortedError as transient and does NOT change connectivity state', async () => {
|
|
255
|
+
// AbortedError reflects a request racing with our own teardown
|
|
256
|
+
// (e.g. inside rebuildClient). Treating it as 'network' would
|
|
257
|
+
// make the state machine fight itself: every rebuild produces
|
|
258
|
+
// ClientDestroyedError -> degraded -> probe -> rebuild ->
|
|
259
|
+
// ClientDestroyedError -> ...
|
|
260
|
+
const m = new ConnectivityManager(makeLog(), fakeCredentials)
|
|
261
|
+
await m.init()
|
|
262
|
+
const { AbortedError } = await import('august-yale')
|
|
263
|
+
const err = new AbortedError('client destroyed', undefined, 'UND_ERR_DESTROYED')
|
|
264
|
+
await expect(
|
|
265
|
+
m.execute('test', async () => { throw err }),
|
|
266
|
+
).rejects.toBe(err)
|
|
267
|
+
// State stays healthy — no probe scheduled.
|
|
268
|
+
expect(m.getState()).toBe('healthy')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('treats 502/503/504 as network errors (HTTP-level fallback)', async () => {
|
|
272
|
+
// Most transport failures are now wrapped upstream as NetworkError,
|
|
273
|
+
// but if august-yale ever surfaces a clean HTTP 5xx response, we
|
|
274
|
+
// still want to recognize 502/503/504 as network-class and probe.
|
|
275
|
+
const m = new ConnectivityManager(makeLog(), fakeCredentials)
|
|
276
|
+
await m.init()
|
|
277
|
+
const err = Object.assign(new Error('Bad Gateway'), { statusCode: 502 })
|
|
278
|
+
await m.execute('test', async () => { throw err })
|
|
279
|
+
expect(m.getState()).toBe('degraded')
|
|
208
280
|
})
|
|
209
281
|
})
|
|
210
282
|
|
|
@@ -213,7 +285,6 @@ describe('ConnectivityManager', () => {
|
|
|
213
285
|
const onClientChanged = vi.fn()
|
|
214
286
|
const m = new ConnectivityManager(makeLog(), fakeCredentials, onClientChanged)
|
|
215
287
|
await m.init()
|
|
216
|
-
const client = m.getClient() as any
|
|
217
288
|
expect(onClientChanged).toHaveBeenCalledTimes(1)
|
|
218
289
|
|
|
219
290
|
// Network failure: degrades and schedules probe.
|
|
@@ -221,15 +292,17 @@ describe('ConnectivityManager', () => {
|
|
|
221
292
|
await m.execute('test', async () => { throw new TimeoutError('boom') })
|
|
222
293
|
expect(m.getState()).toBe('degraded')
|
|
223
294
|
|
|
224
|
-
// Probe
|
|
225
|
-
|
|
295
|
+
// Probe calls resetTransport() on the existing client and then its
|
|
296
|
+
// .locks(). Default mock = locks resolves, so probe succeeds.
|
|
226
297
|
|
|
227
298
|
// Advance time past the first backoff slot (~5s plus jitter).
|
|
228
299
|
await vi.advanceTimersByTimeAsync(7_000)
|
|
229
300
|
|
|
230
|
-
// Probe success →
|
|
301
|
+
// Probe success → healthy. onClientChanged was called only once
|
|
302
|
+
// (at init). The resetTransport-based design does NOT swap the
|
|
303
|
+
// client instance on recovery; it just recycles its transport.
|
|
231
304
|
expect(m.getState()).toBe('healthy')
|
|
232
|
-
expect(onClientChanged).toHaveBeenCalledTimes(
|
|
305
|
+
expect(onClientChanged).toHaveBeenCalledTimes(1)
|
|
233
306
|
})
|
|
234
307
|
|
|
235
308
|
it('escalates from degraded to offline when probe fails, with longer backoff', async () => {
|
|
@@ -238,14 +311,18 @@ describe('ConnectivityManager', () => {
|
|
|
238
311
|
const client = m.getClient() as any
|
|
239
312
|
|
|
240
313
|
const { TimeoutError } = await import('august-yale')
|
|
241
|
-
await m.execute('test', async () => { throw new TimeoutError('boom') })
|
|
242
|
-
expect(m.getState()).toBe('degraded')
|
|
243
314
|
|
|
244
|
-
//
|
|
245
|
-
|
|
315
|
+
// Make the existing client's .locks() always fail. With the
|
|
316
|
+
// resetTransport-based design, probes reuse this client, so a
|
|
317
|
+
// single mockImplementation is sufficient for every probe attempt.
|
|
318
|
+
client.locks = vi.fn().mockImplementation(() => new Promise((_resolve, reject) => {
|
|
246
319
|
setTimeout(() => reject(new TimeoutError('still down')), 100)
|
|
247
320
|
}))
|
|
248
321
|
|
|
322
|
+
// Trigger degraded.
|
|
323
|
+
await m.execute('test', async () => { throw new TimeoutError('boom') })
|
|
324
|
+
expect(m.getState()).toBe('degraded')
|
|
325
|
+
|
|
249
326
|
// Advance past first backoff + probe timeout.
|
|
250
327
|
await vi.advanceTimersByTimeAsync(15_000)
|
|
251
328
|
expect(m.getState()).toBe('offline')
|
|
@@ -275,6 +352,74 @@ describe('ConnectivityManager', () => {
|
|
|
275
352
|
expect(result).toBe('recovered')
|
|
276
353
|
expect(m.getState()).toBe('healthy')
|
|
277
354
|
})
|
|
355
|
+
|
|
356
|
+
it('probes call resetTransport() on the existing client (sheds stale sockets)', async () => {
|
|
357
|
+
// Regression test for the production bug: the original
|
|
358
|
+
// implementation probed against the existing client without
|
|
359
|
+
// resetting its socket pool. The pool kept stale half-open sockets
|
|
360
|
+
// from before the outage, probes failed on those sockets, system
|
|
361
|
+
// stayed offline.
|
|
362
|
+
//
|
|
363
|
+
// The fix: probe still uses the existing client (so we keep auth
|
|
364
|
+
// state), but resetTransport() is called first to throw away the
|
|
365
|
+
// dispatcher and its stale connections. New transport, same auth.
|
|
366
|
+
const m = new ConnectivityManager(makeLog(), fakeCredentials)
|
|
367
|
+
await m.init()
|
|
368
|
+
const client = m.getClient() as any
|
|
369
|
+
|
|
370
|
+
// Sanity: the mock client has a resetTransport method.
|
|
371
|
+
expect(typeof client.resetTransport).toBe('function')
|
|
372
|
+
|
|
373
|
+
// Trigger degraded.
|
|
374
|
+
const { TimeoutError } = await import('august-yale')
|
|
375
|
+
await m.execute('test', async () => { throw new TimeoutError('boom') })
|
|
376
|
+
expect(m.getState()).toBe('degraded')
|
|
377
|
+
|
|
378
|
+
// Advance past the first backoff: probe runs, default mock has
|
|
379
|
+
// .locks() resolving, recovery completes.
|
|
380
|
+
await vi.advanceTimersByTimeAsync(7_000)
|
|
381
|
+
expect(m.getState()).toBe('healthy')
|
|
382
|
+
|
|
383
|
+
// The probe must have called resetTransport on the existing
|
|
384
|
+
// client. This is the structural assertion that locks in the fix.
|
|
385
|
+
expect(client.resetTransport).toHaveBeenCalled()
|
|
386
|
+
// The same client instance is still in place (we did NOT
|
|
387
|
+
// construct a new client — this distinguishes the resetTransport
|
|
388
|
+
// approach from the previous fresh-client approach).
|
|
389
|
+
expect(m.getClient()).toBe(client)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('failed probe leaves the existing client in place (no leak, no rotation)', async () => {
|
|
393
|
+
// With resetTransport(), failed probes don't construct a temp
|
|
394
|
+
// client, so there's nothing to leak. The existing client stays
|
|
395
|
+
// the current client throughout the offline period — only its
|
|
396
|
+
// transport gets recycled, not the August instance itself.
|
|
397
|
+
const m = new ConnectivityManager(makeLog(), fakeCredentials)
|
|
398
|
+
await m.init()
|
|
399
|
+
const client = m.getClient() as any
|
|
400
|
+
|
|
401
|
+
const { TimeoutError } = await import('august-yale')
|
|
402
|
+
|
|
403
|
+
// Make the client's .locks() always fail.
|
|
404
|
+
client.locks = vi.fn().mockImplementation(() => new Promise((_, reject) =>
|
|
405
|
+
setTimeout(() => reject(new TimeoutError('still down')), 100),
|
|
406
|
+
))
|
|
407
|
+
|
|
408
|
+
// Trigger degraded.
|
|
409
|
+
await m.execute('test', async () => { throw new TimeoutError('boom') })
|
|
410
|
+
expect(m.getState()).toBe('degraded')
|
|
411
|
+
|
|
412
|
+
// Advance past first probe → offline.
|
|
413
|
+
await vi.advanceTimersByTimeAsync(15_000)
|
|
414
|
+
expect(m.getState()).toBe('offline')
|
|
415
|
+
|
|
416
|
+
// The existing client is still the current client; nothing got
|
|
417
|
+
// destroyed or replaced.
|
|
418
|
+
expect(m.getClient()).toBe(client)
|
|
419
|
+
expect(client.destroy).not.toHaveBeenCalled()
|
|
420
|
+
// resetTransport was called for each probe attempt.
|
|
421
|
+
expect(client.resetTransport).toHaveBeenCalled()
|
|
422
|
+
})
|
|
278
423
|
})
|
|
279
424
|
|
|
280
425
|
describe('onPubNubReconnect — fast recovery', () => {
|
|
@@ -282,21 +427,23 @@ describe('ConnectivityManager', () => {
|
|
|
282
427
|
const onClientChanged = vi.fn()
|
|
283
428
|
const m = new ConnectivityManager(makeLog(), fakeCredentials, onClientChanged)
|
|
284
429
|
await m.init()
|
|
285
|
-
const client = m.getClient() as any
|
|
286
430
|
|
|
287
431
|
const { TimeoutError } = await import('august-yale')
|
|
288
432
|
await m.execute('test', async () => { throw new TimeoutError('boom') })
|
|
289
433
|
expect(m.getState()).toBe('degraded')
|
|
290
434
|
|
|
291
|
-
|
|
435
|
+
// PubNub reconnect → immediate probe. Default mock has .locks()
|
|
436
|
+
// resolving, so the probe (which calls resetTransport on the
|
|
437
|
+
// existing client and then locks()) succeeds.
|
|
292
438
|
m.onPubNubReconnect()
|
|
293
439
|
|
|
294
440
|
// Probe runs synchronously inside onPubNubReconnect's microtask;
|
|
295
441
|
// need to flush promises but no timer advance required.
|
|
296
442
|
await vi.runAllTimersAsync()
|
|
297
443
|
expect(m.getState()).toBe('healthy')
|
|
298
|
-
//
|
|
299
|
-
|
|
444
|
+
// onClientChanged was only called once (at init). Recovery via
|
|
445
|
+
// resetTransport does not swap the client instance.
|
|
446
|
+
expect(onClientChanged).toHaveBeenCalledTimes(1)
|
|
300
447
|
})
|
|
301
448
|
|
|
302
449
|
it('is a no-op when already healthy', async () => {
|
|
@@ -377,4 +524,67 @@ describe('ConnectivityManager', () => {
|
|
|
377
524
|
expect(good).toHaveBeenCalledTimes(1)
|
|
378
525
|
})
|
|
379
526
|
})
|
|
527
|
+
|
|
528
|
+
describe('offline heartbeat', () => {
|
|
529
|
+
// Helper: drive the manager into offline state and capture the log.
|
|
530
|
+
async function getOfflineWithLog() {
|
|
531
|
+
const log = makeLog()
|
|
532
|
+
const m = new ConnectivityManager(log, fakeCredentials)
|
|
533
|
+
await m.init()
|
|
534
|
+
const client = m.getClient() as any
|
|
535
|
+
|
|
536
|
+
const { TimeoutError } = await import('august-yale')
|
|
537
|
+
|
|
538
|
+
// Make every probe fail by making the existing client's .locks
|
|
539
|
+
// always reject. With the resetTransport-based design, probes
|
|
540
|
+
// reuse this client across attempts, so a single mockImplementation
|
|
541
|
+
// covers every probe in the offline period.
|
|
542
|
+
client.locks = vi.fn().mockImplementation(() => new Promise((_resolve, reject) =>
|
|
543
|
+
setTimeout(() => reject(new TimeoutError('still down')), 100),
|
|
544
|
+
))
|
|
545
|
+
|
|
546
|
+
// Trigger degraded.
|
|
547
|
+
await m.execute('test', async () => { throw new TimeoutError('boom') })
|
|
548
|
+
|
|
549
|
+
// Drive past first probe so we land in offline.
|
|
550
|
+
await vi.advanceTimersByTimeAsync(15_000)
|
|
551
|
+
return { m, log, client }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
it('emits a heartbeat after one hour of being offline', async () => {
|
|
555
|
+
const { log } = await getOfflineWithLog()
|
|
556
|
+
|
|
557
|
+
// Less than an hour: no heartbeat yet.
|
|
558
|
+
await vi.advanceTimersByTimeAsync(30 * 60_000)
|
|
559
|
+
const beforeHour = log._calls.filter(c => c.msg.includes('still offline')).length
|
|
560
|
+
expect(beforeHour).toBe(0)
|
|
561
|
+
|
|
562
|
+
// Past one hour: heartbeat fires. Probes are still failing in the
|
|
563
|
+
// background, so we filter for the heartbeat-specific message.
|
|
564
|
+
await vi.advanceTimersByTimeAsync(35 * 60_000)
|
|
565
|
+
const afterHour = log._calls.filter(c => c.msg.includes('still offline')).length
|
|
566
|
+
expect(afterHour).toBeGreaterThan(0)
|
|
567
|
+
// Heartbeat content includes attempt count and last failure.
|
|
568
|
+
const heartbeatLine = log._calls.find(c => c.msg.includes('still offline'))
|
|
569
|
+
expect(heartbeatLine?.msg).toMatch(/\d+ probe attempts?/)
|
|
570
|
+
expect(heartbeatLine?.msg).toContain('last failure')
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('stops emitting heartbeats after recovery', async () => {
|
|
574
|
+
const { m, log, client } = await getOfflineWithLog()
|
|
575
|
+
|
|
576
|
+
// Allow recovery on the next probe by making .locks resolve again.
|
|
577
|
+
client.locks = vi.fn().mockResolvedValue({})
|
|
578
|
+
|
|
579
|
+
// Skip ahead enough for a probe to fire and succeed.
|
|
580
|
+
await vi.advanceTimersByTimeAsync(60 * 1000)
|
|
581
|
+
expect(m.getState()).toBe('healthy')
|
|
582
|
+
|
|
583
|
+
const beforeIdle = log._calls.filter(c => c.msg.includes('still offline')).length
|
|
584
|
+
// Advance two more hours — should NOT see new heartbeat lines.
|
|
585
|
+
await vi.advanceTimersByTimeAsync(2 * 60 * 60_000)
|
|
586
|
+
const afterIdle = log._calls.filter(c => c.msg.includes('still offline')).length
|
|
587
|
+
expect(afterIdle).toBe(beforeIdle)
|
|
588
|
+
})
|
|
589
|
+
})
|
|
380
590
|
})
|