homebridge-plugin-utils 1.34.0 → 2.0.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 +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +128 -0
- package/dist/backpressure.js +273 -0
- package/dist/backpressure.js.map +1 -0
- package/dist/cli/index.d.ts +95 -0
- package/dist/cli/index.js +355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -257
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +70 -4
- package/dist/ffmpeg/fmp4.js +100 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +143 -91
- package/dist/ffmpeg/process.js +406 -265
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -560
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +473 -244
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -1
- package/dist/ffmpeg/settings.js +20 -2
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -148
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +597 -45
- package/dist/util.js +787 -68
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* logclient/socket.ts: AsyncDisposable Socket.IO log-stream client with reconnect, ping-liveness, and a bounded push-to-pull line stream.
|
|
4
|
+
*/
|
|
5
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
6
|
+
if (value !== null && value !== void 0) {
|
|
7
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
8
|
+
var dispose, inner;
|
|
9
|
+
if (async) {
|
|
10
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.asyncDispose];
|
|
12
|
+
}
|
|
13
|
+
if (dispose === void 0) {
|
|
14
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
15
|
+
dispose = value[Symbol.dispose];
|
|
16
|
+
if (async) inner = dispose;
|
|
17
|
+
}
|
|
18
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
19
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
20
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
21
|
+
}
|
|
22
|
+
else if (async) {
|
|
23
|
+
env.stack.push({ async: true });
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
28
|
+
return function (env) {
|
|
29
|
+
function fail(e) {
|
|
30
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
31
|
+
env.hasError = true;
|
|
32
|
+
}
|
|
33
|
+
var r, s = 0;
|
|
34
|
+
function next() {
|
|
35
|
+
while (r = env.stack.pop()) {
|
|
36
|
+
try {
|
|
37
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
38
|
+
if (r.dispose) {
|
|
39
|
+
var result = r.dispose.call(r.value);
|
|
40
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
41
|
+
}
|
|
42
|
+
else s |= 1;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
fail(e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
49
|
+
if (env.hasError) throw env.error;
|
|
50
|
+
}
|
|
51
|
+
return next();
|
|
52
|
+
};
|
|
53
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
54
|
+
var e = new Error(message);
|
|
55
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* AsyncDisposable live-log socket for the Homebridge UI log stream.
|
|
59
|
+
*
|
|
60
|
+
* {@link LogSocket} owns a single WebSocket connection to the homebridge-config-ui-x Socket.IO log namespace and surfaces the raw `stdout` text the server streams as a
|
|
61
|
+
* bounded push-to-pull `AsyncIterable<string>`. It composes the library's own primitives rather than reinventing them: {@link composeSignals} / {@link onAbort} for
|
|
62
|
+
* lifetime, {@link Watchdog} for ping liveness, {@link retry} (driving the connect phase with the log client's own dev-tuned backoff curve), and {@link HbpuAbortError}
|
|
63
|
+
* for the abort taxonomy. The wire framing is delegated entirely to the pure {@link decodeFrame} / {@link encodeFrame} codec.
|
|
64
|
+
*
|
|
65
|
+
* Lifecycle, in one pass:
|
|
66
|
+
*
|
|
67
|
+
* - A background reconnect loop runs for the socket's lifetime: `while(!signal.aborted)`, each iteration `retry()`s the CONNECT PHASE ONLY (re-acquire the token, open
|
|
68
|
+
* the WebSocket, await the Engine.IO open handshake, join the `/log` namespace, emit `tail-log`). Wrapping only the connect phase in `retry` means the exponential
|
|
69
|
+
* backoff curve RESETS after every healthy session - a socket that stays connected for hours and then drops reconnects briskly rather than at the climbed-up delay a
|
|
70
|
+
* single long-lived `retry` would have reached.
|
|
71
|
+
* - The connect-phase backoff is the log client's own jittered exponential curve: a 500 ms base (`RECONNECT_BASE_MS`) doubling each attempt and plateauing at a 5-second
|
|
72
|
+
* ceiling (`RECONNECT_CAP_MS`), deliberately snappier than `defaultRetryBackoff`'s 30-second ceiling so the tail resumes promptly after a Homebridge restart. `random`
|
|
73
|
+
* is injectable (default `Math.random`) so the jitter is deterministic in tests.
|
|
74
|
+
* - `shouldRetry` vetoes a retry the instant a permanent authentication failure surfaces (wrong password, missing OTP, noauth disabled), so a credential problem fails
|
|
75
|
+
* the socket fast with an actionable error rather than looping forever.
|
|
76
|
+
* - Once connected, each server ping (`"2"`) is answered with a pong (`"3"`) and re-arms the liveness {@link Watchdog}; if no ping arrives within
|
|
77
|
+
* `pingInterval + pingTimeout + MARGIN_MS`, the watchdog aborts the session and the loop reconnects.
|
|
78
|
+
* - A Socket.IO CONNECT_ERROR (`44/log,`) on the namespace is surfaced as a connect-phase failure - transient and retried for a refreshable credential (password/noauth),
|
|
79
|
+
* permanent and made terminal by the `shouldRetry` veto for a static token that cannot be refreshed.
|
|
80
|
+
*
|
|
81
|
+
* Teardown is idempotent and state-gated: it sends a namespace DISCONNECT (`41/log,`) only when the socket is still OPEN, ALWAYS issues `close(1000)`, clears the
|
|
82
|
+
* watchdog, and settles the parked stdout waiter exactly once. The class introduces NO `Clock` dependency - reconnect timing is exercised in tests by injecting a
|
|
83
|
+
* near-zero `backoff`, and the watchdog by `node:test` `mock.timers`.
|
|
84
|
+
*
|
|
85
|
+
* @module
|
|
86
|
+
*/
|
|
87
|
+
import { DEFAULT_PORT, JITTER_FRACTION, LOG_NAMESPACE, MARGIN_MS, PTY_COLUMNS, PTY_ROWS, RECONNECT_BASE_MS, RECONNECT_CAP_MS } from "./settings.js";
|
|
88
|
+
import { HbpuAbortError, Watchdog, composeSignals, formatErrorMessage, onAbort, retry } from "../util.js";
|
|
89
|
+
import { LOG_NAMESPACE_PATH, decodeFrame, encodeFrame } from "./frame.js";
|
|
90
|
+
import { LogAuthError, isPermanentAuthError } from "./auth.js";
|
|
91
|
+
import { LogLineSplitter } from "./parser.js";
|
|
92
|
+
import { socketUrl } from "./endpoints.js";
|
|
93
|
+
// The default high-water mark for the stdout queue: the maximum number of lines buffered between the WebSocket producer and a slow consumer before the oldest lines are
|
|
94
|
+
// dropped. A live consumer (a terminal, a grep) keeps up in practice, so this bound is a safety valve against an unbounded backlog rather than a routine limit; it is
|
|
95
|
+
// generous enough to absorb the server's ~500-line seed plus normal jitter, and overflow is logged once so a chronically-slow consumer is not silently lossy.
|
|
96
|
+
const DEFAULT_STDOUT_HIGH_WATER = 10000;
|
|
97
|
+
/**
|
|
98
|
+
* The numeric `readyState` value denoting an open WebSocket. The DOM `WebSocket.OPEN` constant is `1`; we name it as a module constant so the seam interface does not
|
|
99
|
+
* have to carry the static constant and teardown can gate on it without depending on the concrete class.
|
|
100
|
+
*
|
|
101
|
+
* @category Log Client
|
|
102
|
+
*/
|
|
103
|
+
export const WEBSOCKET_OPEN = 1;
|
|
104
|
+
/**
|
|
105
|
+
* The production {@link WebSocketFactory}: constructs the platform global `WebSocket`. A consumer holds the factory typed as the seam abstraction; a test substitutes a
|
|
106
|
+
* double. The platform `WebSocket` begins connecting on construction, which is exactly the factory contract.
|
|
107
|
+
*
|
|
108
|
+
* @param url - The connect URL.
|
|
109
|
+
*
|
|
110
|
+
* @returns A live platform `WebSocket`, typed as the {@link WebSocketLike} seam.
|
|
111
|
+
*
|
|
112
|
+
* @category Log Client
|
|
113
|
+
*/
|
|
114
|
+
export const webSocketFactory = (url) => new WebSocket(url);
|
|
115
|
+
// Add up-to-`JITTER_FRACTION` upward random jitter to a base delay. Jitter spreads reconnect attempts so a fleet of clients does not stampede the server in lockstep
|
|
116
|
+
// after a shared outage. The `random` source is injected so the jittered delay is deterministic in tests; with `random` pinned near zero the delay is effectively the
|
|
117
|
+
// base, which is how the reconnect tests drive the loop without real waits.
|
|
118
|
+
function withJitter(base, random) {
|
|
119
|
+
return Math.round(base + (base * JITTER_FRACTION * random()));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The log client's connect-phase reconnect backoff policy: a dev-tuned jittered exponential curve with a low ceiling.
|
|
123
|
+
*
|
|
124
|
+
* This is the single source of truth for the socket's default reconnect timing. The base delay is `RECONNECT_BASE_MS` (500 ms) and it doubles with each successive
|
|
125
|
+
* connect attempt, plateauing at the `RECONNECT_CAP_MS` ceiling (5 s) - deliberately snappier than `defaultRetryBackoff`'s 30-second ceiling, because a log-tailing
|
|
126
|
+
* dev tool should resume the tail promptly after the frequent Homebridge restarts a plugin developer does rather than back off to a half-minute lag. Up to
|
|
127
|
+
* `JITTER_FRACTION` of the computed base is added as upward jitter so a fleet of clients does not reconnect in lockstep after a shared outage.
|
|
128
|
+
*
|
|
129
|
+
* It is exported (rather than left inline in the constructor) so the bare schedule is a directly unit-testable function: with `random` pinned to `0` the curve yields the
|
|
130
|
+
* exact, deterministic 500, 1000, 2000, 4000, 5000, 5000, ... sequence. `retry` invokes the socket's backoff 1-indexed with the attempt about to run and never with
|
|
131
|
+
* `attempt === 1` (the first attempt runs immediately), so `attempt - 2` is the zero-based exponent for the second-and-later attempts.
|
|
132
|
+
*
|
|
133
|
+
* @param attempt - The 1-indexed connect attempt about to run. Called only for the second and later attempts (the first runs with no wait).
|
|
134
|
+
* @param random - Source of `[0, 1)` randomness for the jitter. Defaults to `Math.random`; pinned in tests for a deterministic schedule.
|
|
135
|
+
*
|
|
136
|
+
* @returns The delay, in milliseconds, to wait before running `attempt`.
|
|
137
|
+
*
|
|
138
|
+
* @category Log Client
|
|
139
|
+
*/
|
|
140
|
+
export function reconnectBackoff(attempt, random = Math.random) {
|
|
141
|
+
return withJitter(Math.min(RECONNECT_CAP_MS, RECONNECT_BASE_MS * (2 ** (attempt - 2))), random);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* AsyncDisposable client for the live Homebridge log stream over a single Socket.IO WebSocket, with automatic reconnect, ping liveness, and a bounded push-to-pull line
|
|
145
|
+
* stream.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* await using socket = new LogSocket({
|
|
151
|
+
*
|
|
152
|
+
* host: "localhost",
|
|
153
|
+
* log,
|
|
154
|
+
* refreshable: true,
|
|
155
|
+
* tokenProvider: (signal) => acquireToken(credentials, { host: "localhost", port: 8581, signal }),
|
|
156
|
+
* signal: session.signal
|
|
157
|
+
* });
|
|
158
|
+
*
|
|
159
|
+
* for await (const line of socket.stdout()) {
|
|
160
|
+
*
|
|
161
|
+
* process.stdout.write(line + "\n");
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @category Log Client
|
|
166
|
+
*/
|
|
167
|
+
export class LogSocket {
|
|
168
|
+
/**
|
|
169
|
+
* The composed abort signal representing this socket's lifetime. Aborts exactly once - when {@link LogSocket.abort} is called, the parent signal fires, or the
|
|
170
|
+
* reconnect loop gives up on a permanent failure - and `signal.reason` names the cause.
|
|
171
|
+
*/
|
|
172
|
+
signal;
|
|
173
|
+
// The private controller whose signal is composed into `this.signal`. Owned internally so the only abort verb is `abort()`.
|
|
174
|
+
#controller;
|
|
175
|
+
#backoff;
|
|
176
|
+
#host;
|
|
177
|
+
#log;
|
|
178
|
+
#port;
|
|
179
|
+
#random;
|
|
180
|
+
#refreshable;
|
|
181
|
+
#stdoutHighWater;
|
|
182
|
+
#tls;
|
|
183
|
+
#tokenProvider;
|
|
184
|
+
#webSocketFactory;
|
|
185
|
+
// The bounded queue of raw stdout lines staged for the `stdout()` consumer, plus the single parked waiter the consumer blocks on when the queue is empty. The class is
|
|
186
|
+
// single-consumer by design, mirroring `Mp4SegmentAssembler`: one parked-waiter slot is sufficient.
|
|
187
|
+
#stdoutQueue = [];
|
|
188
|
+
#stdoutWaiter;
|
|
189
|
+
// The number of lines dropped because the stdout queue hit its high-water mark, and a one-time flag so a chronically-slow consumer is warned exactly once rather than
|
|
190
|
+
// on every drop. Tracking the count lets the single warning report the magnitude of the loss.
|
|
191
|
+
#droppedLines = 0;
|
|
192
|
+
#overflowLogged = false;
|
|
193
|
+
// The background reconnect loop's promise, retained so `[Symbol.asyncDispose]` can await its completion before returning.
|
|
194
|
+
#loopTask;
|
|
195
|
+
// The current live session, or `undefined` between sessions / after teardown. Held so teardown can close the live WebSocket and so the message handler can address the
|
|
196
|
+
// correct connection's `send`.
|
|
197
|
+
#session;
|
|
198
|
+
/**
|
|
199
|
+
* Construct and start a new live-log socket. The reconnect loop starts synchronously as part of construction: by the time the constructor returns, the first connect
|
|
200
|
+
* attempt is already in flight (unless the signal was pre-aborted, in which case the socket tears down immediately).
|
|
201
|
+
*
|
|
202
|
+
* @param init - Required init options. See {@link LogSocketInit}.
|
|
203
|
+
*/
|
|
204
|
+
constructor(init) {
|
|
205
|
+
this.#host = init.host;
|
|
206
|
+
this.#log = init.log;
|
|
207
|
+
this.#port = init.port ?? DEFAULT_PORT;
|
|
208
|
+
this.#random = init.random ?? Math.random;
|
|
209
|
+
this.#refreshable = init.refreshable;
|
|
210
|
+
this.#stdoutHighWater = init.stdoutHighWater ?? DEFAULT_STDOUT_HIGH_WATER;
|
|
211
|
+
this.#tls = init.tls ?? false;
|
|
212
|
+
this.#tokenProvider = init.tokenProvider;
|
|
213
|
+
this.#webSocketFactory = init.webSocketFactory ?? webSocketFactory;
|
|
214
|
+
// The connect-phase backoff. Defaults to the log client's own jittered exponential curve via {@link reconnectBackoff} - a 500 ms base doubling each attempt and
|
|
215
|
+
// plateauing at a 5-second ceiling, deliberately snappier than `defaultRetryBackoff`'s 30-second ceiling because a dev tool should resume the tail promptly after the
|
|
216
|
+
// frequent Homebridge restarts a plugin developer does. A caller (a test) may inject a near-zero policy to drive the reconnect loop without real waits. Resolved once
|
|
217
|
+
// here, closing over the socket's injected `random`, so the hot reconnect path reads a single field rather than re-deriving the default each attempt.
|
|
218
|
+
this.#backoff = init.backoff ?? ((attempt) => reconnectBackoff(attempt, this.#random));
|
|
219
|
+
this.#controller = new AbortController();
|
|
220
|
+
this.signal = composeSignals(init.signal, this.#controller.signal);
|
|
221
|
+
// Single teardown convergence point. `onAbort` registers the one-shot teardown for the normal abort path AND runs it inline when the signal is already aborted at
|
|
222
|
+
// construction time (a pre-aborted parent), so the stdout waiter is settled and the live connection closed regardless of which path fired.
|
|
223
|
+
onAbort(this.signal, () => this.#teardown());
|
|
224
|
+
if (this.signal.aborted) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Start the background reconnect loop. The returned promise is retained so disposal can await its completion; the loop is written to always resolve (it classifies
|
|
228
|
+
// its own faults), so no `markHandled` wrapper is needed.
|
|
229
|
+
this.#loopTask = this.#runLoop();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Abort the socket and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged.
|
|
233
|
+
*
|
|
234
|
+
* Safe to call more than once: the underlying signal aborts only once, so subsequent calls are no-ops.
|
|
235
|
+
*
|
|
236
|
+
* @param reason - Optional abort reason. Typically an {@link HbpuAbortError}.
|
|
237
|
+
*/
|
|
238
|
+
abort(reason) {
|
|
239
|
+
if (this.aborted) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.#controller.abort(reason ?? new HbpuAbortError("shutdown"));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* `AsyncDisposable` implementation. Aborts the socket (defaulting to `"shutdown"`) and awaits the reconnect loop's completion before returning, so callers using
|
|
246
|
+
* `await using` are guaranteed the connection is closed and the loop has unwound by the time the block exits.
|
|
247
|
+
*
|
|
248
|
+
* @returns A promise that resolves once the reconnect loop has fully exited.
|
|
249
|
+
*/
|
|
250
|
+
async [Symbol.asyncDispose]() {
|
|
251
|
+
this.abort();
|
|
252
|
+
if (this.#loopTask) {
|
|
253
|
+
// The loop is written to always resolve; the catch is belt-and-suspenders so disposal never surfaces a cleanup-side failure the caller cannot react to.
|
|
254
|
+
await this.#loopTask.catch(() => { });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* `true` once `this.signal` has aborted. Derived from the signal; no independent state.
|
|
259
|
+
*/
|
|
260
|
+
get aborted() {
|
|
261
|
+
return this.signal.aborted;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The number of stdout lines dropped so far because the consumer fell behind the high-water mark. Zero in steady state; non-zero only after the bounded queue
|
|
265
|
+
* overflowed, which is also logged once.
|
|
266
|
+
*/
|
|
267
|
+
get droppedLines() {
|
|
268
|
+
return this.#droppedLines;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The bounded push-to-pull stream of raw log lines (ANSI intact, terminators removed) the server streams over the log namespace's `stdout` events.
|
|
272
|
+
*
|
|
273
|
+
* The server delivers `stdout` as raw text chunks whose boundaries do not align with log lines; the socket runs each chunk through a per-session
|
|
274
|
+
* {@link LogLineSplitter}, yields complete lines here, and flushes it on each session's close so the final line is never stranded. Mirroring
|
|
275
|
+
* `Mp4SegmentAssembler.segments`, a bounded queue decouples the WebSocket producer from this consumer, and a single parked waiter blocks the consumer when the queue is
|
|
276
|
+
* empty until a line is pushed or the socket aborts. The queue survives reconnects - the same iterable keeps yielding across a drop-and-reconnect - so a consumer
|
|
277
|
+
* iterates it once for the whole socket lifetime. The stream terminates (returns) when the socket aborts; the queue is drained before it returns, so a line already
|
|
278
|
+
* staged before teardown is never lost.
|
|
279
|
+
*
|
|
280
|
+
* **Single-consumer only.** The parked-waiter slot is single-writer; iterating `stdout()` concurrently from two consumers is unsupported.
|
|
281
|
+
*
|
|
282
|
+
* @returns An async generator yielding raw log lines in stream order.
|
|
283
|
+
*/
|
|
284
|
+
async *stdout() {
|
|
285
|
+
for (;;) {
|
|
286
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
287
|
+
try {
|
|
288
|
+
// Swap-drain the queue: take whatever the producer has staged, leave a fresh empty array for it to push into, then yield the snapshot. Draining unconditionally
|
|
289
|
+
// before the abort check preserves the "no staged line lost on teardown" invariant.
|
|
290
|
+
while (this.#stdoutQueue.length > 0) {
|
|
291
|
+
const drained = this.#stdoutQueue;
|
|
292
|
+
this.#stdoutQueue = [];
|
|
293
|
+
for (const line of drained) {
|
|
294
|
+
yield line;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (this.signal.aborted) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Park until a line is pushed (producer resolves the waiter) or the socket aborts (teardown resolves it). A per-iteration resolver keeps the waiter always fresh.
|
|
301
|
+
const waiter = Promise.withResolvers();
|
|
302
|
+
this.#stdoutWaiter = waiter;
|
|
303
|
+
// `onAbort` unifies registration, the pre-aborted-signal pitfall, and one-shot `{ once: true }` semantics; the disposer is handed to `using` so the listener is
|
|
304
|
+
// removed on every scope-exit path. Matches the parked-wait shape `Mp4SegmentAssembler` uses.
|
|
305
|
+
const _abortRegistration = __addDisposableResource(env_1, onAbort(this.signal, () => waiter.resolve()), false);
|
|
306
|
+
try {
|
|
307
|
+
// eslint-disable-next-line no-await-in-loop
|
|
308
|
+
await waiter.promise;
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
this.#stdoutWaiter = undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (e_1) {
|
|
315
|
+
env_1.error = e_1;
|
|
316
|
+
env_1.hasError = true;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
__disposeResources(env_1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// The background reconnect loop. Runs for the socket's lifetime: each iteration `retry()`s the connect phase to obtain a live session, then streams until the session
|
|
324
|
+
// ends. A clean socket-level abort exits the loop; any other session end loops again, and because only the connect phase is wrapped in `retry`, the next attempt
|
|
325
|
+
// starts from a fresh (reset) backoff curve.
|
|
326
|
+
async #runLoop() {
|
|
327
|
+
while (!this.signal.aborted) {
|
|
328
|
+
let session;
|
|
329
|
+
try {
|
|
330
|
+
// Connect phase only. `retry` owns the backoff and the permanent-failure veto; `Infinity` attempts means the loop reconnects indefinitely until the socket is
|
|
331
|
+
// aborted or a permanent failure is vetoed. The default backoff is the log client's own dev-tuned exponential curve (a 500 ms base doubling each attempt and
|
|
332
|
+
// capped at a 5-second ceiling, snappier than `defaultRetryBackoff`'s 30-second ceiling) with up-to-`JITTER_FRACTION` upward jitter layered on, so a fleet of
|
|
333
|
+
// clients does not stampede the server in lockstep after a shared outage. Because only the connect phase is wrapped, the backoff resets every time this returns -
|
|
334
|
+
// a socket that stayed healthy for hours reconnects briskly rather than at a climbed-up delay.
|
|
335
|
+
// eslint-disable-next-line no-await-in-loop
|
|
336
|
+
session = await retry((attemptSignal) => this.#connect(attemptSignal), {
|
|
337
|
+
attempts: Infinity,
|
|
338
|
+
backoff: this.#backoff,
|
|
339
|
+
shouldRetry: (error) => !isPermanentAuthError(error),
|
|
340
|
+
signal: this.signal
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
// The connect phase exhausted its retries or hit a permanent failure (a vetoed auth error), or the socket was aborted mid-connect. When the socket is already
|
|
345
|
+
// aborted, this is orderly teardown - stay quiet and exit. Otherwise classify the connection as permanently failed, abort the socket so `stdout()` terminates,
|
|
346
|
+
// and surface an actionable error. The signal's aborted state changes asynchronously while `await retry(...)` is in flight, so the loop-condition narrowing the
|
|
347
|
+
// compiler applies here does not reflect the runtime value.
|
|
348
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
349
|
+
if (this.signal.aborted) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.#log.error("Unable to connect to the Homebridge log stream: %s.", formatErrorMessage(error));
|
|
353
|
+
this.#controller.abort(new HbpuAbortError("failed", { cause: error }));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Stream until the session ends (peer close, error, watchdog timeout, or socket abort). When it returns, loop to reconnect unless the socket itself was aborted.
|
|
357
|
+
// eslint-disable-next-line no-await-in-loop
|
|
358
|
+
await this.#stream(session);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Connect phase. Re-acquires a fresh token, opens the WebSocket, awaits the Engine.IO open handshake (capturing its ping cadence), joins the `/log` namespace, and
|
|
362
|
+
// emits `tail-log`. Resolves with the live session once `tail-log` has been emitted; rejects on any handshake fault (WebSocket error, CONNECT_ERROR, peer close, or
|
|
363
|
+
// attempt abort), so `retry` governs the backoff. A per-attempt controller composed under the attempt signal ensures a failed attempt's WebSocket and listeners are
|
|
364
|
+
// torn down before the next attempt opens a new one; on success that same controller becomes the session controller, so the streaming phase shares the teardown path.
|
|
365
|
+
async #connect(attemptSignal) {
|
|
366
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
367
|
+
try {
|
|
368
|
+
const token = await this.#tokenProvider(attemptSignal);
|
|
369
|
+
const url = socketUrl({ host: this.#host, port: this.#port, tls: this.#tls, token });
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
const composed = composeSignals(attemptSignal, controller.signal);
|
|
372
|
+
const ws = this.#webSocketFactory(url);
|
|
373
|
+
// Capture the Engine.IO open handshake's ping cadence so the streaming phase can size the liveness watchdog from the server's real cadence rather than a guess.
|
|
374
|
+
let pingInterval = 0;
|
|
375
|
+
let pingTimeout = 0;
|
|
376
|
+
// The connect handshake is a one-shot promise resolved by the message handler once `tail-log` has been emitted, or rejected by an error / close / CONNECT_ERROR /
|
|
377
|
+
// abort.
|
|
378
|
+
const { promise: connected, resolve, reject } = Promise.withResolvers();
|
|
379
|
+
// Tear down this attempt's WebSocket when the composed signal aborts (attempt abort or our own controller abort), and reject the connect promise so a pending
|
|
380
|
+
// handshake unwinds. The `using` disposer removes the listener when this method's scope exits.
|
|
381
|
+
const _abortRegistration = __addDisposableResource(env_2, onAbort(composed, () => {
|
|
382
|
+
reject(composed.reason);
|
|
383
|
+
this.#closeWebSocket(ws);
|
|
384
|
+
}), false);
|
|
385
|
+
ws.addEventListener("error", (event) => {
|
|
386
|
+
reject(new HbpuAbortError("failed", { cause: this.#describeWsError(event) }));
|
|
387
|
+
});
|
|
388
|
+
ws.addEventListener("close", (event) => {
|
|
389
|
+
// A close during the connect handshake is a connect-phase failure (the server hung up before we finished joining the namespace); reject so `retry` backs off.
|
|
390
|
+
reject(new HbpuAbortError("closed", { cause: { code: event.code } }));
|
|
391
|
+
});
|
|
392
|
+
ws.addEventListener("message", (event) => {
|
|
393
|
+
if (typeof event.data !== "string") {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const decoded = decodeFrame(event.data);
|
|
397
|
+
switch (decoded.kind) {
|
|
398
|
+
case "open": {
|
|
399
|
+
// The Engine.IO handshake. Record the ping cadence and join the `/log` namespace; the server replies with a namespace CONNECT acknowledgement (or a
|
|
400
|
+
// CONNECT_ERROR).
|
|
401
|
+
pingInterval = decoded.pingInterval;
|
|
402
|
+
pingTimeout = decoded.pingTimeout;
|
|
403
|
+
ws.send(encodeFrame({ kind: "connect", namespace: LOG_NAMESPACE }));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
case "namespaceConnect": {
|
|
407
|
+
// The namespace is joined. Request the tail with the advertised PTY geometry, then consider the handshake complete - from here the server streams `stdout`.
|
|
408
|
+
ws.send(encodeFrame({ args: { cols: PTY_COLUMNS, rows: PTY_ROWS }, event: "tail-log", kind: "event", namespace: LOG_NAMESPACE }));
|
|
409
|
+
resolve();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
case "namespaceError": {
|
|
413
|
+
// The server rejected the namespace join (a Socket.IO CONNECT_ERROR, commonly an auth failure on a stale or invalid token). How we classify it depends on
|
|
414
|
+
// whether the backing credential can mint a fresh token on the next attempt. With a refreshable credential (password/noauth) the next reconnect re-auths from
|
|
415
|
+
// the stored credentials, so this is a transient connect-phase failure and we surface it as a plain `HbpuAbortError("failed")` that `retry` governs. With
|
|
416
|
+
// a static `token` credential there is nothing to refresh, so retrying the same rejected token would loop forever; we raise a PERMANENT `LogAuthError` instead,
|
|
417
|
+
// so the connect-phase `shouldRetry` veto (`!isPermanentAuthError`) makes the socket fail fast with an actionable error rather than spinning on a doomed token.
|
|
418
|
+
if (this.#refreshable) {
|
|
419
|
+
reject(new HbpuAbortError("failed", { cause: { reason: decoded.reason } }));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
reject(new LogAuthError("Authentication rejected; the token cannot be refreshed - provide a fresh --token or use --user/--pass.", { cause: { reason: decoded.reason }, kind: "permanent" }));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
default: {
|
|
426
|
+
// ping/pong/message/unknown during the handshake are not part of the connect sequence; ignore them. The server does not stream `stdout` until `tail-log` is
|
|
427
|
+
// emitted, so a `message` before that is not expected, but ignoring it is harmless.
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
// Await the handshake. On any failure path - a WebSocket error, a CONNECT_ERROR, a peer close mid-handshake, or an attempt abort - close this attempt's WebSocket so
|
|
433
|
+
// a failed attempt never leaks its connection before `retry` opens the next one. The `using` abort registration handles the abort path's close; this catch covers
|
|
434
|
+
// the listener-driven rejections (error / close / CONNECT_ERROR) where the composed signal did not abort.
|
|
435
|
+
try {
|
|
436
|
+
await connected;
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
this.#closeWebSocket(ws);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
return { closed: false, controller, pingInterval, pingTimeout, ws };
|
|
443
|
+
}
|
|
444
|
+
catch (e_2) {
|
|
445
|
+
env_2.error = e_2;
|
|
446
|
+
env_2.hasError = true;
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
__disposeResources(env_2);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Streaming phase. The session is connected; split `stdout` chunks into lines and route them into the bounded queue, answer pings with pongs and re-arm the liveness
|
|
453
|
+
// watchdog, and end the session on close / error / watchdog timeout. Resolves when the session's signal aborts - which the outer loop reads to decide whether to
|
|
454
|
+
// reconnect. The watchdog window is sized from the server's advertised ping cadence plus a margin; the watchdog uses real timers (no `Clock` seam), so tests drive it
|
|
455
|
+
// with `mock.timers`.
|
|
456
|
+
async #stream(session) {
|
|
457
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
458
|
+
try {
|
|
459
|
+
this.#session = session;
|
|
460
|
+
const composed = composeSignals(this.signal, session.controller.signal);
|
|
461
|
+
// The per-session line splitter. The server delivers `stdout` events as raw text chunks whose boundaries do not align with log lines; the splitter reassembles lines
|
|
462
|
+
// across chunk boundaries and normalizes the mixed newline conventions. It is per-session (a fresh connection starts with a clean carry) and is flushed on session
|
|
463
|
+
// close so the final line - which the splitter withholds when a chunk ends on a lone line-feed pending a possible cross-chunk pair - surfaces rather than stranding.
|
|
464
|
+
const splitter = new LogLineSplitter();
|
|
465
|
+
// The liveness watchdog, sized from the server's advertised ping cadence captured during the connect handshake: one ping interval plus the ping timeout plus a fixed
|
|
466
|
+
// margin, so a single late ping does not trip it. A zero/absent cadence (malformed handshake) falls back to the margin alone. Each inbound ping re-arms it; if the
|
|
467
|
+
// window lapses with no ping, it aborts the SESSION (not the whole socket), so the outer loop reconnects. The watchdog uses real timers (no `Clock` seam), so tests
|
|
468
|
+
// drive its firing with `node:test` `mock.timers`.
|
|
469
|
+
const windowMs = ((session.pingInterval > 0) ? session.pingInterval : 0) + ((session.pingTimeout > 0) ? session.pingTimeout : 0) + MARGIN_MS;
|
|
470
|
+
const watchdog = new Watchdog({ onFire: () => session.controller.abort(new HbpuAbortError("timeout")), signal: composed, timeoutMs: windowMs });
|
|
471
|
+
// A single resolver settled when the session ends so this method awaits the session lifetime without a busy loop. `onAbort` settles it on session abort; the message
|
|
472
|
+
// and close handlers drive the session controller, which aborts the composed signal, which fires this.
|
|
473
|
+
const { promise: ended, resolve: endSession } = Promise.withResolvers();
|
|
474
|
+
const _watchdog = __addDisposableResource(env_3, watchdog, false);
|
|
475
|
+
const _abortRegistration = __addDisposableResource(env_3, onAbort(composed, () => endSession()), false);
|
|
476
|
+
// Arm the watchdog immediately so a session that never receives a single ping (a silently-wedged connection) still trips the liveness timer; each inbound ping
|
|
477
|
+
// re-arms it from here.
|
|
478
|
+
watchdog.arm();
|
|
479
|
+
session.ws.addEventListener("message", (event) => {
|
|
480
|
+
this.#handleStreamMessage(session, watchdog, splitter, event.data);
|
|
481
|
+
});
|
|
482
|
+
session.ws.addEventListener("close", (event) => {
|
|
483
|
+
// The peer closed the connection. End the session with `"closed"` so the outer loop reconnects (the socket-level signal is untouched, so this is a session end,
|
|
484
|
+
// not a socket end).
|
|
485
|
+
if (!session.controller.signal.aborted) {
|
|
486
|
+
session.controller.abort(new HbpuAbortError("closed", { cause: { code: event.code } }));
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
session.ws.addEventListener("error", (event) => {
|
|
490
|
+
if (!session.controller.signal.aborted) {
|
|
491
|
+
session.controller.abort(new HbpuAbortError("failed", { cause: this.#describeWsError(event) }));
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
await ended;
|
|
495
|
+
// Flush the splitter on session end so the final line - withheld by the splitter when the last chunk ended on a lone line-feed pending a possible cross-chunk pair -
|
|
496
|
+
// surfaces rather than stranding. This is the load-bearing flush the wiring note calls for; mid-stream the next chunk drains the carry, but at session close no next
|
|
497
|
+
// chunk arrives.
|
|
498
|
+
for (const line of splitter.flush()) {
|
|
499
|
+
this.#enqueueStdout(line);
|
|
500
|
+
}
|
|
501
|
+
// Tear down the session's WebSocket on the way out: send the namespace DISCONNECT only when still OPEN, then always close(1000). Clearing `#session` prevents a stale
|
|
502
|
+
// reference from leaking into the next iteration's teardown.
|
|
503
|
+
this.#closeSession(session);
|
|
504
|
+
if (this.#session === session) {
|
|
505
|
+
this.#session = undefined;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (e_3) {
|
|
509
|
+
env_3.error = e_3;
|
|
510
|
+
env_3.hasError = true;
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
__disposeResources(env_3);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Handle a message in the streaming phase. A ping is answered with a pong and re-arms the liveness watchdog (each ping is the server's heartbeat). A `stdout` message's
|
|
517
|
+
// payload is a raw log-text chunk; it is fed through the per-session splitter and every complete line it yields is enqueued for the consumer (the server's chunk
|
|
518
|
+
// boundaries do not align with log lines, so splitting must happen here, before the bounded queue).
|
|
519
|
+
#handleStreamMessage(session, watchdog, splitter, data) {
|
|
520
|
+
if (typeof data !== "string") {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const event = decodeFrame(data);
|
|
524
|
+
switch (event.kind) {
|
|
525
|
+
case "ping": {
|
|
526
|
+
// Answer the heartbeat and re-arm the liveness watchdog. Re-arming on each ping is the liveness contract: as long as pings keep arriving, the watchdog never
|
|
527
|
+
// fires.
|
|
528
|
+
session.ws.send(encodeFrame({ kind: "pong" }));
|
|
529
|
+
watchdog.arm();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
case "message": {
|
|
533
|
+
// A namespace event. The log stream's only event is `stdout`, whose payload is a raw text chunk; split it into lines and enqueue each for the consumer. Any other
|
|
534
|
+
// event name is ignored.
|
|
535
|
+
if ((event.event === "stdout") && (typeof event.payload === "string")) {
|
|
536
|
+
for (const line of splitter.consume(event.payload)) {
|
|
537
|
+
this.#enqueueStdout(line);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
default: {
|
|
543
|
+
// open / pong / namespaceConnect / namespaceError / unknown in the streaming phase are not actionable here; ignore them.
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Enqueue a complete log line for the `stdout()` consumer, enforcing the high-water bound. When the queue is at capacity the OLDEST line is dropped (a live tail cares
|
|
549
|
+
// about recent output, not stale backlog), the drop counter is incremented, and a single warning is logged so a chronically-slow consumer learns it is lossy without
|
|
550
|
+
// the log being flooded. Resolving the parked waiter wakes a blocked consumer.
|
|
551
|
+
#enqueueStdout(line) {
|
|
552
|
+
if (this.#stdoutQueue.length >= this.#stdoutHighWater) {
|
|
553
|
+
this.#stdoutQueue.shift();
|
|
554
|
+
this.#droppedLines++;
|
|
555
|
+
if (!this.#overflowLogged) {
|
|
556
|
+
this.#overflowLogged = true;
|
|
557
|
+
this.#log.warn("The Homebridge log stream is producing output faster than it is being consumed; the oldest buffered lines are being dropped.");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
this.#stdoutQueue.push(line);
|
|
561
|
+
this.#stdoutWaiter?.resolve();
|
|
562
|
+
}
|
|
563
|
+
// Render a WebSocket `"error"` event for diagnostics. The DOM error event carries no structured detail; a Node `ws` error event may carry an `error` field. We surface
|
|
564
|
+
// whichever is present, falling back to a generic label, so the abort cause is at least minimally informative.
|
|
565
|
+
#describeWsError(event) {
|
|
566
|
+
if ((typeof event === "object") && (event !== null) && ("error" in event)) {
|
|
567
|
+
return event.error;
|
|
568
|
+
}
|
|
569
|
+
return new Error("WebSocket connection error.");
|
|
570
|
+
}
|
|
571
|
+
// Close a session's WebSocket cleanly: send the namespace DISCONNECT (`41/log,`) only when the connection is still OPEN, then ALWAYS issue `close(1000)`. Idempotent
|
|
572
|
+
// via the session's one-shot `closed` latch - both the streaming phase's post-end cleanup and the socket-level teardown can reach the same live session, and the latch
|
|
573
|
+
// ensures the DISCONNECT-and-close sequence runs exactly once. The readyState gate avoids a send on a half-closed connection; the unconditional close is the teardown
|
|
574
|
+
// invariant.
|
|
575
|
+
#closeSession(session) {
|
|
576
|
+
if (session.closed) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
session.closed = true;
|
|
580
|
+
if (session.ws.readyState === WEBSOCKET_OPEN) {
|
|
581
|
+
try {
|
|
582
|
+
session.ws.send("41" + LOG_NAMESPACE_PATH + ",");
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// A send on a connection that closed between the readyState check and here is harmless; the close below is what matters.
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
this.#closeWebSocket(session.ws);
|
|
589
|
+
}
|
|
590
|
+
// Issue the orderly WebSocket close, swallowing any throw (closing an already-closed or still-connecting socket can throw on some platforms). `close(1000)` is the
|
|
591
|
+
// normal-closure code; teardown always sends it so the server sees a clean disconnect.
|
|
592
|
+
#closeWebSocket(ws) {
|
|
593
|
+
try {
|
|
594
|
+
ws.close(1000);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// Closing a socket that is already closed (or in a state where close throws) is a no-op for our purposes.
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// Single teardown convergence point, fired exactly once when `this.signal` aborts. Closes the live session's WebSocket (sending the namespace DISCONNECT if OPEN, then
|
|
601
|
+
// always close(1000)) and settles the parked stdout waiter so the consumer's `stdout()` generator wakes, drains the queue, and returns. The reconnect loop observes the
|
|
602
|
+
// aborted signal and exits on its next iteration boundary; the watchdog self-cleans through its own composed-signal listener.
|
|
603
|
+
#teardown() {
|
|
604
|
+
if (this.#session !== undefined) {
|
|
605
|
+
this.#closeSession(this.#session);
|
|
606
|
+
this.#session = undefined;
|
|
607
|
+
}
|
|
608
|
+
this.#stdoutWaiter?.resolve();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* The production {@link LogSocketFactory}: builds the concrete WebSocket-backed {@link LogSocket}. A client holds the factory typed as the seam abstraction; a test
|
|
613
|
+
* substitutes a fake factory. `create` is exactly the {@link LogSocket} constructor call, so wiring construction through this seam is behavior-neutral.
|
|
614
|
+
*
|
|
615
|
+
* @category Log Client
|
|
616
|
+
*/
|
|
617
|
+
export const logSocketFactory = {
|
|
618
|
+
create: (init) => new LogSocket(init)
|
|
619
|
+
};
|
|
620
|
+
//# sourceMappingURL=socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../../src/logclient/socket.ts"],"names":[],"mappings":"AAAA;;;GAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACpJ,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,cAAc,EAAE,kBAAkB,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC1G,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,wKAAwK;AACxK,sKAAsK;AACtK,8JAA8J;AAC9J,MAAM,yBAAyB,GAAG,KAAK,CAAC;AAgDxC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAqB,CAAC,GAAW,EAAiB,EAAE,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC;AAuCrG,qKAAqK;AACrK,sKAAsK;AACtK,4EAA4E;AAC5E,SAAS,UAAU,CAAC,IAAY,EAAE,MAAoB;IAEpD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,GAAG,eAAe,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,SAAuB,IAAI,CAAC,MAAM;IAElF,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAClG,CAAC;AA2ED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,SAAS;IAEpB;;;OAGG;IACa,MAAM,CAAc;IAEpC,4HAA4H;IACnH,WAAW,CAAkB;IAE7B,QAAQ,CAA8B;IACtC,KAAK,CAAS;IACd,IAAI,CAA0B;IAC9B,KAAK,CAAS;IACd,OAAO,CAAe;IACtB,YAAY,CAAU;IACtB,gBAAgB,CAAS;IACzB,IAAI,CAAU;IACd,cAAc,CAAgB;IAC9B,iBAAiB,CAAmB;IAE7C,uKAAuK;IACvK,oGAAoG;IACpG,YAAY,GAAa,EAAE,CAAC;IAC5B,aAAa,CAAyC;IAEtD,sKAAsK;IACtK,8FAA8F;IAC9F,aAAa,GAAG,CAAC,CAAC;IAClB,eAAe,GAAG,KAAK,CAAC;IAExB,0HAA0H;IAC1H,SAAS,CAA4B;IAErC,uKAAuK;IACvK,+BAA+B;IAC/B,QAAQ,CAAsB;IAE9B;;;;;OAKG;IACH,YAAmB,IAAmB;QAEpC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,IAAI,YAAY,CAAC;QACvC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;QAC1C,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC;QACrC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,eAAe,IAAI,yBAAyB,CAAC;QAC1E,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,IAAI,KAAK,CAAC;QAC9B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,aAAa,CAAC;QACzC,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,IAAI,gBAAgB,CAAC;QAEnE,gKAAgK;QAChK,sKAAsK;QACtK,sKAAsK;QACtK,sJAAsJ;QACtJ,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,OAAe,EAAU,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAEvG,IAAI,CAAC,WAAW,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAEnE,kKAAkK;QAClK,2IAA2I;QAC3I,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAE7C,IAAG,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAEvB,OAAO;QACT,CAAC;QAED,mKAAmK;QACnK,0DAA0D;QAC1D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;IACnC,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,MAAgB;QAE3B,IAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YAEhB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IACnE,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QAEhC,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,IAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAElB,wJAAwJ;YACxJ,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,GAAmC,CAAC,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IAED;;OAEG;IACH,IAAW,OAAO;QAEhB,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACH,IAAW,YAAY;QAErB,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;;;OAaG;IACI,KAAK,CAAC,CAAC,MAAM;QAElB,SAAQ,CAAC;;;gBAEP,gKAAgK;gBAChK,oFAAoF;gBACpF,OAAM,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAEnC,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC;oBAElC,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;oBAEvB,KAAI,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;wBAE1B,MAAM,IAAI,CAAC;oBACb,CAAC;gBACH,CAAC;gBAED,IAAG,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBAEvB,OAAO;gBACT,CAAC;gBAED,kKAAkK;gBAClK,MAAM,MAAM,GAA+B,OAAO,CAAC,aAAa,EAAE,CAAC;gBAEnE,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC;gBAE5B,gKAAgK;gBAChK,8FAA8F;gBAC9F,MAAM,kBAAkB,kCAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,QAAA,CAAC;gBAExE,IAAI,CAAC;oBAEH,4CAA4C;oBAC5C,MAAM,MAAM,CAAC,OAAO,CAAC;gBACvB,CAAC;wBAAS,CAAC;oBAET,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;gBACjC,CAAC;;;;;;;;;SACF;IACH,CAAC;IAED,sKAAsK;IACtK,iKAAiK;IACjK,6CAA6C;IAC7C,KAAK,CAAC,QAAQ;QAEZ,OAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAE3B,IAAI,OAAgB,CAAC;YAErB,IAAI,CAAC;gBAEH,8JAA8J;gBAC9J,6JAA6J;gBAC7J,8JAA8J;gBAC9J,kKAAkK;gBAClK,+FAA+F;gBAC/F,4CAA4C;gBAC5C,OAAO,GAAG,MAAM,KAAK,CAAC,CAAC,aAAa,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;oBAErE,QAAQ,EAAE,QAAQ;oBAClB,OAAO,EAAE,IAAI,CAAC,QAAQ;oBACtB,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,oBAAoB,CAAC,KAAK,CAAC;oBACpD,MAAM,EAAE,IAAI,CAAC,MAAM;iBACpB,CAAC,CAAC;YACL,CAAC;YAAC,OAAM,KAAc,EAAE,CAAC;gBAEvB,8JAA8J;gBAC9J,+JAA+J;gBAC/J,gKAAgK;gBAChK,4DAA4D;gBAC5D,uEAAuE;gBACvE,IAAG,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBAEvB,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,qDAAqD,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;gBAClG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;gBAEvE,OAAO;YACT,CAAC;YAED,iKAAiK;YACjK,4CAA4C;YAC5C,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,mKAAmK;IACnK,oKAAoK;IACpK,oKAAoK;IACpK,sKAAsK;IACtK,KAAK,CAAC,QAAQ,CAAC,aAA0B;;;YAEvC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;YACvD,MAAM,GAAG,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAErF,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,cAAc,CAAC,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;YAClE,MAAM,EAAE,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;YAEvC,gKAAgK;YAChK,IAAI,YAAY,GAAG,CAAC,CAAC;YACrB,IAAI,WAAW,GAAG,CAAC,CAAC;YAEpB,kKAAkK;YAClK,SAAS;YACT,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,GAA+B,OAAO,CAAC,aAAa,EAAE,CAAC;YAEpG,8JAA8J;YAC9J,+FAA+F;YAC/F,MAAM,kBAAkB,kCAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE;gBAEhD,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACxB,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;YAC3B,CAAC,CAAC,QAAA,CAAC;YAEH,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAc,EAAE,EAAE;gBAE9C,MAAM,CAAC,IAAI,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;YAChF,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAgC,EAAE,EAAE;gBAEhE,8JAA8J;gBAC9J,MAAM,CAAC,IAAI,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;YACxE,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAiC,EAAE,EAAE;gBAEnE,IAAG,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAElC,OAAO;gBACT,CAAC;gBAED,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAExC,QAAO,OAAO,CAAC,IAAI,EAAE,CAAC;oBAEpB,KAAK,MAAM,CAAC,CAAC,CAAC;wBAEZ,oJAAoJ;wBACpJ,kBAAkB;wBAClB,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;wBACpC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;wBAClC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;wBAEpE,OAAO;oBACT,CAAC;oBAED,KAAK,kBAAkB,CAAC,CAAC,CAAC;wBAExB,4JAA4J;wBAC5J,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;wBAClI,OAAO,EAAE,CAAC;wBAEV,OAAO;oBACT,CAAC;oBAED,KAAK,gBAAgB,CAAC,CAAC,CAAC;wBAEtB,0JAA0J;wBAC1J,8JAA8J;wBAC9J,0JAA0J;wBAC1J,gKAAgK;wBAChK,gKAAgK;wBAChK,IAAG,IAAI,CAAC,YAAY,EAAE,CAAC;4BAErB,MAAM,CAAC,IAAI,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;4BAE5E,OAAO;wBACT,CAAC;wBAED,MAAM,CAAC,IAAI,YAAY,CAAC,wGAAwG,EAC9H,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;wBAE7D,OAAO;oBACT,CAAC;oBAED,OAAO,CAAC,CAAC,CAAC;wBAER,4JAA4J;wBAC5J,oFAAoF;wBAEpF,OAAO;oBACT,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,qKAAqK;YACrK,kKAAkK;YAClK,0GAA0G;YAC1G,IAAI,CAAC;gBAEH,MAAM,SAAS,CAAC;YAClB,CAAC;YAAC,OAAM,KAAc,EAAE,CAAC;gBAEvB,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;gBAEzB,MAAM,KAAK,CAAC;YACd,CAAC;YAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;;;;;;;;;KACrE;IAED,qKAAqK;IACrK,iKAAiK;IACjK,sKAAsK;IACtK,sBAAsB;IACtB,KAAK,CAAC,OAAO,CAAC,OAAgB;;;YAE5B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;YAExB,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAExE,qKAAqK;YACrK,mKAAmK;YACnK,qKAAqK;YACrK,MAAM,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAC;YAEvC,qKAAqK;YACrK,mKAAmK;YACnK,oKAAoK;YACpK,mDAAmD;YACnD,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;YAC7I,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAS,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;YAEtJ,qKAAqK;YACrK,uGAAuG;YACvG,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,GAA+B,OAAO,CAAC,aAAa,EAAE,CAAC;YAEpG,MAAM,SAAS,kCAAG,QAAQ,QAAA,CAAC;YAC3B,MAAM,kBAAkB,kCAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,QAAA,CAAC;YAEjE,+JAA+J;YAC/J,wBAAwB;YACxB,QAAQ,CAAC,GAAG,EAAE,CAAC;YAEf,OAAO,CAAC,EAAE,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAiC,EAAE,EAAE;gBAE3E,IAAI,CAAC,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACrE,CAAC,CAAC,CAAC;YAEH,OAAO,CAAC,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAgC,EAAE,EAAE;gBAExE,gKAAgK;gBAChK,qBAAqB;gBACrB,IAAG,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBAEtC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;gBAC1F,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,OAAO,CAAC,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAc,EAAE,EAAE;gBAEtD,IAAG,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBAEtC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;gBAClG,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,MAAM,KAAK,CAAC;YAEZ,qKAAqK;YACrK,qKAAqK;YACrK,iBAAiB;YACjB,KAAI,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC;gBAEnC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;YAED,sKAAsK;YACtK,6DAA6D;YAC7D,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAE5B,IAAG,IAAI,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBAE7B,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;YAC5B,CAAC;;;;;;;;;KACF;IAED,wKAAwK;IACxK,iKAAiK;IACjK,oGAAoG;IACpG,oBAAoB,CAAC,OAAgB,EAAE,QAAkB,EAAE,QAAyB,EAAE,IAAa;QAEjG,IAAG,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAE5B,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAEhC,QAAO,KAAK,CAAC,IAAI,EAAE,CAAC;YAElB,KAAK,MAAM,CAAC,CAAC,CAAC;gBAEZ,6JAA6J;gBAC7J,SAAS;gBACT,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;gBAC/C,QAAQ,CAAC,GAAG,EAAE,CAAC;gBAEf,OAAO;YACT,CAAC;YAED,KAAK,SAAS,CAAC,CAAC,CAAC;gBAEf,kKAAkK;gBAClK,yBAAyB;gBACzB,IAAG,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC,EAAE,CAAC;oBAErE,KAAI,MAAM,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;wBAElD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;oBAC5B,CAAC;gBACH,CAAC;gBAED,OAAO;YACT,CAAC;YAED,OAAO,CAAC,CAAC,CAAC;gBAER,yHAAyH;gBAEzH,OAAO;YACT,CAAC;QACH,CAAC;IACH,CAAC;IAED,uKAAuK;IACvK,qKAAqK;IACrK,+EAA+E;IAC/E,cAAc,CAAC,IAAY;QAEzB,IAAG,IAAI,CAAC,YAAY,CAAC,MAAM,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAErD,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,aAAa,EAAE,CAAC;YAErB,IAAG,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;gBAEzB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;gBAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,8HAA8H,CAAC,CAAC;YACjJ,CAAC;QACH,CAAC;QAED,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC;IAChC,CAAC;IAED,uKAAuK;IACvK,+GAA+G;IAC/G,gBAAgB,CAAC,KAAc;QAE7B,IAAG,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC;YAEzE,OAAO,KAAK,CAAC,KAAK,CAAC;QACrB,CAAC;QAED,OAAO,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAClD,CAAC;IAED,qKAAqK;IACrK,uKAAuK;IACvK,sKAAsK;IACtK,aAAa;IACb,aAAa,CAAC,OAAgB;QAE5B,IAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAElB,OAAO;QACT,CAAC;QAED,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;QAEtB,IAAG,OAAO,CAAC,EAAE,CAAC,UAAU,KAAK,cAAc,EAAE,CAAC;YAE5C,IAAI,CAAC;gBAEH,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,GAAG,kBAAkB,GAAG,GAAG,CAAC,CAAC;YACnD,CAAC;YAAC,MAAM,CAAC;gBAEP,yHAAyH;YAC3H,CAAC;QACH,CAAC;QAED,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,mKAAmK;IACnK,uFAAuF;IACvF,eAAe,CAAC,EAAiB;QAE/B,IAAI,CAAC;YAEH,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YAEP,0GAA0G;QAC5G,CAAC;IACH,CAAC;IAED,uKAAuK;IACvK,wKAAwK;IACxK,8HAA8H;IAC9H,SAAS;QAEP,IAAG,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAE/B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClC,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC5B,CAAC;QAED,IAAI,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC;IAChC,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAqB;IAEhD,MAAM,EAAE,CAAC,IAAmB,EAAiB,EAAE,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC;CACpE,CAAC"}
|