homebridge-plugin-utils 1.35.0 → 2.1.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.
Files changed (204) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +94 -45
  20. package/dist/backpressure.js +229 -93
  21. package/dist/backpressure.js.map +1 -1
  22. package/dist/cli/index.d.ts +166 -0
  23. package/dist/cli/index.js +551 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/clock-double.d.ts +93 -0
  26. package/dist/clock-double.js +141 -0
  27. package/dist/clock-double.js.map +1 -0
  28. package/dist/clock.d.ts +39 -0
  29. package/dist/clock.js +34 -0
  30. package/dist/clock.js.map +1 -0
  31. package/dist/disposable-stack.d.ts +59 -0
  32. package/dist/disposable-stack.js +155 -0
  33. package/dist/disposable-stack.js.map +1 -0
  34. package/dist/docChrome.d.ts +260 -0
  35. package/dist/docChrome.js +361 -0
  36. package/dist/docChrome.js.map +1 -0
  37. package/dist/eslint-plugin/config.d.mts +193 -0
  38. package/dist/eslint-plugin/index.d.mts +2 -0
  39. package/dist/eslint-plugin/plugin.d.mts +87 -0
  40. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  41. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  42. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  43. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  44. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  45. package/dist/featureOptions-docs.d.ts +96 -0
  46. package/dist/featureOptions-docs.js +193 -0
  47. package/dist/featureOptions-docs.js.map +1 -0
  48. package/dist/featureOptions.d.ts +674 -0
  49. package/dist/featureOptions.js +870 -0
  50. package/dist/featureOptions.js.map +1 -0
  51. package/dist/ffmpeg/codecs.d.ts +256 -72
  52. package/dist/ffmpeg/codecs.js +477 -262
  53. package/dist/ffmpeg/codecs.js.map +1 -1
  54. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  55. package/dist/ffmpeg/dgram-util.js +38 -0
  56. package/dist/ffmpeg/dgram-util.js.map +1 -0
  57. package/dist/ffmpeg/exec.d.ts +83 -64
  58. package/dist/ffmpeg/exec.js +77 -86
  59. package/dist/ffmpeg/exec.js.map +1 -1
  60. package/dist/ffmpeg/fmp4.d.ts +49 -2
  61. package/dist/ffmpeg/fmp4.js +47 -11
  62. package/dist/ffmpeg/fmp4.js.map +1 -1
  63. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  64. package/dist/ffmpeg/hap-enums.js +92 -0
  65. package/dist/ffmpeg/hap-enums.js.map +1 -0
  66. package/dist/ffmpeg/index.d.ts +16 -9
  67. package/dist/ffmpeg/index.js +6 -0
  68. package/dist/ffmpeg/index.js.map +1 -1
  69. package/dist/ffmpeg/mp4-assembler.d.ts +161 -0
  70. package/dist/ffmpeg/mp4-assembler.js +424 -0
  71. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  72. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  73. package/dist/ffmpeg/mp4-parser.js +130 -0
  74. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  75. package/dist/ffmpeg/options.d.ts +62 -149
  76. package/dist/ffmpeg/options.js +608 -499
  77. package/dist/ffmpeg/options.js.map +1 -1
  78. package/dist/ffmpeg/process.d.ts +142 -96
  79. package/dist/ffmpeg/process.js +406 -278
  80. package/dist/ffmpeg/process.js.map +1 -1
  81. package/dist/ffmpeg/record.d.ts +325 -186
  82. package/dist/ffmpeg/record.js +418 -565
  83. package/dist/ffmpeg/record.js.map +1 -1
  84. package/dist/ffmpeg/recording-process-double.d.ts +157 -0
  85. package/dist/ffmpeg/recording-process-double.js +190 -0
  86. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  87. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  88. package/dist/ffmpeg/rtp-parser.js +77 -0
  89. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  90. package/dist/ffmpeg/rtp.d.ts +198 -141
  91. package/dist/ffmpeg/rtp.js +474 -251
  92. package/dist/ffmpeg/rtp.js.map +1 -1
  93. package/dist/ffmpeg/settings.d.ts +5 -2
  94. package/dist/ffmpeg/settings.js +20 -5
  95. package/dist/ffmpeg/settings.js.map +1 -1
  96. package/dist/ffmpeg/stream.d.ts +57 -107
  97. package/dist/ffmpeg/stream.js +121 -150
  98. package/dist/ffmpeg/stream.js.map +1 -1
  99. package/dist/formatters.d.ts +106 -0
  100. package/dist/formatters.js +174 -0
  101. package/dist/formatters.js.map +1 -0
  102. package/dist/homebridge-enums.d.ts +30 -0
  103. package/dist/homebridge-enums.js +17 -0
  104. package/dist/homebridge-enums.js.map +1 -0
  105. package/dist/index.d.ts +13 -6
  106. package/dist/index.js +8 -2
  107. package/dist/index.js.map +1 -1
  108. package/dist/logclient/auth.d.ts +114 -0
  109. package/dist/logclient/auth.js +199 -0
  110. package/dist/logclient/auth.js.map +1 -0
  111. package/dist/logclient/cli-run.d.ts +76 -0
  112. package/dist/logclient/cli-run.js +639 -0
  113. package/dist/logclient/cli-run.js.map +1 -0
  114. package/dist/logclient/cli.d.ts +3 -0
  115. package/dist/logclient/cli.js +97 -0
  116. package/dist/logclient/cli.js.map +1 -0
  117. package/dist/logclient/client.d.ts +145 -0
  118. package/dist/logclient/client.js +600 -0
  119. package/dist/logclient/client.js.map +1 -0
  120. package/dist/logclient/config.d.ts +173 -0
  121. package/dist/logclient/config.js +199 -0
  122. package/dist/logclient/config.js.map +1 -0
  123. package/dist/logclient/endpoints.d.ts +54 -0
  124. package/dist/logclient/endpoints.js +73 -0
  125. package/dist/logclient/endpoints.js.map +1 -0
  126. package/dist/logclient/filter.d.ts +45 -0
  127. package/dist/logclient/filter.js +51 -0
  128. package/dist/logclient/filter.js.map +1 -0
  129. package/dist/logclient/frame.d.ts +93 -0
  130. package/dist/logclient/frame.js +203 -0
  131. package/dist/logclient/frame.js.map +1 -0
  132. package/dist/logclient/index.d.ts +31 -0
  133. package/dist/logclient/index.js +12 -0
  134. package/dist/logclient/index.js.map +1 -0
  135. package/dist/logclient/parser.d.ts +211 -0
  136. package/dist/logclient/parser.js +393 -0
  137. package/dist/logclient/parser.js.map +1 -0
  138. package/dist/logclient/rest.d.ts +41 -0
  139. package/dist/logclient/rest.js +111 -0
  140. package/dist/logclient/rest.js.map +1 -0
  141. package/dist/logclient/settings.d.ts +15 -0
  142. package/dist/logclient/settings.js +64 -0
  143. package/dist/logclient/settings.js.map +1 -0
  144. package/dist/logclient/socket-double.d.ts +201 -0
  145. package/dist/logclient/socket-double.js +384 -0
  146. package/dist/logclient/socket-double.js.map +1 -0
  147. package/dist/logclient/socket.d.ts +257 -0
  148. package/dist/logclient/socket.js +620 -0
  149. package/dist/logclient/socket.js.map +1 -0
  150. package/dist/logclient/stitch.d.ts +83 -0
  151. package/dist/logclient/stitch.js +146 -0
  152. package/dist/logclient/stitch.js.map +1 -0
  153. package/dist/logclient/time-expression.d.ts +42 -0
  154. package/dist/logclient/time-expression.js +181 -0
  155. package/dist/logclient/time-expression.js.map +1 -0
  156. package/dist/logclient/time-window.d.ts +38 -0
  157. package/dist/logclient/time-window.js +53 -0
  158. package/dist/logclient/time-window.js.map +1 -0
  159. package/dist/logclient/types.d.ts +107 -0
  160. package/dist/logclient/types.js +6 -0
  161. package/dist/logclient/types.js.map +1 -0
  162. package/dist/mqttClient.d.ts +287 -0
  163. package/dist/mqttClient.js +433 -0
  164. package/dist/mqttClient.js.map +1 -0
  165. package/dist/service.d.ts +64 -15
  166. package/dist/service.js +93 -66
  167. package/dist/service.js.map +1 -1
  168. package/dist/ui/featureOptions.js +870 -0
  169. package/dist/ui/featureOptions.js.map +1 -0
  170. package/dist/ui/formatters.js +174 -0
  171. package/dist/ui/formatters.js.map +1 -0
  172. package/dist/ui/pluginConfigSession.mjs +141 -0
  173. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  174. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  175. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  176. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  177. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  179. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  180. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  181. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  182. package/dist/ui/webUi-featureOptions/utils.mjs +213 -0
  183. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  184. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  185. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  186. package/dist/ui/webUi-featureOptions/views/nav.mjs +341 -0
  187. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  188. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  189. package/dist/ui/webUi-featureOptions.mjs +702 -0
  190. package/dist/ui/webUi.mjs +225 -88
  191. package/dist/util.d.ts +679 -45
  192. package/dist/util.js +830 -77
  193. package/dist/util.js.map +1 -1
  194. package/package.json +33 -15
  195. package/build/eslint-rules.mjs +0 -511
  196. package/dist/featureoptions.d.ts +0 -264
  197. package/dist/featureoptions.js +0 -480
  198. package/dist/featureoptions.js.map +0 -1
  199. package/dist/mqttclient.d.ts +0 -178
  200. package/dist/mqttclient.js +0 -310
  201. package/dist/mqttclient.js.map +0 -1
  202. package/dist/ui/featureoptions.js +0 -480
  203. package/dist/ui/featureoptions.js.map +0 -1
  204. package/dist/ui/webUi-featureoptions.mjs +0 -3765
@@ -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
+ }
@@ -5,10 +5,13 @@
5
5
  {
6
6
  "compilerOptions": {
7
7
 
8
- "allowSyntheticDefaultImports": true,
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.Iterator"
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:
@@ -1,79 +1,128 @@
1
+ import type { Nullable } from "./util.ts";
2
+ import type { Writable } from "node:stream";
1
3
  /**
2
- * Backpressure-aware write queue for Node.js writable streams.
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.
3
6
  *
4
- * This module provides a utility class for writing data to a writable stream while respecting backpressure signals. It maintains an internal queue and serializes writes,
5
- * pausing when the stream signals it isn't ready and resuming when it drains. This is particularly useful when feeding data from an event-driven source (e.g., fMP4
6
- * segments from a livestream) into a process stdin that may not consume data as fast as it arrives.
7
+ * Separate from the "writer has aborted" and "underlying stream is dead" failure modes so callers can distinguish backpressure-overflow (back off and retry later)
8
+ * from terminal failures (give up or escalate) by type rather than by inspecting error message text.
7
9
  *
8
- * @module
10
+ * @category Utilities
9
11
  */
10
- import type { Nullable } from "./util.js";
11
- import type { Writable } from "node:stream";
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
+ }
12
27
  /**
13
- * A backpressure-aware write queue that serializes writes to a writable stream, pausing when the stream signals backpressure and resuming on drain.
28
+ * Construction-time options for {@link BackpressureWriter}.
14
29
  *
15
- * The stream is resolved lazily via a getter function on each write, allowing the writer to be created before the stream exists and to handle stream replacement across
16
- * process restarts.
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).
17
50
  *
18
51
  * @example
19
52
  *
20
53
  * ```ts
21
- * // Create a writer that feeds segments to an FFmpeg process stdin.
22
- * const writer = new BackpressureWriter(() => ffmpegProcess.stdin ?? null);
54
+ * import { BackpressureWriter } from "homebridge-plugin-utils";
23
55
  *
24
- * // Enqueue segments as they arrive from a livestream source.
25
- * livestream.on("segment", (segment) => writer.write(segment));
56
+ * await using writer = new BackpressureWriter(() => ffmpegProcess.stdin ?? null, { signal: session.signal });
26
57
  *
27
- * // When the session ends, close the writer to release pending data.
28
- * writer.close();
58
+ * // Each write awaits its own flush; concurrent writes queue behind prior ones.
59
+ * await writer.write(segmentOne);
60
+ * await writer.write(segmentTwo);
29
61
  * ```
30
62
  *
31
63
  * @category Utilities
32
64
  */
33
- export declare class BackpressureWriter {
34
- private drainListener;
35
- private drainStream;
36
- private readonly getStream;
37
- private isClosed;
38
- private isWriting;
39
- private readonly onWrite;
40
- private readonly queue;
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;
41
72
  /**
42
- * Creates a new backpressure-aware write queue.
73
+ * Construct a new backpressure-aware writer.
43
74
  *
44
- * @param getStream - A function that returns the current writable stream, or `null` if the stream is unavailable. Evaluated on each write attempt, allowing the
45
- * writer to be created before the stream exists or to track a stream that changes across process restarts. For a static stream, wrap it in an
46
- * arrow function: `() => stream`.
47
- * @param onWrite - Optional. A callback invoked after each segment is successfully written to the underlying stream. Useful for tracking write statistics.
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}.
48
77
  *
49
78
  * @example
50
79
  *
51
80
  * ```ts
52
- * // Lazy resolution...the stream is resolved on each write.
53
- * const writer = new BackpressureWriter(() => this.ffmpegProcess?.stdin ?? null, () => segmentCount++);
54
- *
55
- * // Static stream...wrap in an arrow function.
56
- * const writer = new BackpressureWriter(() => stream);
81
+ * await using writer = new BackpressureWriter(() => this.ffmpegProcess?.stdin ?? null, { highWaterMark: 64, signal: session.signal });
57
82
  * ```
58
83
  */
59
- constructor(getStream: () => Nullable<Writable>, onWrite?: () => void);
84
+ constructor(streamProvider: () => Nullable<Writable>, init?: BackpressureWriterInit);
60
85
  /**
61
- * Enqueues data to be written to the stream. If the stream is available and not under backpressure, the data is written immediately. Otherwise, it is queued and
62
- * written when the stream signals it is ready via the drain event.
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.
63
97
  *
64
- * @param data - The buffer to write to the stream.
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.
65
113
  *
66
- * @returns Returns `true` if the data was accepted (stream is available), `false` if the stream is unavailable or the writer has been closed.
114
+ * @returns A promise that resolves once the drain loop has fully exited.
67
115
  */
68
- write(data: Buffer): boolean;
116
+ [Symbol.asyncDispose](): Promise<void>;
69
117
  /**
70
- * Closes the writer, clearing any pending data and removing drain listeners. After closing, all subsequent writes are rejected. This should be called when the
71
- * underlying stream is being shut down or the session is ending.
118
+ * `true` once `this.signal` has aborted. Derived from the signal; no independent state.
72
119
  */
73
- close(): void;
120
+ get aborted(): boolean;
74
121
  /**
75
- * Returns the number of segments currently queued and waiting to be written.
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.
76
125
  */
77
126
  get pending(): number;
78
- private processQueue;
79
127
  }
128
+ //# sourceMappingURL=backpressure.d.ts.map