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
package/build/fs-ops.mjs
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* fs-ops.mjs: Native node:fs/promises filesystem operations for HBPU's build and test scripts (rm, cp, mkdir). Node 22+ provides recursive flags natively, so no
|
|
5
|
+
* shell-portability shim is needed for the handful of operations we use.
|
|
6
|
+
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
*
|
|
9
|
+
* build-ui mkdir dist/ui and copy shippable ui/*.mjs (excluding test-only files) into it (invoked by the `build-ui` npm script).
|
|
10
|
+
* clean <paths...> rm -rf each path, silently ignoring missing entries; errors if no paths are supplied (invoked by `clean`).
|
|
11
|
+
* clean-docs remove TypeDoc-generated markdown from docs/ and docs/ffmpeg/ before regeneration (invoked by `build-docs`).
|
|
12
|
+
* finalize ready the emitted dist/ for shipping and local consumption: copy the browser-runtime modules into dist/ui and mark every declared bin
|
|
13
|
+
* entry executable, after tsc emits them (invoked at the tail of `build`).
|
|
14
|
+
*/
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
|
+
import { chmod, cp, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
17
|
+
|
|
18
|
+
// Hand-authored docs preserved through the clean-docs sweep. Everything else at the root of docs/ (and the entire docs/ffmpeg/ tree) is transient and regenerated by
|
|
19
|
+
// `npm run build-docs` on every run. Add entries to this set when introducing another file that should survive regeneration.
|
|
20
|
+
const PRESERVED_DOCS = new Set([ "Changelog.md", "Overview.md" ]);
|
|
21
|
+
|
|
22
|
+
// Recursively remove every path in the list. `rm` with `force: true` already silences ENOENT, so missing paths are a no-op and the behavior matches shx's `rm -rf`.
|
|
23
|
+
async function clean(paths) {
|
|
24
|
+
|
|
25
|
+
await Promise.all(paths.map((path) => rm(path, { force: true, recursive: true })));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Ensure dist/ui exists and copy the browser-side UI assets into it. Runs during the clean and build-ui phases, before tsc emits the compiled featureOptions.js,
|
|
29
|
+
// so the directory has to exist up front and the featureOptions copy step runs separately after tsc. We filter to shippable artifacts by excluding the
|
|
30
|
+
// test-only file shapes: `*.test.mjs` (suite files), `*.fixtures.mjs` (shared test data), `*.helpers.mjs` (shared test code), and `test-*.mjs` (test infrastructure,
|
|
31
|
+
// currently the `registerHooks` module-resolution loader). Suffix matches are centralized in `TEST_ONLY_SUFFIXES` so adding a new test-file shape is a one-line edit
|
|
32
|
+
// rather than a chain of `&& !entry.endsWith(...)` clauses. The resulting dist/ui/ contains only the browser runtime files consumers execute.
|
|
33
|
+
//
|
|
34
|
+
// The copy is recursive so the directory structure under `ui/` (the `webUi-featureOptions/`, `webUi-featureOptions/effects/`, `webUi-featureOptions/views/`
|
|
35
|
+
// subdirectories) is preserved as-is in `dist/ui/`. The filter is consulted per-entry: directories always pass through (returning `true` lets `cp` walk into them);
|
|
36
|
+
// files are kept only when they match the shippable shape.
|
|
37
|
+
const TEST_ONLY_SUFFIXES = [ ".fixtures.mjs", ".helpers.mjs", ".test.mjs" ];
|
|
38
|
+
|
|
39
|
+
async function buildUi() {
|
|
40
|
+
|
|
41
|
+
await mkdir("dist/ui", { recursive: true });
|
|
42
|
+
|
|
43
|
+
await cp("ui", "dist/ui", {
|
|
44
|
+
|
|
45
|
+
filter: async (src) => {
|
|
46
|
+
|
|
47
|
+
const s = await stat(src);
|
|
48
|
+
|
|
49
|
+
if(s.isDirectory()) {
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const name = basename(src);
|
|
55
|
+
|
|
56
|
+
if(!name.endsWith(".mjs") || name.startsWith("test-")) {
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return !TEST_ONLY_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
62
|
+
},
|
|
63
|
+
recursive: true
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Copy the compiled browser-runnable modules into dist/ui so the browser-side webUI can resolve them as siblings of the orchestrator. Runs after tsc emits the
|
|
68
|
+
// compiled `dist/*.js` files. The module list is the SSOT for "what `featureOptions.js` needs at runtime in the browser" - currently `featureOptions.js` itself
|
|
69
|
+
// plus `formatters.js` (the magnitude/percentage formatters the catalog's built-in renderer registry imports). Adding a new browser-side runtime dependency
|
|
70
|
+
// means adding its name here AND keeping that artifact's own imports browser-safe.
|
|
71
|
+
//
|
|
72
|
+
// Each module ships paired with its `.js.map` so browser DevTools can map runtime errors back to the original `.ts` source. The `.d.ts` / `.d.ts.map` artifacts
|
|
73
|
+
// are TypeScript-only and stay in `dist/` for type consumers; the browser never loads them, so they have no place in `dist/ui/`.
|
|
74
|
+
async function copyFeatureOptions() {
|
|
75
|
+
|
|
76
|
+
const modules = [ "featureOptions", "formatters" ];
|
|
77
|
+
|
|
78
|
+
await Promise.all(modules.flatMap((name) => [
|
|
79
|
+
|
|
80
|
+
cp(join("dist", name + ".js"), join("dist/ui", name + ".js")),
|
|
81
|
+
cp(join("dist", name + ".js.map"), join("dist/ui", name + ".js.map"))
|
|
82
|
+
]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Mark every executable declared in package.json's `bin` field as user-executable (0o755). tsc emits plain 0o644 files with no execute bit. For a registry install
|
|
86
|
+
// npm sets the bit on bin targets itself, but it does NOT chmod the live source file behind a `file:` symlinked dependency - so a plugin consuming HBPU locally
|
|
87
|
+
// gets a non-executable CLI entry and a "Permission denied" the moment it invokes the bin. Reading the path set straight from package.json keeps the `bin`
|
|
88
|
+
// declaration the single source of truth: the build chmods whatever `bin` points at, so introducing or relocating a bin entry needs no parallel edit here.
|
|
89
|
+
async function makeBinExecutable() {
|
|
90
|
+
|
|
91
|
+
const manifest = JSON.parse(await readFile("package.json", "utf8"));
|
|
92
|
+
const bin = manifest.bin;
|
|
93
|
+
|
|
94
|
+
// npm permits `bin` as either a string (a single executable named after the package) or an object map of command-name to path. Normalize both shapes to a flat
|
|
95
|
+
// list of paths so the chmod loop is agnostic to which form the manifest uses.
|
|
96
|
+
const binPaths = (typeof bin === "string") ? [bin] : Object.values(bin ?? {});
|
|
97
|
+
|
|
98
|
+
await Promise.all(binPaths.map((binPath) => chmod(binPath, 0o755)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Post-tsc finalization: ready the emitted dist/ for shipping and local consumption. Placing the browser-runtime modules alongside the webUI and marking the
|
|
102
|
+
// CLI bin executable are independent concerns, so they run concurrently. This is the single tail-of-build step the `build` script invokes after tsc.
|
|
103
|
+
async function finalize() {
|
|
104
|
+
|
|
105
|
+
await Promise.all([ copyFeatureOptions(), makeBinExecutable() ]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Remove the TypeDoc-generated markdown fragments from docs/ before regeneration. Entries listed in PRESERVED_DOCS survive at the root; docs/ffmpeg/ is cleared in
|
|
109
|
+
// full since everything under it is regenerated from source on every build-docs run.
|
|
110
|
+
async function cleanDocs() {
|
|
111
|
+
|
|
112
|
+
await Promise.all([
|
|
113
|
+
|
|
114
|
+
cleanDir("docs", (entry) => entry.endsWith(".md") && !PRESERVED_DOCS.has(entry)),
|
|
115
|
+
cleanDir("docs/ffmpeg", (entry) => entry.endsWith(".md"))
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Utility shared by cleanDocs: list a directory and remove every entry that matches the predicate. A missing parent directory resolves as a no-op rather than
|
|
120
|
+
// surfacing ENOENT, since a fresh clone won't have `docs/` at all and the build-docs flow needs to tolerate that.
|
|
121
|
+
async function cleanDir(dir, matches) {
|
|
122
|
+
|
|
123
|
+
let entries;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
|
|
127
|
+
entries = await readdir(dir);
|
|
128
|
+
} catch(error) {
|
|
129
|
+
|
|
130
|
+
if((error instanceof Error) && ("code" in error) && (error.code === "ENOENT")) {
|
|
131
|
+
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await Promise.all(entries.filter(matches).map((entry) => rm(join(dir, entry), { force: true })));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const [ command, ...args ] = process.argv.slice(2);
|
|
142
|
+
|
|
143
|
+
switch(command) {
|
|
144
|
+
|
|
145
|
+
case "build-ui": {
|
|
146
|
+
|
|
147
|
+
await buildUi();
|
|
148
|
+
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "clean": {
|
|
153
|
+
|
|
154
|
+
if(!args.length) {
|
|
155
|
+
|
|
156
|
+
process.stderr.write("Usage: fs-ops.mjs clean <paths...>\n");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await clean(args);
|
|
161
|
+
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case "clean-docs": {
|
|
166
|
+
|
|
167
|
+
await cleanDocs();
|
|
168
|
+
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case "finalize": {
|
|
173
|
+
|
|
174
|
+
await finalize();
|
|
175
|
+
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
default: {
|
|
180
|
+
|
|
181
|
+
process.stderr.write("Usage: fs-ops.mjs {build-ui | clean <paths...> | clean-docs | finalize}\n");
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/build/tsconfig.json
CHANGED
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
{
|
|
6
6
|
"compilerOptions": {
|
|
7
7
|
|
|
8
|
-
"
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
9
|
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"erasableSyntaxOnly": true,
|
|
10
12
|
"esModuleInterop": true,
|
|
11
13
|
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"isolatedModules": true,
|
|
12
15
|
|
|
13
16
|
"lib": [
|
|
14
17
|
|
|
@@ -16,13 +19,25 @@
|
|
|
16
19
|
"ES2024",
|
|
17
20
|
"ESNext.Array",
|
|
18
21
|
"ESNext.Collection",
|
|
19
|
-
"ESNext.
|
|
22
|
+
"ESNext.Disposable",
|
|
23
|
+
"ESNext.Iterator",
|
|
24
|
+
"ESNext.Promise"
|
|
20
25
|
],
|
|
21
26
|
|
|
22
27
|
"module": "nodenext",
|
|
28
|
+
"noFallthroughCasesInSwitch": true,
|
|
29
|
+
"noImplicitOverride": true,
|
|
30
|
+
"noImplicitReturns": true,
|
|
31
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
32
|
+
"noUncheckedIndexedAccess": true,
|
|
33
|
+
"noUncheckedSideEffectImports": true,
|
|
34
|
+
"resolveJsonModule": true,
|
|
35
|
+
"rewriteRelativeImportExtensions": true,
|
|
36
|
+
"skipLibCheck": true,
|
|
23
37
|
"sourceMap": true,
|
|
24
38
|
"strict": true,
|
|
25
|
-
"target": "ES2024"
|
|
39
|
+
"target": "ES2024",
|
|
40
|
+
"verbatimModuleSyntax": true
|
|
26
41
|
}
|
|
27
42
|
}
|
|
28
43
|
/* When creating your own tsconfig.json, use the following as a starting point to inherit these defaults:
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Nullable } from "./util.ts";
|
|
2
|
+
import type { Writable } from "node:stream";
|
|
3
|
+
/**
|
|
4
|
+
* Thrown synchronously by {@link BackpressureWriter.write} when the pending queue is already at the configured {@link BackpressureWriterInit.highWaterMark} and
|
|
5
|
+
* accepting the new chunk would push it over.
|
|
6
|
+
*
|
|
7
|
+
* Separate from the "writer has aborted" and "underlying stream is dead" failure modes so callers can discriminate backpressure-overflow (back off and retry later)
|
|
8
|
+
* from terminal failures (give up or escalate) by type rather than by inspecting error message text.
|
|
9
|
+
*
|
|
10
|
+
* @category Utilities
|
|
11
|
+
*/
|
|
12
|
+
export declare class BackpressureOverflowError extends Error {
|
|
13
|
+
readonly name: "BackpressureOverflowError";
|
|
14
|
+
constructor(message?: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rejected by an individual {@link BackpressureWriter.write} promise when the provider returned a {@link Writable} whose `writable` flag is `false` (the stream has
|
|
18
|
+
* ended or been destroyed). The writer itself remains alive - a later stream replacement via the provider may revive the pipeline - so this is a per-write rejection,
|
|
19
|
+
* not a terminal writer error.
|
|
20
|
+
*
|
|
21
|
+
* @category Utilities
|
|
22
|
+
*/
|
|
23
|
+
export declare class BackpressureClosedStreamError extends Error {
|
|
24
|
+
readonly name: "BackpressureClosedStreamError";
|
|
25
|
+
constructor(message?: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Construction-time options for {@link BackpressureWriter}.
|
|
29
|
+
*
|
|
30
|
+
* @property highWaterMark - Optional ceiling on the total pending-write depth, including the in-flight entry that is currently awaiting `drain`. When specified, a
|
|
31
|
+
* `write()` call that would push the pending queue past this depth rejects synchronously with a {@link BackpressureOverflowError} rather
|
|
32
|
+
* than buffering unboundedly. Omit for unbounded queueing (the caller trusts upstream producers not to outrun the stream by more than
|
|
33
|
+
* available memory).
|
|
34
|
+
* @property signal - Optional parent {@link AbortSignal} to compose with the writer's internal controller. When the parent aborts, the writer tears down:
|
|
35
|
+
* pending writes reject with `signal.reason`, any in-flight drain wait unwinds, and every subsequent `write()` call rejects immediately.
|
|
36
|
+
*
|
|
37
|
+
* @category Utilities
|
|
38
|
+
*/
|
|
39
|
+
export interface BackpressureWriterInit {
|
|
40
|
+
highWaterMark?: number;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* AsyncDisposable backpressure-aware write queue for Node {@link Writable} streams.
|
|
45
|
+
*
|
|
46
|
+
* Each call to {@link BackpressureWriter.write} returns a Promise that resolves once the chunk has been written (and any triggered backpressure has drained) or rejects
|
|
47
|
+
* if the writer aborts mid-write. Concurrent writes serialize through an internal FIFO queue; ordering matches call order. The stream itself is resolved lazily through
|
|
48
|
+
* a caller-supplied provider on each drain turn, so the writer may outlive any particular stream instance - the provider is consulted per chunk, and a `null` return is
|
|
49
|
+
* a signal to drop the chunk (the associated write promise still resolves, treating the drop as a success from the caller's perspective).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { BackpressureWriter } from "homebridge-plugin-utils";
|
|
55
|
+
*
|
|
56
|
+
* await using writer = new BackpressureWriter(() => ffmpegProcess.stdin ?? null, { signal: session.signal });
|
|
57
|
+
*
|
|
58
|
+
* // Each write awaits its own flush; concurrent writes queue behind prior ones.
|
|
59
|
+
* await writer.write(segmentOne);
|
|
60
|
+
* await writer.write(segmentTwo);
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @category Utilities
|
|
64
|
+
*/
|
|
65
|
+
export declare class BackpressureWriter implements AsyncDisposable {
|
|
66
|
+
#private;
|
|
67
|
+
/**
|
|
68
|
+
* The composed abort signal representing this writer's lifetime. Aborts exactly once when {@link BackpressureWriter.abort} is called, when the parent signal fires,
|
|
69
|
+
* or when the underlying stream surfaces an error that invalidates the writer; `signal.reason` names the cause.
|
|
70
|
+
*/
|
|
71
|
+
readonly signal: AbortSignal;
|
|
72
|
+
/**
|
|
73
|
+
* Construct a new backpressure-aware writer.
|
|
74
|
+
*
|
|
75
|
+
* @param streamProvider - A function that returns the current writable stream, or `null` to drop incoming chunks. Evaluated lazily on each drain-loop iteration.
|
|
76
|
+
* @param init - Optional init options. See {@link BackpressureWriterInit}.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* await using writer = new BackpressureWriter(() => this.ffmpegProcess?.stdin ?? null, { highWaterMark: 64, signal: session.signal });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
constructor(streamProvider: () => Nullable<Writable>, init?: BackpressureWriterInit);
|
|
85
|
+
/**
|
|
86
|
+
* Enqueue `chunk` for writing. Concurrent calls serialize in FIFO order via the internal queue.
|
|
87
|
+
*
|
|
88
|
+
* @param chunk - The buffer to write.
|
|
89
|
+
*
|
|
90
|
+
* @returns A promise that resolves when the chunk has been flushed to the underlying stream (including any required drain wait), or immediately if the provider
|
|
91
|
+
* returned `null` at dispatch time (drop semantics). The promise rejects in the following cases:
|
|
92
|
+
*
|
|
93
|
+
* - `this.signal.reason` - the writer aborted before or during the write.
|
|
94
|
+
* - {@link BackpressureOverflowError} (thrown synchronously) - `highWaterMark` is configured and the queue depth already equals or exceeds it.
|
|
95
|
+
* - {@link BackpressureClosedStreamError} - the provider returned a stream whose `writable` flag is `false`. The writer itself stays alive for a potential later
|
|
96
|
+
* stream replacement.
|
|
97
|
+
*
|
|
98
|
+
* @throws {@link BackpressureOverflowError} when `highWaterMark` is exceeded.
|
|
99
|
+
*/
|
|
100
|
+
write(chunk: Buffer): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Abort the writer and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged.
|
|
103
|
+
*
|
|
104
|
+
* Safe to call more than once: subsequent calls are no-ops because the underlying signal only aborts once. Every queued write rejects with the signal's reason; any
|
|
105
|
+
* in-flight drain wait rejects with the signal's reason as well, and that rejection propagates out of the in-flight `write()` promise.
|
|
106
|
+
*
|
|
107
|
+
* @param reason - Optional abort reason. Typically an {@link HbpuAbortError}; platform errors (`TimeoutError`, `AbortError`) also interoperate by convention.
|
|
108
|
+
*/
|
|
109
|
+
abort(reason?: unknown): void;
|
|
110
|
+
/**
|
|
111
|
+
* `AsyncDisposable` implementation. Aborts the writer (defaulting to `"shutdown"`) and awaits actual drain-loop completion before returning, so callers using
|
|
112
|
+
* `await using` are guaranteed every pending write has settled by the time the surrounding scope exits.
|
|
113
|
+
*
|
|
114
|
+
* @returns A promise that resolves once the drain loop has fully exited.
|
|
115
|
+
*/
|
|
116
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* `true` once `this.signal` has aborted. Derived from the signal; no independent state.
|
|
119
|
+
*/
|
|
120
|
+
get aborted(): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Total number of entries in the pending-write queue, including the in-flight entry (if the drain loop is parked on `events.once(stream, "drain", { signal })`).
|
|
123
|
+
* Matches the depth that the configured `highWaterMark` is compared against, so adaptive producers watching this value see the same accounting the writer uses
|
|
124
|
+
* internally.
|
|
125
|
+
*/
|
|
126
|
+
get pending(): number;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=backpressure.d.ts.map
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* backpressure.ts: Signal-driven, backpressure-aware write queue for Node.js writable streams.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* AsyncDisposable write queue that serializes Buffer writes onto a Node {@link Writable}, respects backpressure via the stream's `drain` event, and composes into the
|
|
7
|
+
* library's `AbortSignal`-driven lifecycle so a parent signal can tear the writer down uniformly with every other HBPU resource class.
|
|
8
|
+
*
|
|
9
|
+
* Primary use case: feeding fMP4 segments from a livestream event source into an FFmpeg process's stdin, where the downstream may not consume as fast as the upstream
|
|
10
|
+
* produces.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { HbpuAbortError, composeSignals, onAbort } from "./util.js";
|
|
15
|
+
import { once } from "node:events";
|
|
16
|
+
/**
|
|
17
|
+
* Thrown synchronously by {@link BackpressureWriter.write} when the pending queue is already at the configured {@link BackpressureWriterInit.highWaterMark} and
|
|
18
|
+
* accepting the new chunk would push it over.
|
|
19
|
+
*
|
|
20
|
+
* Separate from the "writer has aborted" and "underlying stream is dead" failure modes so callers can discriminate backpressure-overflow (back off and retry later)
|
|
21
|
+
* from terminal failures (give up or escalate) by type rather than by inspecting error message text.
|
|
22
|
+
*
|
|
23
|
+
* @category Utilities
|
|
24
|
+
*/
|
|
25
|
+
export class BackpressureOverflowError extends Error {
|
|
26
|
+
name = "BackpressureOverflowError";
|
|
27
|
+
constructor(message = "BackpressureWriter: queue depth exceeds configured highWaterMark.") {
|
|
28
|
+
super(message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rejected by an individual {@link BackpressureWriter.write} promise when the provider returned a {@link Writable} whose `writable` flag is `false` (the stream has
|
|
33
|
+
* ended or been destroyed). The writer itself remains alive - a later stream replacement via the provider may revive the pipeline - so this is a per-write rejection,
|
|
34
|
+
* not a terminal writer error.
|
|
35
|
+
*
|
|
36
|
+
* @category Utilities
|
|
37
|
+
*/
|
|
38
|
+
export class BackpressureClosedStreamError extends Error {
|
|
39
|
+
name = "BackpressureClosedStreamError";
|
|
40
|
+
constructor(message = "BackpressureWriter: underlying stream is not writable.") {
|
|
41
|
+
super(message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* AsyncDisposable backpressure-aware write queue for Node {@link Writable} streams.
|
|
46
|
+
*
|
|
47
|
+
* Each call to {@link BackpressureWriter.write} returns a Promise that resolves once the chunk has been written (and any triggered backpressure has drained) or rejects
|
|
48
|
+
* if the writer aborts mid-write. Concurrent writes serialize through an internal FIFO queue; ordering matches call order. The stream itself is resolved lazily through
|
|
49
|
+
* a caller-supplied provider on each drain turn, so the writer may outlive any particular stream instance - the provider is consulted per chunk, and a `null` return is
|
|
50
|
+
* a signal to drop the chunk (the associated write promise still resolves, treating the drop as a success from the caller's perspective).
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { BackpressureWriter } from "homebridge-plugin-utils";
|
|
56
|
+
*
|
|
57
|
+
* await using writer = new BackpressureWriter(() => ffmpegProcess.stdin ?? null, { signal: session.signal });
|
|
58
|
+
*
|
|
59
|
+
* // Each write awaits its own flush; concurrent writes queue behind prior ones.
|
|
60
|
+
* await writer.write(segmentOne);
|
|
61
|
+
* await writer.write(segmentTwo);
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @category Utilities
|
|
65
|
+
*/
|
|
66
|
+
export class BackpressureWriter {
|
|
67
|
+
/**
|
|
68
|
+
* The composed abort signal representing this writer's lifetime. Aborts exactly once when {@link BackpressureWriter.abort} is called, when the parent signal fires,
|
|
69
|
+
* or when the underlying stream surfaces an error that invalidates the writer; `signal.reason` names the cause.
|
|
70
|
+
*/
|
|
71
|
+
signal;
|
|
72
|
+
// The private AbortController whose signal is composed into `this.signal`. Owning the controller internally keeps teardown reachable from any handler - drain-wait
|
|
73
|
+
// cancellation, stream-error escalation, explicit abort - without giving callers a handle to the raw controller.
|
|
74
|
+
#controller;
|
|
75
|
+
// Optional queue-depth ceiling. When undefined, the queue is unbounded and the only memory discipline is whatever the caller imposes by awaiting prior writes before
|
|
76
|
+
// issuing new ones.
|
|
77
|
+
#highWaterMark;
|
|
78
|
+
// Caller-provided resolver for the target stream. Evaluated lazily on each drain-loop iteration so the writer can survive stream replacement across FFmpeg process
|
|
79
|
+
// restarts, and so callers can bind the writer to a stream that does not yet exist at construction time.
|
|
80
|
+
#streamProvider;
|
|
81
|
+
// FIFO queue of pending writes. Each entry pairs the buffer with its resolver so the drain loop can settle the caller's promise at the same point it consumes the
|
|
82
|
+
// chunk. The drain loop peeks `queue[0]` during the write (so queue.length reflects total pending, including the in-flight entry) and shifts the entry off only
|
|
83
|
+
// after it has fully flushed or failed.
|
|
84
|
+
#queue = [];
|
|
85
|
+
// The active drain-loop promise, if any. Held so `[Symbol.asyncDispose]` can await actual completion before returning, so `await using` callers are guaranteed no
|
|
86
|
+
// stream interaction is still in flight by the time the surrounding scope exits.
|
|
87
|
+
#drainTask;
|
|
88
|
+
// True while the drain loop is active (set at entry, cleared in the loop's `finally`). Used by `write()` to decide whether to spawn a new drain - the flag flips
|
|
89
|
+
// back to false the moment the loop exits, whether synchronously (queue emptied in one pass, no backpressure) or asynchronously (after a drain wait or abort) - so
|
|
90
|
+
// a subsequent `write()` that arrives right after an empty-queue exit correctly re-spawns the loop rather than being stranded by a stale drain-task reference.
|
|
91
|
+
#processing = false;
|
|
92
|
+
/**
|
|
93
|
+
* Construct a new backpressure-aware writer.
|
|
94
|
+
*
|
|
95
|
+
* @param streamProvider - A function that returns the current writable stream, or `null` to drop incoming chunks. Evaluated lazily on each drain-loop iteration.
|
|
96
|
+
* @param init - Optional init options. See {@link BackpressureWriterInit}.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* await using writer = new BackpressureWriter(() => this.ffmpegProcess?.stdin ?? null, { highWaterMark: 64, signal: session.signal });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
constructor(streamProvider, init = {}) {
|
|
105
|
+
this.#streamProvider = streamProvider;
|
|
106
|
+
this.#highWaterMark = init.highWaterMark;
|
|
107
|
+
this.#controller = new AbortController();
|
|
108
|
+
this.signal = composeSignals(init.signal, this.#controller.signal);
|
|
109
|
+
// Single teardown convergence point. `onAbort` registers the one-shot teardown handler AND handles the pre-aborted-signal edge case where `addEventListener`
|
|
110
|
+
// would otherwise skip the handler. Rejects every pending entry - including the in-flight entry when the drain loop is parked on
|
|
111
|
+
// `events.once(stream, "drain", { signal })`. The drain loop's catch branch may also reject the in-flight resolver under the same abort; promise resolvers are
|
|
112
|
+
// idempotent after first settlement, so the second call is a no-op. Rejecting from here unconditionally is load-bearing in the stream-error escalation path,
|
|
113
|
+
// where the drain loop shifts the in-flight entry before calling `this.#controller.abort(...)` - by the time the listener runs, only the queued entries remain
|
|
114
|
+
// in the queue, and leaving any of them unrejected would orphan the caller's promise.
|
|
115
|
+
onAbort(this.signal, () => this.#teardown());
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Enqueue `chunk` for writing. Concurrent calls serialize in FIFO order via the internal queue.
|
|
119
|
+
*
|
|
120
|
+
* @param chunk - The buffer to write.
|
|
121
|
+
*
|
|
122
|
+
* @returns A promise that resolves when the chunk has been flushed to the underlying stream (including any required drain wait), or immediately if the provider
|
|
123
|
+
* returned `null` at dispatch time (drop semantics). The promise rejects in the following cases:
|
|
124
|
+
*
|
|
125
|
+
* - `this.signal.reason` - the writer aborted before or during the write.
|
|
126
|
+
* - {@link BackpressureOverflowError} (thrown synchronously) - `highWaterMark` is configured and the queue depth already equals or exceeds it.
|
|
127
|
+
* - {@link BackpressureClosedStreamError} - the provider returned a stream whose `writable` flag is `false`. The writer itself stays alive for a potential later
|
|
128
|
+
* stream replacement.
|
|
129
|
+
*
|
|
130
|
+
* @throws {@link BackpressureOverflowError} when `highWaterMark` is exceeded.
|
|
131
|
+
*/
|
|
132
|
+
async write(chunk) {
|
|
133
|
+
// Pre-aborted short-circuit: a write issued after teardown has nothing live to attach to. Rejecting with the signal's reason keeps semantics uniform with the
|
|
134
|
+
// mid-write abort path - callers discriminate on `signal.reason` / `isHbpuAbortReason` in either case.
|
|
135
|
+
if (this.signal.aborted) {
|
|
136
|
+
throw this.signal.reason;
|
|
137
|
+
}
|
|
138
|
+
// Queue-overflow fail-fast. Rejecting synchronously lets upstream producers apply their own pacing rather than silently accumulating unbounded backlog. A
|
|
139
|
+
// dedicated {@link BackpressureOverflowError} class (not an `HbpuAbortError`) is thrown here because the writer itself is still healthy - the caller supplied too
|
|
140
|
+
// much work, not the system asking for teardown - and consumers who care about the distinction can `instanceof`-check rather than match on message text.
|
|
141
|
+
if ((this.#highWaterMark !== undefined) && (this.#queue.length >= this.#highWaterMark)) {
|
|
142
|
+
throw new BackpressureOverflowError();
|
|
143
|
+
}
|
|
144
|
+
const resolvers = Promise.withResolvers();
|
|
145
|
+
this.#queue.push({ chunk, resolvers });
|
|
146
|
+
// Spin up the drain loop when nothing is already processing. We key off the `#processing` flag rather than `#drainTask` because an async function's body runs
|
|
147
|
+
// synchronously until its first `await`: a no-backpressure drain completes before `write()` returns, leaving `#drainTask` set to a resolved-but-not-yet-cleared
|
|
148
|
+
// promise that would falsely gate subsequent writes. `#processing` is flipped true at the top of `#drain` and false in its `finally`, so checking it gives us a
|
|
149
|
+
// truthful "is a drain currently running?" answer with no chained-promise races.
|
|
150
|
+
if (!this.#processing) {
|
|
151
|
+
this.#drainTask = this.#drain();
|
|
152
|
+
}
|
|
153
|
+
return resolvers.promise;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Abort the writer and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged.
|
|
157
|
+
*
|
|
158
|
+
* Safe to call more than once: subsequent calls are no-ops because the underlying signal only aborts once. Every queued write rejects with the signal's reason; any
|
|
159
|
+
* in-flight drain wait rejects with the signal's reason as well, and that rejection propagates out of the in-flight `write()` promise.
|
|
160
|
+
*
|
|
161
|
+
* @param reason - Optional abort reason. Typically an {@link HbpuAbortError}; platform errors (`TimeoutError`, `AbortError`) also interoperate by convention.
|
|
162
|
+
*/
|
|
163
|
+
abort(reason) {
|
|
164
|
+
if (this.aborted) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.#controller.abort(reason ?? new HbpuAbortError("shutdown"));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* `AsyncDisposable` implementation. Aborts the writer (defaulting to `"shutdown"`) and awaits actual drain-loop completion before returning, so callers using
|
|
171
|
+
* `await using` are guaranteed every pending write has settled by the time the surrounding scope exits.
|
|
172
|
+
*
|
|
173
|
+
* @returns A promise that resolves once the drain loop has fully exited.
|
|
174
|
+
*/
|
|
175
|
+
async [Symbol.asyncDispose]() {
|
|
176
|
+
this.abort();
|
|
177
|
+
if (this.#drainTask) {
|
|
178
|
+
// Drain failures are already observed through the individual write promises; swallow here so `await using` does not surface cleanup-side errors the caller has
|
|
179
|
+
// no way to react to.
|
|
180
|
+
await this.#drainTask.catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* `true` once `this.signal` has aborted. Derived from the signal; no independent state.
|
|
185
|
+
*/
|
|
186
|
+
get aborted() {
|
|
187
|
+
return this.signal.aborted;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Total number of entries in the pending-write queue, including the in-flight entry (if the drain loop is parked on `events.once(stream, "drain", { signal })`).
|
|
191
|
+
* Matches the depth that the configured `highWaterMark` is compared against, so adaptive producers watching this value see the same accounting the writer uses
|
|
192
|
+
* internally.
|
|
193
|
+
*/
|
|
194
|
+
get pending() {
|
|
195
|
+
return this.#queue.length;
|
|
196
|
+
}
|
|
197
|
+
// Drain loop. Iterates the queue, writing each chunk through the provider's current stream, honoring backpressure via `events.once(stream, "drain", { signal })`.
|
|
198
|
+
// Runs until the queue is empty or the signal aborts; every in-flight write is settled before exit (resolved on successful write, rejected via drain-abort on
|
|
199
|
+
// teardown). Note the peek-then-shift pattern: the entry stays at `queue[0]` while the write is in flight, so `queue.length` reflects the true pending count
|
|
200
|
+
// (including the in-flight entry) and `highWaterMark` checks in `write()` work uniformly whether the drain is idle or mid-wait.
|
|
201
|
+
async #drain() {
|
|
202
|
+
this.#processing = true;
|
|
203
|
+
try {
|
|
204
|
+
while ((this.#queue.length > 0) && !this.signal.aborted) {
|
|
205
|
+
const entry = this.#queue[0];
|
|
206
|
+
// `length`-guarded peek: the loop condition just checked non-empty, so we always have an entry. `noUncheckedIndexedAccess` widens the indexed access to
|
|
207
|
+
// `PendingWrite | undefined`; the null check below narrows for the compiler without changing runtime semantics.
|
|
208
|
+
if (!entry) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
// Re-evaluate the stream on every iteration. The provider is free to return a different instance across turns (stream replacement across restarts) or `null`
|
|
212
|
+
// (no live stream right now - drop the chunk). Both cases are expected in HBUP's pipeline and are handled here without surfacing as errors to the caller.
|
|
213
|
+
const stream = this.#streamProvider();
|
|
214
|
+
if (!stream) {
|
|
215
|
+
// Drop semantics: the provider explicitly said "no stream." The write promise resolves so the caller can continue without branching on a per-write return;
|
|
216
|
+
// the surrounding session is responsible for deciding whether the lack of a stream is itself a fault.
|
|
217
|
+
this.#queue.shift();
|
|
218
|
+
entry.resolvers.resolve();
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!stream.writable) {
|
|
222
|
+
// Stream exists but has ended. Reject this specific write with a {@link BackpressureClosedStreamError} and let the caller decide whether to escalate - we
|
|
223
|
+
// do not abort the writer here, because a later stream replacement (via the provider) may revive the pipeline.
|
|
224
|
+
this.#queue.shift();
|
|
225
|
+
entry.resolvers.reject(new BackpressureClosedStreamError());
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
// `stream.write()` returns `false` when the high-water mark has been breached; the write is still queued by Node, but we must wait for `drain` before
|
|
230
|
+
// pushing more bytes or we risk unbounded growth of Node's internal write buffer in process memory. `events.once` with `{ signal }` ties the wait to the
|
|
231
|
+
// writer's lifetime so an abort during drain rejects the wait with the signal's reason rather than hanging.
|
|
232
|
+
if (!stream.write(entry.chunk)) {
|
|
233
|
+
// eslint-disable-next-line no-await-in-loop
|
|
234
|
+
await once(stream, "drain", { signal: this.signal });
|
|
235
|
+
}
|
|
236
|
+
this.#queue.shift();
|
|
237
|
+
entry.resolvers.resolve();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// Two rejection shapes land here. (a) Our signal aborted mid-drain: `once` rejects with `signal.reason`; we surface that to the caller and exit the loop
|
|
241
|
+
// because teardown is already in flight. (b) The stream emitted an error via some path `events.once` exposes: the write that triggered the error cannot
|
|
242
|
+
// complete, so we reject this caller and abort the writer with `"failed"` so no subsequent write tries to reuse a broken stream silently.
|
|
243
|
+
this.#queue.shift();
|
|
244
|
+
if (this.aborted) {
|
|
245
|
+
entry.resolvers.reject(this.signal.reason);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
entry.resolvers.reject(error);
|
|
249
|
+
this.#controller.abort(new HbpuAbortError("failed", { cause: error }));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
this.#processing = false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Teardown convergence point, fired exactly once when `this.signal` aborts. Rejects every pending entry with the signal's reason, regardless of whether the drain
|
|
259
|
+
// loop is currently processing. Promise resolvers are idempotent after first settlement, so if the drain loop's catch branch also rejects the in-flight resolver
|
|
260
|
+
// (e.g., when `events.once(..., { signal })` rejects on the same abort), the second call is a no-op. Draining the queue unconditionally here prevents the
|
|
261
|
+
// stream-error escalation path from orphaning queued resolvers - `#drain`'s catch shifts the in-flight entry and then calls `this.#controller.abort(...)`, which
|
|
262
|
+
// fires this listener synchronously while the remaining queued entries are still in the queue.
|
|
263
|
+
#teardown() {
|
|
264
|
+
// `signal.reason` is typed `any` in the DOM lib. The `: unknown` annotation narrows it to `unknown` for the `@typescript-eslint/no-unsafe-assignment` rule, which
|
|
265
|
+
// surfaces stray `any` values on assignment. `unknown` flows through `resolvers.reject()` unchanged - reject accepts any value.
|
|
266
|
+
const reason = this.signal.reason;
|
|
267
|
+
const pending = this.#queue.splice(0);
|
|
268
|
+
for (const entry of pending) {
|
|
269
|
+
entry.resolvers.reject(reason);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=backpressure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backpressure.js","sourceRoot":"","sources":["../src/backpressure.ts"],"names":[],"mappings":"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"}
|