@cj-tech-master/excelts 4.2.0 → 4.2.1-canary.20260111102127.f808a37

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 (241) hide show
  1. package/THIRD_PARTY_NOTICES.md +0 -31
  2. package/dist/browser/index.browser.d.ts +1 -0
  3. package/dist/browser/index.browser.js +12 -0
  4. package/dist/{types/modules/archive → browser/modules/archive/compression}/compress.base.d.ts +1 -0
  5. package/dist/browser/modules/archive/{compress.base.js → compression/compress.base.js} +2 -1
  6. package/dist/{types/modules/archive → browser/modules/archive/compression}/compress.browser.d.ts +10 -8
  7. package/dist/{esm/modules/archive → browser/modules/archive/compression}/compress.browser.js +18 -19
  8. package/dist/browser/modules/archive/{compress.d.ts → compression/compress.d.ts} +2 -2
  9. package/dist/browser/modules/archive/{compress.js → compression/compress.js} +1 -1
  10. package/dist/browser/modules/archive/{crc32.browser.d.ts → compression/crc32.browser.d.ts} +1 -1
  11. package/dist/browser/modules/archive/{crc32.d.ts → compression/crc32.d.ts} +1 -1
  12. package/dist/browser/modules/archive/{crc32.js → compression/crc32.js} +1 -1
  13. package/dist/browser/modules/archive/{deflate-fallback.js → compression/deflate-fallback.js} +1 -1
  14. package/dist/browser/modules/archive/{streaming-compress.browser.d.ts → compression/streaming-compress.browser.d.ts} +2 -2
  15. package/dist/browser/modules/archive/{streaming-compress.browser.js → compression/streaming-compress.browser.js} +3 -3
  16. package/dist/browser/modules/archive/{streaming-compress.d.ts → compression/streaming-compress.d.ts} +2 -2
  17. package/dist/browser/modules/archive/{streaming-compress.js → compression/streaming-compress.js} +2 -2
  18. package/dist/browser/modules/archive/defaults.d.ts +1 -0
  19. package/dist/browser/modules/archive/defaults.js +6 -3
  20. package/dist/browser/modules/archive/index.base.d.ts +4 -4
  21. package/dist/browser/modules/archive/index.base.js +3 -6
  22. package/dist/browser/modules/archive/index.browser.d.ts +3 -4
  23. package/dist/browser/modules/archive/index.browser.js +3 -7
  24. package/dist/browser/modules/archive/index.d.ts +3 -4
  25. package/dist/browser/modules/archive/index.js +3 -5
  26. package/dist/browser/modules/archive/internal/byte-queue.d.ts +33 -0
  27. package/dist/browser/modules/archive/internal/byte-queue.js +407 -0
  28. package/dist/browser/modules/archive/io/archive-sink.d.ts +9 -0
  29. package/dist/browser/modules/archive/io/archive-sink.js +77 -0
  30. package/dist/browser/modules/archive/io/archive-source.d.ts +8 -0
  31. package/dist/browser/modules/archive/io/archive-source.js +107 -0
  32. package/dist/browser/modules/archive/{extract.d.ts → unzip/extract.d.ts} +2 -2
  33. package/dist/browser/modules/archive/unzip/index.d.ts +40 -0
  34. package/dist/browser/modules/archive/unzip/index.js +164 -0
  35. package/dist/browser/modules/archive/{parse.base.d.ts → unzip/stream.base.d.ts} +58 -3
  36. package/dist/browser/modules/archive/unzip/stream.base.js +1022 -0
  37. package/dist/browser/modules/archive/{parse.browser.d.ts → unzip/stream.browser.d.ts} +1 -1
  38. package/dist/browser/modules/archive/{parse.browser.js → unzip/stream.browser.js} +376 -110
  39. package/dist/browser/modules/archive/{parse.d.ts → unzip/stream.d.ts} +2 -2
  40. package/dist/{esm/modules/archive/parse.js → browser/modules/archive/unzip/stream.js} +7 -6
  41. package/dist/{types/modules/archive → browser/modules/archive/unzip}/zip-parser.d.ts +1 -1
  42. package/dist/{esm/modules/archive → browser/modules/archive/unzip}/zip-parser.js +38 -24
  43. package/dist/browser/modules/archive/utils/async-queue.d.ts +7 -0
  44. package/dist/browser/modules/archive/utils/async-queue.js +103 -0
  45. package/dist/browser/modules/archive/utils/bytes.js +16 -16
  46. package/dist/browser/modules/archive/utils/compressibility.d.ts +10 -0
  47. package/dist/browser/modules/archive/utils/compressibility.js +57 -0
  48. package/dist/browser/modules/archive/utils/parse-buffer.js +21 -23
  49. package/dist/browser/modules/archive/utils/pattern-scanner.d.ts +21 -0
  50. package/dist/browser/modules/archive/utils/pattern-scanner.js +27 -0
  51. package/dist/browser/modules/archive/utils/timestamps.js +62 -1
  52. package/dist/browser/modules/archive/utils/zip-extra-fields.d.ts +1 -1
  53. package/dist/browser/modules/archive/utils/zip-extra-fields.js +26 -14
  54. package/dist/browser/modules/archive/zip/index.d.ts +42 -0
  55. package/dist/browser/modules/archive/zip/index.js +157 -0
  56. package/dist/browser/modules/archive/{streaming-zip.d.ts → zip/stream.d.ts} +28 -5
  57. package/dist/browser/modules/archive/{streaming-zip.js → zip/stream.js} +192 -48
  58. package/dist/browser/modules/archive/zip/zip-bytes.d.ts +73 -0
  59. package/dist/browser/modules/archive/zip/zip-bytes.js +239 -0
  60. package/dist/{esm/modules/archive → browser/modules/archive/zip}/zip-entry-metadata.js +3 -3
  61. package/dist/browser/modules/archive/{zip-records.d.ts → zip-spec/zip-records.d.ts} +20 -0
  62. package/dist/browser/modules/archive/zip-spec/zip-records.js +126 -0
  63. package/dist/browser/modules/excel/form-control.d.ts +2 -0
  64. package/dist/browser/modules/excel/form-control.js +54 -16
  65. package/dist/browser/modules/excel/stream/workbook-reader.browser.js +1 -1
  66. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +1 -1
  67. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +1 -1
  68. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +17 -3
  69. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +3 -6
  70. package/dist/browser/modules/excel/xlsx/xlsx.js +1 -1
  71. package/dist/browser/modules/stream/streams.browser.d.ts +28 -28
  72. package/dist/browser/modules/stream/streams.browser.js +850 -697
  73. package/dist/browser/modules/stream/streams.js +140 -58
  74. package/dist/cjs/modules/archive/{compress.base.js → compression/compress.base.js} +2 -1
  75. package/dist/cjs/modules/archive/{compress.browser.js → compression/compress.browser.js} +18 -19
  76. package/dist/cjs/modules/archive/{compress.js → compression/compress.js} +1 -1
  77. package/dist/cjs/modules/archive/{crc32.js → compression/crc32.js} +1 -1
  78. package/dist/cjs/modules/archive/{deflate-fallback.js → compression/deflate-fallback.js} +1 -1
  79. package/dist/cjs/modules/archive/{streaming-compress.browser.js → compression/streaming-compress.browser.js} +3 -3
  80. package/dist/cjs/modules/archive/{streaming-compress.js → compression/streaming-compress.js} +2 -2
  81. package/dist/cjs/modules/archive/defaults.js +7 -4
  82. package/dist/cjs/modules/archive/index.base.js +9 -19
  83. package/dist/cjs/modules/archive/index.browser.js +4 -10
  84. package/dist/cjs/modules/archive/index.js +4 -8
  85. package/dist/cjs/modules/archive/internal/byte-queue.js +411 -0
  86. package/dist/cjs/modules/archive/io/archive-sink.js +82 -0
  87. package/dist/cjs/modules/archive/io/archive-source.js +114 -0
  88. package/dist/cjs/modules/archive/unzip/index.js +170 -0
  89. package/dist/cjs/modules/archive/unzip/stream.base.js +1044 -0
  90. package/dist/cjs/modules/archive/{parse.browser.js → unzip/stream.browser.js} +377 -111
  91. package/dist/cjs/modules/archive/{parse.js → unzip/stream.js} +9 -8
  92. package/dist/cjs/modules/archive/{zip-parser.js → unzip/zip-parser.js} +47 -33
  93. package/dist/cjs/modules/archive/utils/async-queue.js +106 -0
  94. package/dist/cjs/modules/archive/utils/bytes.js +16 -16
  95. package/dist/cjs/modules/archive/utils/compressibility.js +60 -0
  96. package/dist/cjs/modules/archive/utils/parse-buffer.js +21 -23
  97. package/dist/cjs/modules/archive/utils/pattern-scanner.js +31 -0
  98. package/dist/cjs/modules/archive/utils/timestamps.js +64 -3
  99. package/dist/cjs/modules/archive/utils/zip-extra-fields.js +26 -14
  100. package/dist/cjs/modules/archive/zip/index.js +162 -0
  101. package/dist/cjs/modules/archive/{streaming-zip.js → zip/stream.js} +194 -50
  102. package/dist/cjs/modules/archive/zip/zip-bytes.js +242 -0
  103. package/dist/cjs/modules/archive/{zip-entry-metadata.js → zip/zip-entry-metadata.js} +5 -5
  104. package/dist/cjs/modules/archive/zip-spec/zip-records.js +136 -0
  105. package/dist/cjs/modules/excel/form-control.js +54 -16
  106. package/dist/cjs/modules/excel/stream/workbook-reader.browser.js +2 -2
  107. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +4 -4
  108. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +17 -3
  109. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +6 -9
  110. package/dist/cjs/modules/excel/xlsx/xlsx.js +2 -2
  111. package/dist/cjs/modules/stream/streams.browser.js +850 -697
  112. package/dist/cjs/modules/stream/streams.js +140 -58
  113. package/dist/esm/index.browser.js +12 -0
  114. package/dist/esm/modules/archive/{compress.base.js → compression/compress.base.js} +2 -1
  115. package/dist/{browser/modules/archive → esm/modules/archive/compression}/compress.browser.js +18 -19
  116. package/dist/esm/modules/archive/{compress.js → compression/compress.js} +1 -1
  117. package/dist/esm/modules/archive/{crc32.js → compression/crc32.js} +1 -1
  118. package/dist/esm/modules/archive/{deflate-fallback.js → compression/deflate-fallback.js} +1 -1
  119. package/dist/esm/modules/archive/{streaming-compress.browser.js → compression/streaming-compress.browser.js} +3 -3
  120. package/dist/esm/modules/archive/{streaming-compress.js → compression/streaming-compress.js} +2 -2
  121. package/dist/esm/modules/archive/defaults.js +6 -3
  122. package/dist/esm/modules/archive/index.base.js +3 -6
  123. package/dist/esm/modules/archive/index.browser.js +3 -7
  124. package/dist/esm/modules/archive/index.js +3 -5
  125. package/dist/esm/modules/archive/internal/byte-queue.js +407 -0
  126. package/dist/esm/modules/archive/io/archive-sink.js +77 -0
  127. package/dist/esm/modules/archive/io/archive-source.js +107 -0
  128. package/dist/esm/modules/archive/unzip/index.js +164 -0
  129. package/dist/esm/modules/archive/unzip/stream.base.js +1022 -0
  130. package/dist/esm/modules/archive/{parse.browser.js → unzip/stream.browser.js} +376 -110
  131. package/dist/{browser/modules/archive/parse.js → esm/modules/archive/unzip/stream.js} +7 -6
  132. package/dist/{browser/modules/archive → esm/modules/archive/unzip}/zip-parser.js +38 -24
  133. package/dist/esm/modules/archive/utils/async-queue.js +103 -0
  134. package/dist/esm/modules/archive/utils/bytes.js +16 -16
  135. package/dist/esm/modules/archive/utils/compressibility.js +57 -0
  136. package/dist/esm/modules/archive/utils/parse-buffer.js +21 -23
  137. package/dist/esm/modules/archive/utils/pattern-scanner.js +27 -0
  138. package/dist/esm/modules/archive/utils/timestamps.js +62 -1
  139. package/dist/esm/modules/archive/utils/zip-extra-fields.js +26 -14
  140. package/dist/esm/modules/archive/zip/index.js +157 -0
  141. package/dist/esm/modules/archive/{streaming-zip.js → zip/stream.js} +192 -48
  142. package/dist/esm/modules/archive/zip/zip-bytes.js +239 -0
  143. package/dist/{browser/modules/archive → esm/modules/archive/zip}/zip-entry-metadata.js +3 -3
  144. package/dist/esm/modules/archive/zip-spec/zip-records.js +126 -0
  145. package/dist/esm/modules/excel/form-control.js +54 -16
  146. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +1 -1
  147. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +1 -1
  148. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +17 -3
  149. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +3 -6
  150. package/dist/esm/modules/excel/xlsx/xlsx.js +1 -1
  151. package/dist/esm/modules/stream/streams.browser.js +850 -697
  152. package/dist/esm/modules/stream/streams.js +140 -58
  153. package/dist/iife/THIRD_PARTY_NOTICES.md +81 -0
  154. package/dist/iife/excelts.iife.js +4777 -2863
  155. package/dist/iife/excelts.iife.js.map +1 -1
  156. package/dist/iife/excelts.iife.min.js +103 -31
  157. package/dist/types/index.browser.d.ts +1 -0
  158. package/dist/{browser/modules/archive → types/modules/archive/compression}/compress.base.d.ts +1 -0
  159. package/dist/{browser/modules/archive → types/modules/archive/compression}/compress.browser.d.ts +10 -8
  160. package/dist/types/modules/archive/{streaming-compress.browser.d.ts → compression/streaming-compress.browser.d.ts} +1 -1
  161. package/dist/types/modules/archive/defaults.d.ts +1 -0
  162. package/dist/types/modules/archive/index.base.d.ts +4 -4
  163. package/dist/types/modules/archive/index.browser.d.ts +3 -4
  164. package/dist/types/modules/archive/index.d.ts +3 -4
  165. package/dist/types/modules/archive/internal/byte-queue.d.ts +33 -0
  166. package/dist/types/modules/archive/io/archive-sink.d.ts +9 -0
  167. package/dist/types/modules/archive/io/archive-source.d.ts +8 -0
  168. package/dist/types/modules/archive/unzip/index.d.ts +40 -0
  169. package/dist/types/modules/archive/{parse.base.d.ts → unzip/stream.base.d.ts} +60 -5
  170. package/dist/types/modules/archive/{parse.browser.d.ts → unzip/stream.browser.d.ts} +2 -2
  171. package/dist/types/modules/archive/{parse.d.ts → unzip/stream.d.ts} +3 -3
  172. package/dist/{browser/modules/archive → types/modules/archive/unzip}/zip-parser.d.ts +1 -1
  173. package/dist/types/modules/archive/utils/async-queue.d.ts +7 -0
  174. package/dist/types/modules/archive/utils/compressibility.d.ts +10 -0
  175. package/dist/types/modules/archive/utils/pattern-scanner.d.ts +21 -0
  176. package/dist/types/modules/archive/utils/zip-extra-fields.d.ts +1 -1
  177. package/dist/types/modules/archive/zip/index.d.ts +42 -0
  178. package/dist/types/modules/archive/{streaming-zip.d.ts → zip/stream.d.ts} +29 -6
  179. package/dist/types/modules/archive/zip/zip-bytes.d.ts +73 -0
  180. package/dist/types/modules/archive/{zip-entry-metadata.d.ts → zip/zip-entry-metadata.d.ts} +1 -1
  181. package/dist/types/modules/archive/{zip-records.d.ts → zip-spec/zip-records.d.ts} +20 -0
  182. package/dist/types/modules/excel/form-control.d.ts +2 -0
  183. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +1 -1
  184. package/dist/types/modules/stream/streams.browser.d.ts +28 -28
  185. package/package.json +10 -6
  186. package/dist/browser/modules/archive/byte-queue.d.ts +0 -18
  187. package/dist/browser/modules/archive/byte-queue.js +0 -125
  188. package/dist/browser/modules/archive/parse.base.js +0 -610
  189. package/dist/browser/modules/archive/utils/zip-extra.d.ts +0 -18
  190. package/dist/browser/modules/archive/utils/zip-extra.js +0 -68
  191. package/dist/browser/modules/archive/zip-builder.d.ts +0 -117
  192. package/dist/browser/modules/archive/zip-builder.js +0 -292
  193. package/dist/browser/modules/archive/zip-constants.d.ts +0 -18
  194. package/dist/browser/modules/archive/zip-constants.js +0 -23
  195. package/dist/browser/modules/archive/zip-records.js +0 -84
  196. package/dist/cjs/modules/archive/byte-queue.js +0 -129
  197. package/dist/cjs/modules/archive/parse.base.js +0 -632
  198. package/dist/cjs/modules/archive/utils/zip-extra.js +0 -74
  199. package/dist/cjs/modules/archive/zip-builder.js +0 -297
  200. package/dist/cjs/modules/archive/zip-constants.js +0 -26
  201. package/dist/cjs/modules/archive/zip-records.js +0 -90
  202. package/dist/esm/modules/archive/byte-queue.js +0 -125
  203. package/dist/esm/modules/archive/parse.base.js +0 -610
  204. package/dist/esm/modules/archive/utils/zip-extra.js +0 -68
  205. package/dist/esm/modules/archive/zip-builder.js +0 -292
  206. package/dist/esm/modules/archive/zip-constants.js +0 -23
  207. package/dist/esm/modules/archive/zip-records.js +0 -84
  208. package/dist/types/modules/archive/byte-queue.d.ts +0 -18
  209. package/dist/types/modules/archive/utils/zip-extra.d.ts +0 -18
  210. package/dist/types/modules/archive/zip-builder.d.ts +0 -117
  211. package/dist/types/modules/archive/zip-constants.d.ts +0 -18
  212. /package/dist/browser/modules/archive/{crc32.base.d.ts → compression/crc32.base.d.ts} +0 -0
  213. /package/dist/browser/modules/archive/{crc32.base.js → compression/crc32.base.js} +0 -0
  214. /package/dist/browser/modules/archive/{crc32.browser.js → compression/crc32.browser.js} +0 -0
  215. /package/dist/browser/modules/archive/{deflate-fallback.d.ts → compression/deflate-fallback.d.ts} +0 -0
  216. /package/dist/browser/modules/archive/{streaming-compress.base.d.ts → compression/streaming-compress.base.d.ts} +0 -0
  217. /package/dist/browser/modules/archive/{streaming-compress.base.js → compression/streaming-compress.base.js} +0 -0
  218. /package/dist/browser/modules/archive/{extract.js → unzip/extract.js} +0 -0
  219. /package/dist/browser/modules/archive/{zip-entry-metadata.d.ts → zip/zip-entry-metadata.d.ts} +0 -0
  220. /package/dist/browser/modules/archive/{zip-entry-info.d.ts → zip-spec/zip-entry-info.d.ts} +0 -0
  221. /package/dist/browser/modules/archive/{zip-entry-info.js → zip-spec/zip-entry-info.js} +0 -0
  222. /package/dist/cjs/modules/archive/{crc32.base.js → compression/crc32.base.js} +0 -0
  223. /package/dist/cjs/modules/archive/{crc32.browser.js → compression/crc32.browser.js} +0 -0
  224. /package/dist/cjs/modules/archive/{streaming-compress.base.js → compression/streaming-compress.base.js} +0 -0
  225. /package/dist/cjs/modules/archive/{extract.js → unzip/extract.js} +0 -0
  226. /package/dist/cjs/modules/archive/{zip-entry-info.js → zip-spec/zip-entry-info.js} +0 -0
  227. /package/dist/esm/modules/archive/{crc32.base.js → compression/crc32.base.js} +0 -0
  228. /package/dist/esm/modules/archive/{crc32.browser.js → compression/crc32.browser.js} +0 -0
  229. /package/dist/esm/modules/archive/{streaming-compress.base.js → compression/streaming-compress.base.js} +0 -0
  230. /package/dist/esm/modules/archive/{extract.js → unzip/extract.js} +0 -0
  231. /package/dist/esm/modules/archive/{zip-entry-info.js → zip-spec/zip-entry-info.js} +0 -0
  232. /package/dist/{LICENSE → iife/LICENSE} +0 -0
  233. /package/dist/types/modules/archive/{compress.d.ts → compression/compress.d.ts} +0 -0
  234. /package/dist/types/modules/archive/{crc32.base.d.ts → compression/crc32.base.d.ts} +0 -0
  235. /package/dist/types/modules/archive/{crc32.browser.d.ts → compression/crc32.browser.d.ts} +0 -0
  236. /package/dist/types/modules/archive/{crc32.d.ts → compression/crc32.d.ts} +0 -0
  237. /package/dist/types/modules/archive/{deflate-fallback.d.ts → compression/deflate-fallback.d.ts} +0 -0
  238. /package/dist/types/modules/archive/{streaming-compress.base.d.ts → compression/streaming-compress.base.d.ts} +0 -0
  239. /package/dist/types/modules/archive/{streaming-compress.d.ts → compression/streaming-compress.d.ts} +0 -0
  240. /package/dist/types/modules/archive/{extract.d.ts → unzip/extract.d.ts} +0 -0
  241. /package/dist/types/modules/archive/{zip-entry-info.d.ts → zip-spec/zip-entry-info.d.ts} +0 -0
@@ -14,6 +14,40 @@ import { EventEmitter } from "./event-emitter.js";
14
14
  import { PullStream as StandalonePullStream } from "./pull-stream.js";
15
15
  import { BufferedStream as StandaloneBufferedStream, StringChunk as StandaloneStringChunk, BufferChunk as StandaloneBufferChunk } from "./buffered-stream.js";
16
16
  import { concatUint8Arrays, getTextDecoder, textDecoder } from "./shared.js";
17
+ const removeEmitterListener = (emitter, event, listener) => {
18
+ if (typeof emitter.off === "function") {
19
+ emitter.off(event, listener);
20
+ }
21
+ else if (typeof emitter.removeListener === "function") {
22
+ emitter.removeListener(event, listener);
23
+ }
24
+ };
25
+ const addEmitterListener = (emitter, event, listener, options) => {
26
+ if (options?.once && typeof emitter.once === "function") {
27
+ emitter.once(event, listener);
28
+ }
29
+ else {
30
+ emitter.on(event, listener);
31
+ }
32
+ return () => removeEmitterListener(emitter, event, listener);
33
+ };
34
+ const createListenerRegistry = () => {
35
+ const listeners = [];
36
+ return {
37
+ add: (emitter, event, listener) => {
38
+ listeners.push(addEmitterListener(emitter, event, listener));
39
+ },
40
+ once: (emitter, event, listener) => {
41
+ listeners.push(addEmitterListener(emitter, event, listener, { once: true }));
42
+ },
43
+ cleanup: () => {
44
+ for (let i = listeners.length - 1; i >= 0; i--) {
45
+ listeners[i]();
46
+ }
47
+ listeners.length = 0;
48
+ }
49
+ };
50
+ };
17
51
  // =============================================================================
18
52
  // Readable Stream Wrapper
19
53
  // =============================================================================
@@ -30,6 +64,7 @@ export class Readable extends EventEmitter {
30
64
  this._bufferSize = 0;
31
65
  this._reading = false;
32
66
  this._ended = false;
67
+ this._endEmitted = false;
33
68
  this._destroyed = false;
34
69
  this._errored = null;
35
70
  this._closed = false;
@@ -86,20 +121,7 @@ export class Readable extends EventEmitter {
86
121
  */
87
122
  static from(iterable, options) {
88
123
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
89
- (async () => {
90
- try {
91
- for await (const chunk of iterable) {
92
- if (!readable.push(chunk)) {
93
- // Backpressure
94
- await new Promise(resolve => setTimeout(resolve, 0));
95
- }
96
- }
97
- readable.push(null);
98
- }
99
- catch (err) {
100
- readable.destroy(err);
101
- }
102
- })();
124
+ pumpAsyncIterableToReadable(readable, toAsyncIterable(iterable));
103
125
  return readable;
104
126
  }
105
127
  /**
@@ -144,16 +166,30 @@ export class Readable extends EventEmitter {
144
166
  // Controller may already be closed
145
167
  }
146
168
  }
147
- this.emit("end");
169
+ // Emit 'end' only after buffered data is fully drained.
170
+ // This avoids premature 'end' when producers push null while paused.
171
+ if (this._bufferedLength() === 0) {
172
+ this._emitEndOnce();
173
+ }
148
174
  // Note: Don't call destroy() here, let the stream be consumed naturally
149
175
  // The reader will return done:true when it finishes reading
150
176
  return false;
151
177
  }
178
+ // Keep the internal Web ReadableStream in sync for controllable streams.
179
+ // For external Web streams (_webStreamMode=true), push() is not the data source.
180
+ if (controller && !this._webStreamMode) {
181
+ try {
182
+ controller.enqueue(chunk);
183
+ }
184
+ catch {
185
+ // Controller may be closed/errored; Node-side buffering/events still work.
186
+ }
187
+ }
152
188
  if (this._flowing) {
153
189
  // In flowing mode, emit data directly without buffering or enqueueing
154
190
  // const chunkStr = chunk instanceof Uint8Array ? new TextDecoder().decode(chunk.slice(0, 50)) : String(chunk).slice(0, 50);
155
191
  // console.log(`[Readable#${this._id}.push FLOWING] emit data size:${(chunk as any).length || (chunk as any).byteLength} start:"${chunkStr}"`);
156
- this.emit("data", chunk);
192
+ this.emit("data", this._applyEncoding(chunk));
157
193
  // Check if stream was paused during emit (backpressure from consumer)
158
194
  if (!this._flowing) {
159
195
  return false;
@@ -178,10 +214,8 @@ export class Readable extends EventEmitter {
178
214
  if (!this.objectMode) {
179
215
  this._bufferSize += this._getChunkSize(chunk);
180
216
  }
181
- // NOTE: Do NOT enqueue to Web Stream controller here!
182
- // In push mode, _buffer is the only source of data for data events.
183
- // Web Stream is only used for async iteration when not in push mode.
184
- // Enqueueing here would cause data duplication when _startReading is also running.
217
+ // NOTE: We still buffer for Node-style read()/data semantics.
218
+ // The internal Web ReadableStream is also fed via controller.enqueue() above.
185
219
  // Emit readable event when buffer goes from empty to having data
186
220
  if (wasEmpty) {
187
221
  queueMicrotask(() => this.emit("readable"));
@@ -195,6 +229,13 @@ export class Readable extends EventEmitter {
195
229
  return this._bufferSize < this.readableHighWaterMark;
196
230
  }
197
231
  }
232
+ _emitEndOnce() {
233
+ if (this._endEmitted) {
234
+ return;
235
+ }
236
+ this._endEmitted = true;
237
+ this.emit("end");
238
+ }
198
239
  /**
199
240
  * Put a chunk back at the front of the buffer
200
241
  * Note: unshift is allowed even after end, as it's used to put back already read data
@@ -219,14 +260,22 @@ export class Readable extends EventEmitter {
219
260
  if (!this.objectMode) {
220
261
  this._bufferSize -= this._getChunkSize(chunk);
221
262
  }
222
- return this._applyEncoding(chunk);
263
+ const decoded = this._applyEncoding(chunk);
264
+ if (this._ended && this._bufferedLength() === 0) {
265
+ queueMicrotask(() => this._emitEndOnce());
266
+ }
267
+ return decoded;
223
268
  }
224
269
  // For binary mode, handle size
225
270
  const chunk = this._bufferShift();
226
271
  if (!this.objectMode) {
227
272
  this._bufferSize -= this._getChunkSize(chunk);
228
273
  }
229
- return this._applyEncoding(chunk);
274
+ const decoded = this._applyEncoding(chunk);
275
+ if (this._ended && this._bufferedLength() === 0) {
276
+ queueMicrotask(() => this._emitEndOnce());
277
+ }
278
+ return decoded;
230
279
  }
231
280
  return null;
232
281
  }
@@ -332,11 +381,11 @@ export class Readable extends EventEmitter {
332
381
  if (!this.objectMode) {
333
382
  this._bufferSize -= this._getChunkSize(chunk);
334
383
  }
335
- this.emit("data", chunk);
384
+ this.emit("data", this._applyEncoding(chunk));
336
385
  }
337
386
  // If already ended, emit end event
338
387
  if (this._ended && this._bufferedLength() === 0) {
339
- this.emit("end");
388
+ this._emitEndOnce();
340
389
  }
341
390
  else if (this._read) {
342
391
  // Call user-provided read function asynchronously
@@ -384,58 +433,72 @@ export class Readable extends EventEmitter {
384
433
  // causing `instanceof Transform/Writable/Duplex` to fail even when the object
385
434
  // is a valid destination.
386
435
  const dest = destination;
387
- // Get the actual writable target.
388
- // Prefer internal `_writable` (Transform/Duplex wrappers), else treat the destination as writable-like.
389
- const candidate = dest?._writable ?? dest;
390
- const hasWrite = typeof candidate?.write === "function";
391
- const hasEnd = typeof candidate?.end === "function";
392
- const hasOn = typeof candidate?.on === "function";
393
- const hasOnce = typeof candidate?.once === "function";
394
- const hasOff = typeof candidate?.off === "function";
395
- if (!hasWrite || !hasEnd || (!hasOnce && !hasOn) || (!hasOff && !candidate?.removeListener)) {
436
+ // For event handling (drain, once, off), we need the object that emits events.
437
+ // For write/end, we must call the destination's own write()/end() methods,
438
+ // NOT the internal _writable, because Transform.write() has important logic
439
+ // (like auto-consume) that _writable.write() bypasses.
440
+ const eventTarget = dest;
441
+ const hasWrite = typeof dest?.write === "function";
442
+ const hasEnd = typeof dest?.end === "function";
443
+ const hasOn = typeof eventTarget?.on === "function";
444
+ const hasOnce = typeof eventTarget?.once === "function";
445
+ const hasOff = typeof eventTarget?.off === "function";
446
+ if (!hasWrite || !hasEnd || (!hasOnce && !hasOn) || (!hasOff && !eventTarget?.removeListener)) {
396
447
  throw new Error("Readable.pipe: invalid destination");
397
448
  }
398
- const target = candidate;
399
- this._pipeTo.push(target);
449
+ this._pipeTo.push(dest);
400
450
  // Create listeners that we can later remove
451
+ let drainListener;
452
+ const removeDrainListener = () => {
453
+ if (!drainListener) {
454
+ return;
455
+ }
456
+ if (typeof eventTarget.off === "function") {
457
+ eventTarget.off("drain", drainListener);
458
+ }
459
+ else if (typeof eventTarget.removeListener === "function") {
460
+ eventTarget.removeListener("drain", drainListener);
461
+ }
462
+ drainListener = undefined;
463
+ };
401
464
  const dataListener = (chunk) => {
402
- const canWrite = target.write(chunk);
465
+ // Call destination's write() method (not internal _writable.write())
466
+ // This ensures Transform.write() logic runs properly
467
+ const canWrite = dest.write(chunk);
403
468
  if (!canWrite) {
404
469
  this.pause();
405
- if (typeof target.once === "function") {
406
- target.once("drain", () => this.resume());
407
- }
408
- else {
409
- const resumeOnce = () => {
410
- if (typeof target.off === "function") {
411
- target.off("drain", resumeOnce);
412
- }
413
- else if (typeof target.removeListener === "function") {
414
- target.removeListener("drain", resumeOnce);
415
- }
470
+ // Install a removable, once-style drain listener.
471
+ if (!drainListener) {
472
+ drainListener = () => {
473
+ removeDrainListener();
416
474
  this.resume();
417
475
  };
418
- target.on("drain", resumeOnce);
476
+ eventTarget.on("drain", drainListener);
477
+ const entry = this._pipeListeners.get(dest);
478
+ if (entry) {
479
+ entry.drain = drainListener;
480
+ }
419
481
  }
420
482
  }
421
483
  };
422
484
  const endListener = () => {
423
- target.end();
485
+ dest.end();
424
486
  };
425
487
  const errorListener = (err) => {
426
- if (typeof target.destroy === "function") {
427
- target.destroy(err);
488
+ if (typeof dest.destroy === "function") {
489
+ dest.destroy(err);
428
490
  }
429
491
  else {
430
492
  // Best-effort: forward error to the destination if it supports events.
431
- target.emit?.("error", err);
493
+ eventTarget.emit?.("error", err);
432
494
  }
433
495
  };
434
496
  // Store listeners for later removal in unpipe
435
- this._pipeListeners.set(target, {
497
+ this._pipeListeners.set(dest, {
436
498
  data: dataListener,
437
499
  end: endListener,
438
- error: errorListener
500
+ error: errorListener,
501
+ eventTarget
439
502
  });
440
503
  this.on("data", dataListener);
441
504
  this.once("end", endListener);
@@ -458,6 +521,14 @@ export class Readable extends EventEmitter {
458
521
  this.off("data", listeners.data);
459
522
  this.off("end", listeners.end);
460
523
  this.off("error", listeners.error);
524
+ if (listeners.drain) {
525
+ if (typeof listeners.eventTarget?.off === "function") {
526
+ listeners.eventTarget.off("drain", listeners.drain);
527
+ }
528
+ else if (typeof listeners.eventTarget?.removeListener === "function") {
529
+ listeners.eventTarget.removeListener("drain", listeners.drain);
530
+ }
531
+ }
461
532
  this._pipeListeners.delete(destination);
462
533
  }
463
534
  }
@@ -469,6 +540,14 @@ export class Readable extends EventEmitter {
469
540
  this.off("data", listeners.data);
470
541
  this.off("end", listeners.end);
471
542
  this.off("error", listeners.error);
543
+ if (listeners.drain) {
544
+ if (typeof listeners.eventTarget?.off === "function") {
545
+ listeners.eventTarget.off("drain", listeners.drain);
546
+ }
547
+ else if (typeof listeners.eventTarget?.removeListener === "function") {
548
+ listeners.eventTarget.removeListener("drain", listeners.drain);
549
+ }
550
+ }
472
551
  this._pipeListeners.delete(target);
473
552
  }
474
553
  }
@@ -487,12 +566,26 @@ export class Readable extends EventEmitter {
487
566
  }
488
567
  this._destroyed = true;
489
568
  this._ended = true;
569
+ // Ensure we detach from destinations to avoid leaking listeners.
570
+ this.unpipe();
490
571
  if (error) {
491
572
  this._errored = error;
492
573
  this.emit("error", error);
493
574
  }
494
575
  if (this._reader) {
495
- this._reader.cancel().catch(() => { });
576
+ const reader = this._reader;
577
+ this._reader = null;
578
+ reader
579
+ .cancel()
580
+ .catch(() => { })
581
+ .finally(() => {
582
+ try {
583
+ reader.releaseLock();
584
+ }
585
+ catch {
586
+ // Ignore if a read is still pending
587
+ }
588
+ });
496
589
  }
497
590
  this._closed = true;
498
591
  this.emit("close");
@@ -571,18 +664,38 @@ export class Readable extends EventEmitter {
571
664
  const { done, value } = await this._reader.read();
572
665
  // Check _pushMode again after async read - if push() was called, stop reading
573
666
  if (this._pushMode) {
667
+ if (this._reader) {
668
+ const reader = this._reader;
669
+ this._reader = null;
670
+ try {
671
+ reader.releaseLock();
672
+ }
673
+ catch {
674
+ // Ignore if a read is still pending
675
+ }
676
+ }
574
677
  break;
575
678
  }
576
679
  if (done) {
577
680
  this._ended = true;
578
- this.emit("end");
681
+ this._emitEndOnce();
682
+ if (this._reader) {
683
+ const reader = this._reader;
684
+ this._reader = null;
685
+ try {
686
+ reader.releaseLock();
687
+ }
688
+ catch {
689
+ // Ignore if a read is still pending
690
+ }
691
+ }
579
692
  break;
580
693
  }
581
694
  if (value !== undefined) {
582
695
  // In flowing mode, emit data directly without buffering
583
696
  // Only buffer if not flowing (paused mode)
584
697
  if (this._flowing) {
585
- this.emit("data", value);
698
+ this.emit("data", this._applyEncoding(value));
586
699
  }
587
700
  else {
588
701
  this._buffer.push(value);
@@ -595,6 +708,16 @@ export class Readable extends EventEmitter {
595
708
  }
596
709
  catch (err) {
597
710
  this.emit("error", err);
711
+ if (this._reader) {
712
+ const reader = this._reader;
713
+ this._reader = null;
714
+ try {
715
+ reader.releaseLock();
716
+ }
717
+ catch {
718
+ // Ignore if a read is still pending
719
+ }
720
+ }
598
721
  }
599
722
  finally {
600
723
  this._reading = false;
@@ -602,7 +725,8 @@ export class Readable extends EventEmitter {
602
725
  }
603
726
  /**
604
727
  * Async iterator support
605
- * Uses Web Stream reader for non-push mode, event-based for push mode
728
+ * Uses a unified event-queue iterator with simple backpressure.
729
+ * This matches Node's behavior more closely (iterator drives flowing mode).
606
730
  */
607
731
  async *[Symbol.asyncIterator]() {
608
732
  // First yield any buffered data
@@ -611,117 +735,124 @@ export class Readable extends EventEmitter {
611
735
  if (!this.objectMode) {
612
736
  this._bufferSize -= this._getChunkSize(chunk);
613
737
  }
614
- yield chunk;
738
+ yield this._applyEncoding(chunk);
615
739
  }
616
- // If already ended, we're done
617
740
  if (this._ended) {
618
741
  return;
619
742
  }
620
- // For controllable streams (not created from external Web Stream),
621
- // use event-based iteration since data comes from push() calls
622
- if (!this._webStreamMode) {
623
- // Create a promise-based queue for incoming data
624
- const dataQueue = [];
625
- let resolveNext = null;
626
- let rejectNext = null;
627
- let done = false;
628
- let streamError = null;
629
- let dataQueueIndex = 0;
630
- const dataHandler = (chunk) => {
631
- if (resolveNext) {
632
- resolveNext(chunk);
633
- resolveNext = null;
634
- rejectNext = null;
635
- }
636
- else {
637
- dataQueue.push(chunk);
638
- }
639
- };
640
- const endHandler = () => {
641
- done = true;
642
- if (resolveNext) {
643
- resolveNext(null);
644
- resolveNext = null;
645
- rejectNext = null;
646
- }
647
- };
648
- const errorHandler = (err) => {
649
- done = true;
650
- streamError = err;
651
- if (rejectNext) {
652
- rejectNext(err);
653
- resolveNext = null;
654
- rejectNext = null;
655
- }
656
- };
657
- const closeHandler = () => {
658
- // If stream closed without end event (e.g., after destroy()),
659
- // treat it as done
660
- done = true;
661
- if (resolveNext) {
662
- resolveNext(null);
663
- resolveNext = null;
664
- rejectNext = null;
665
- }
666
- };
667
- this.on("data", dataHandler);
668
- this.on("end", endHandler);
669
- this.on("error", errorHandler);
670
- this.on("close", closeHandler);
671
- try {
672
- // Enter flowing mode
673
- this.resume();
674
- while (!done || dataQueueIndex < dataQueue.length) {
675
- // Check for error before processing
676
- if (streamError) {
677
- throw streamError;
678
- }
679
- if (dataQueueIndex < dataQueue.length) {
680
- const chunk = dataQueue[dataQueueIndex++];
681
- if (dataQueueIndex >= 1024 && dataQueueIndex * 2 >= dataQueue.length) {
682
- dataQueue.splice(0, dataQueueIndex);
683
- dataQueueIndex = 0;
684
- }
685
- yield chunk;
686
- }
687
- else if (!done) {
688
- const chunk = await new Promise((resolve, reject) => {
689
- resolveNext = resolve;
690
- rejectNext = reject;
691
- });
692
- if (chunk !== null) {
693
- yield chunk;
694
- }
695
- }
696
- }
697
- // Check for error after loop
698
- if (streamError) {
699
- throw streamError;
700
- }
743
+ const highWaterMark = this.readableHighWaterMark;
744
+ const lowWaterMark = Math.max(0, Math.floor(highWaterMark / 2));
745
+ const chunkSizeForBackpressure = (chunk) => {
746
+ if (this.objectMode) {
747
+ return 1;
701
748
  }
702
- finally {
703
- this.off("data", dataHandler);
704
- this.off("end", endHandler);
705
- this.off("error", errorHandler);
706
- this.off("close", closeHandler);
749
+ if (chunk instanceof Uint8Array) {
750
+ return chunk.byteLength;
707
751
  }
708
- return;
709
- }
710
- // For Web Stream mode, use the underlying reader
711
- if (!this._reader) {
712
- this._reader = this._stream.getReader();
713
- }
752
+ if (typeof chunk === "string") {
753
+ return chunk.length;
754
+ }
755
+ return 1;
756
+ };
757
+ const dataQueue = [];
758
+ let dataQueueIndex = 0;
759
+ let queuedSize = 0;
760
+ let resolveNext = null;
761
+ let rejectNext = null;
762
+ let done = false;
763
+ let pausedByIterator = false;
764
+ let streamError = null;
765
+ const dataHandler = (chunk) => {
766
+ // data events are already encoding-aware; do not decode again here.
767
+ if (resolveNext) {
768
+ resolveNext(chunk);
769
+ resolveNext = null;
770
+ rejectNext = null;
771
+ }
772
+ else {
773
+ dataQueue.push(chunk);
774
+ }
775
+ queuedSize += chunkSizeForBackpressure(chunk);
776
+ if (!pausedByIterator && queuedSize >= highWaterMark) {
777
+ pausedByIterator = true;
778
+ this.pause();
779
+ }
780
+ };
781
+ const endHandler = () => {
782
+ done = true;
783
+ if (resolveNext) {
784
+ resolveNext(null);
785
+ resolveNext = null;
786
+ rejectNext = null;
787
+ }
788
+ };
789
+ const closeHandler = () => {
790
+ done = true;
791
+ if (resolveNext) {
792
+ resolveNext(null);
793
+ resolveNext = null;
794
+ rejectNext = null;
795
+ }
796
+ };
797
+ const errorHandler = (err) => {
798
+ done = true;
799
+ streamError = err;
800
+ if (rejectNext) {
801
+ rejectNext(err);
802
+ resolveNext = null;
803
+ rejectNext = null;
804
+ }
805
+ };
806
+ this.on("data", dataHandler);
807
+ this.on("end", endHandler);
808
+ this.on("error", errorHandler);
809
+ this.on("close", closeHandler);
714
810
  try {
811
+ // Iterator consumption should drive the stream.
812
+ this.resume();
715
813
  while (true) {
716
- const { done, value } = await this._reader.read();
814
+ if (streamError) {
815
+ throw streamError;
816
+ }
817
+ if (dataQueueIndex < dataQueue.length) {
818
+ const chunk = dataQueue[dataQueueIndex++];
819
+ queuedSize -= chunkSizeForBackpressure(chunk);
820
+ if (dataQueueIndex >= 1024 && dataQueueIndex * 2 >= dataQueue.length) {
821
+ dataQueue.splice(0, dataQueueIndex);
822
+ dataQueueIndex = 0;
823
+ }
824
+ if (pausedByIterator && queuedSize <= lowWaterMark && !done && !this._destroyed) {
825
+ pausedByIterator = false;
826
+ this.resume();
827
+ }
828
+ yield chunk;
829
+ continue;
830
+ }
717
831
  if (done) {
718
832
  break;
719
833
  }
720
- yield value;
834
+ const chunk = await new Promise((resolve, reject) => {
835
+ resolveNext = resolve;
836
+ rejectNext = reject;
837
+ });
838
+ if (chunk !== null) {
839
+ queuedSize -= chunkSizeForBackpressure(chunk);
840
+ if (pausedByIterator && queuedSize <= lowWaterMark && !done && !this._destroyed) {
841
+ pausedByIterator = false;
842
+ this.resume();
843
+ }
844
+ yield chunk;
845
+ }
846
+ }
847
+ if (streamError) {
848
+ throw streamError;
721
849
  }
722
850
  }
723
851
  finally {
724
- this._reader.releaseLock();
852
+ this.off("data", dataHandler);
853
+ this.off("end", endHandler);
854
+ this.off("error", errorHandler);
855
+ this.off("close", closeHandler);
725
856
  }
726
857
  }
727
858
  /**
@@ -940,6 +1071,32 @@ export class Readable extends EventEmitter {
940
1071
  return stream;
941
1072
  }
942
1073
  }
1074
+ function toAsyncIterable(iterable) {
1075
+ if (iterable && typeof iterable[Symbol.asyncIterator] === "function") {
1076
+ return iterable;
1077
+ }
1078
+ return (async function* () {
1079
+ for (const item of iterable) {
1080
+ yield item;
1081
+ }
1082
+ })();
1083
+ }
1084
+ function pumpAsyncIterableToReadable(readable, iterable) {
1085
+ (async () => {
1086
+ try {
1087
+ for await (const chunk of iterable) {
1088
+ if (!readable.push(chunk)) {
1089
+ // Simple backpressure: yield to consumer.
1090
+ await new Promise(resolve => setTimeout(resolve, 0));
1091
+ }
1092
+ }
1093
+ readable.push(null);
1094
+ }
1095
+ catch (err) {
1096
+ readable.destroy(err);
1097
+ }
1098
+ })();
1099
+ }
943
1100
  // =============================================================================
944
1101
  // Writable Stream Wrapper
945
1102
  // =============================================================================
@@ -957,10 +1114,12 @@ export class Writable extends EventEmitter {
957
1114
  this._closed = false;
958
1115
  this._pendingWrites = 0;
959
1116
  this._writableLength = 0;
1117
+ this._needDrain = false;
960
1118
  this._corked = 0;
961
1119
  this._corkedChunks = [];
962
1120
  this._defaultEncoding = "utf8";
963
1121
  this._aborted = false;
1122
+ this._ownsStream = false;
964
1123
  this.objectMode = options?.objectMode ?? false;
965
1124
  this.writableHighWaterMark = options?.highWaterMark ?? 16384;
966
1125
  this.autoDestroy = options?.autoDestroy ?? true;
@@ -976,8 +1135,10 @@ export class Writable extends EventEmitter {
976
1135
  }
977
1136
  if (options?.stream) {
978
1137
  this._stream = options.stream;
1138
+ this._ownsStream = false;
979
1139
  }
980
1140
  else {
1141
+ this._ownsStream = true;
981
1142
  // Create bound references to instance properties/methods for use in WritableStream callbacks
982
1143
  const getWriteFunc = () => this._writeFunc;
983
1144
  const getFinalFunc = () => this._finalFunc;
@@ -1084,25 +1245,37 @@ export class Writable extends EventEmitter {
1084
1245
  this._writableLength += chunkSize;
1085
1246
  return this._writableLength < this.writableHighWaterMark;
1086
1247
  }
1087
- return this._doWrite(chunk, cb);
1248
+ const ok = this._doWrite(chunk, cb);
1249
+ if (!ok) {
1250
+ this._needDrain = true;
1251
+ }
1252
+ return ok;
1088
1253
  }
1089
1254
  _doWrite(chunk, callback) {
1090
1255
  // Track pending writes for writableLength
1091
1256
  const chunkSize = this._getChunkSize(chunk);
1092
1257
  this._pendingWrites++;
1093
1258
  this._writableLength += chunkSize;
1094
- this._getWriter()
1259
+ const writer = this._getWriter();
1260
+ writer
1095
1261
  .write(chunk)
1096
1262
  .then(() => {
1097
1263
  this._pendingWrites--;
1098
1264
  this._writableLength -= chunkSize;
1099
- this.emit("drain");
1265
+ if (this._needDrain && this._writableLength < this.writableHighWaterMark) {
1266
+ this._needDrain = false;
1267
+ this.emit("drain");
1268
+ }
1100
1269
  callback?.(null);
1101
1270
  })
1102
1271
  .catch(err => {
1103
1272
  this._pendingWrites--;
1104
1273
  this._writableLength -= chunkSize;
1105
- this.emit("error", err);
1274
+ // Avoid double-emitting if we're already in an errored/destroyed state.
1275
+ if (!this._destroyed) {
1276
+ this._errored = err;
1277
+ this.emit("error", err);
1278
+ }
1106
1279
  callback?.(err);
1107
1280
  });
1108
1281
  // Return false if we've exceeded high water mark (for backpressure)
@@ -1133,12 +1306,29 @@ export class Writable extends EventEmitter {
1133
1306
  : callback;
1134
1307
  const finish = async () => {
1135
1308
  try {
1309
+ const writer = this._getWriter();
1136
1310
  if (chunk !== undefined) {
1137
- await this._getWriter().write(chunk);
1311
+ await writer.write(chunk);
1312
+ }
1313
+ await writer.close();
1314
+ if (this._writer === writer) {
1315
+ this._writer = null;
1316
+ try {
1317
+ writer.releaseLock();
1318
+ }
1319
+ catch {
1320
+ // Ignore
1321
+ }
1322
+ }
1323
+ // If we own the underlying Web WritableStream, its `close()` handler already
1324
+ // emits finish/close. For external streams, we must emit finish ourselves.
1325
+ if (!this._ownsStream) {
1326
+ this._finished = true;
1327
+ this.emit("finish");
1328
+ if (this.emitClose) {
1329
+ this.emit("close");
1330
+ }
1138
1331
  }
1139
- await this._getWriter().close();
1140
- this._finished = true;
1141
- this.emit("finish");
1142
1332
  if (cb) {
1143
1333
  cb();
1144
1334
  }
@@ -1159,12 +1349,24 @@ export class Writable extends EventEmitter {
1159
1349
  }
1160
1350
  this._destroyed = true;
1161
1351
  this._ended = true;
1162
- if (error) {
1352
+ if (error && !this._errored) {
1163
1353
  this._errored = error;
1164
1354
  this.emit("error", error);
1165
1355
  }
1166
1356
  if (this._writer) {
1167
- this._writer.abort(error).catch(() => { });
1357
+ const writer = this._writer;
1358
+ this._writer = null;
1359
+ writer
1360
+ .abort(error)
1361
+ .catch(() => { })
1362
+ .finally(() => {
1363
+ try {
1364
+ writer.releaseLock();
1365
+ }
1366
+ catch {
1367
+ // Ignore
1368
+ }
1369
+ });
1168
1370
  }
1169
1371
  this._closed = true;
1170
1372
  this.emit("close");
@@ -1315,332 +1517,259 @@ export function normalizeWritable(stream) {
1315
1517
  */
1316
1518
  export class Transform extends EventEmitter {
1317
1519
  /**
1318
- * Push data to the readable side (Node.js compatibility)
1319
- * Can be called from within transform callback
1520
+ * Push data to the readable side (Node.js compatibility).
1521
+ * Intended to be called from within transform/flush.
1320
1522
  */
1321
1523
  push(chunk) {
1322
- if (chunk === null) {
1323
- return false;
1324
- }
1325
- if (this._transformController) {
1326
- // If we're in a transform callback, enqueue directly
1327
- this._transformController.enqueue(chunk);
1328
- }
1329
- else {
1330
- // Otherwise buffer for later
1331
- this._pushBuffer.push(chunk);
1332
- }
1333
- return true;
1524
+ return this._readable.push(chunk);
1334
1525
  }
1335
1526
  constructor(options) {
1336
1527
  super();
1337
- this._ended = false;
1338
1528
  this._destroyed = false;
1529
+ this._ended = false;
1339
1530
  this._errored = false;
1340
- // Buffer for Node.js style push() calls during transform
1341
- this._pushBuffer = [];
1342
- // Controller for enqueueing pushed data (set during transform execution)
1343
- this._transformController = null;
1344
- // Buffer for writes that occur after end() but before writable is closed
1345
- this._pendingEndWrites = [];
1346
- // Whether end() has been called but writable not yet closed
1347
- this._endPending = false;
1348
- // Track if we've already set up data forwarding
1349
1531
  this._dataForwardingSetup = false;
1350
- /** @internal - whether we have a data event consumer */
1351
- this._hasDataConsumer = false;
1352
- /** @internal - whether we're auto-consuming the readable */
1353
- this._readableConsuming = false;
1354
- /** @internal - buffer for auto-consumed data */
1355
- this._autoConsumedBuffer = [];
1356
- this._autoConsumedBufferIndex = 0;
1357
- /** @internal - whether auto-consume has ended */
1358
- this._autoConsumeEnded = false;
1359
- /** @internal - promise that resolves when auto-consume finishes */
1360
- this._autoConsumePromise = null;
1532
+ this._endTimer = null;
1533
+ this._webStream = null;
1534
+ this._sideForwardingCleanup = null;
1361
1535
  this.objectMode = options?.objectMode ?? false;
1362
- const userTransform = options?.transform;
1363
- const userFlush = options?.flush;
1364
- // Create bound references for use in TransformStream callbacks
1365
- const setController = (ctrl) => {
1366
- this._transformController = ctrl;
1367
- };
1368
- const emitEvent = (event, ...args) => {
1369
- if (event === "error") {
1370
- // Only emit error once to prevent duplicate events
1371
- if (this._errored) {
1372
- return false;
1373
- }
1374
- this._errored = true;
1375
- // Also destroy the writable to prevent further writes
1376
- this._writable.destroy(args[0]);
1536
+ this._transformImpl = options?.transform;
1537
+ this._flushImpl = options?.flush;
1538
+ this._readable = new Readable({
1539
+ objectMode: this.objectMode
1540
+ });
1541
+ this._writable = new Writable({
1542
+ objectMode: this.objectMode,
1543
+ write: (chunk, _encoding, callback) => {
1544
+ this._runTransform(chunk)
1545
+ .then(() => callback(null))
1546
+ .catch(err => callback(err));
1547
+ },
1548
+ final: callback => {
1549
+ this._runFlush()
1550
+ .then(() => {
1551
+ this._readable.push(null);
1552
+ callback(null);
1553
+ })
1554
+ .catch(err => callback(err));
1377
1555
  }
1378
- return this.emit(event, ...args);
1379
- };
1380
- const getInstance = () => this;
1381
- // Check if subclass overrides _transform (for Node.js compatibility)
1382
- // We need to check this at runtime since the subclass constructor runs after super()
1383
- const hasSubclassTransform = () => {
1384
- // If userTransform was provided in options, use that
1385
- if (userTransform) {
1386
- return false;
1556
+ });
1557
+ this._setupSideForwarding();
1558
+ }
1559
+ _setupSideForwarding() {
1560
+ if (this._sideForwardingCleanup) {
1561
+ this._sideForwardingCleanup();
1562
+ this._sideForwardingCleanup = null;
1563
+ }
1564
+ const registry = createListenerRegistry();
1565
+ registry.once(this._readable, "end", () => this.emit("end"));
1566
+ registry.add(this._readable, "error", err => this._emitErrorOnce(err));
1567
+ registry.once(this._writable, "finish", () => this.emit("finish"));
1568
+ registry.add(this._writable, "drain", () => this.emit("drain"));
1569
+ registry.add(this._writable, "error", err => this._emitErrorOnce(err));
1570
+ this._sideForwardingCleanup = () => registry.cleanup();
1571
+ }
1572
+ _scheduleEnd() {
1573
+ if (this._destroyed || this._errored) {
1574
+ return;
1575
+ }
1576
+ if (this._writable.writableEnded) {
1577
+ return;
1578
+ }
1579
+ if (this._endTimer) {
1580
+ clearTimeout(this._endTimer);
1581
+ }
1582
+ // Defer closing to allow writes triggered during 'data' callbacks.
1583
+ this._endTimer = setTimeout(() => {
1584
+ this._endTimer = null;
1585
+ if (this._destroyed || this._errored || this._writable.writableEnded) {
1586
+ return;
1387
1587
  }
1388
- // Check if _transform is overridden (not the base class no-op)
1389
- const proto = Object.getPrototypeOf(this);
1390
- return proto._transform !== Transform.prototype._transform;
1391
- };
1392
- const hasSubclassFlush = () => {
1393
- if (userFlush) {
1394
- return false;
1588
+ this._writable.end();
1589
+ }, 0);
1590
+ }
1591
+ _emitErrorOnce(err) {
1592
+ if (this._errored) {
1593
+ return;
1594
+ }
1595
+ this._errored = true;
1596
+ const error = err instanceof Error ? err : new Error(String(err));
1597
+ this.emit("error", error);
1598
+ if (!this._destroyed) {
1599
+ this._destroyed = true;
1600
+ this._readable.destroy(error);
1601
+ this._writable.destroy(error);
1602
+ queueMicrotask(() => this.emit("close"));
1603
+ }
1604
+ }
1605
+ _hasSubclassTransform() {
1606
+ if (this._transformImpl) {
1607
+ return false;
1608
+ }
1609
+ const proto = Object.getPrototypeOf(this);
1610
+ return proto._transform !== Transform.prototype._transform;
1611
+ }
1612
+ _hasSubclassFlush() {
1613
+ if (this._flushImpl) {
1614
+ return false;
1615
+ }
1616
+ const proto = Object.getPrototypeOf(this);
1617
+ return proto._flush !== Transform.prototype._flush;
1618
+ }
1619
+ async _runTransform(chunk) {
1620
+ if (this._destroyed || this._errored) {
1621
+ throw new Error(this._errored ? "Cannot write after stream errored" : "Cannot write after stream destroyed");
1622
+ }
1623
+ try {
1624
+ if (this._hasSubclassTransform()) {
1625
+ await new Promise((resolve, reject) => {
1626
+ this._transform(chunk, "utf8", (err, data) => {
1627
+ if (err) {
1628
+ reject(err);
1629
+ return;
1630
+ }
1631
+ if (data !== undefined) {
1632
+ this.push(data);
1633
+ }
1634
+ resolve();
1635
+ });
1636
+ });
1637
+ return;
1395
1638
  }
1396
- const proto = Object.getPrototypeOf(this);
1397
- return proto._flush !== Transform.prototype._flush;
1398
- };
1399
- this._stream = new TransformStream({
1400
- transform: async (chunk, controller) => {
1401
- // Skip processing if already errored
1402
- if (this._errored) {
1403
- return;
1404
- }
1405
- try {
1406
- // Set controller for push() to use
1407
- setController(controller);
1408
- // Check for subclass _transform override first
1409
- if (hasSubclassTransform()) {
1410
- // Call subclass _transform method (Node.js style)
1411
- // _transform signature is (chunk, encoding, callback)
1412
- await new Promise((resolve, reject) => {
1413
- this._transform(chunk, "utf8", (err, data) => {
1414
- if (err) {
1415
- reject(err);
1416
- }
1417
- else {
1418
- if (data !== undefined) {
1419
- controller.enqueue(data);
1420
- }
1421
- resolve();
1422
- }
1423
- });
1424
- });
1425
- }
1426
- else if (userTransform) {
1427
- const transformParamCount = userTransform.length;
1428
- if (transformParamCount >= 3) {
1429
- // Node.js style: transform(chunk, encoding, callback)
1430
- await new Promise((resolve, reject) => {
1431
- userTransform.call(getInstance(), chunk, "utf8", (err, data) => {
1432
- if (err) {
1433
- reject(err);
1434
- }
1435
- else {
1436
- if (data !== undefined) {
1437
- controller.enqueue(data);
1438
- }
1439
- resolve();
1440
- }
1441
- });
1442
- });
1639
+ const userTransform = this._transformImpl;
1640
+ if (!userTransform) {
1641
+ this.push(chunk);
1642
+ return;
1643
+ }
1644
+ const paramCount = userTransform.length;
1645
+ if (paramCount >= 3) {
1646
+ await new Promise((resolve, reject) => {
1647
+ userTransform.call(this, chunk, "utf8", (err, data) => {
1648
+ if (err) {
1649
+ reject(err);
1650
+ return;
1443
1651
  }
1444
- else if (transformParamCount === 2) {
1445
- await new Promise((resolve, reject) => {
1446
- userTransform.call(getInstance(), chunk, (err, data) => {
1447
- if (err) {
1448
- reject(err);
1449
- }
1450
- else {
1451
- if (data !== undefined) {
1452
- controller.enqueue(data);
1453
- }
1454
- resolve();
1455
- }
1456
- });
1457
- });
1652
+ if (data !== undefined) {
1653
+ this.push(data);
1458
1654
  }
1459
- else {
1460
- // Simple style: transform(chunk) => result
1461
- const result = userTransform.call(getInstance(), chunk);
1462
- if (result && typeof result.then === "function") {
1463
- const awaitedResult = await result;
1464
- if (awaitedResult !== undefined) {
1465
- controller.enqueue(awaitedResult);
1466
- }
1467
- }
1468
- else {
1469
- if (result !== undefined) {
1470
- controller.enqueue(result);
1471
- }
1472
- }
1655
+ resolve();
1656
+ });
1657
+ });
1658
+ return;
1659
+ }
1660
+ if (paramCount === 2) {
1661
+ await new Promise((resolve, reject) => {
1662
+ userTransform.call(this, chunk, (err, data) => {
1663
+ if (err) {
1664
+ reject(err);
1665
+ return;
1473
1666
  }
1474
- }
1475
- else {
1476
- // Default: pass through
1477
- controller.enqueue(chunk);
1478
- }
1479
- }
1480
- catch (err) {
1481
- controller.error(err);
1482
- emitEvent("error", err);
1483
- }
1484
- finally {
1485
- setController(null);
1667
+ if (data !== undefined) {
1668
+ this.push(data);
1669
+ }
1670
+ resolve();
1671
+ });
1672
+ });
1673
+ return;
1674
+ }
1675
+ const result = userTransform.call(this, chunk);
1676
+ if (result && typeof result.then === "function") {
1677
+ const awaited = await result;
1678
+ if (awaited !== undefined) {
1679
+ this.push(awaited);
1486
1680
  }
1487
- },
1488
- flush: async (controller) => {
1489
- try {
1490
- setController(controller);
1491
- // Check for subclass _flush override first
1492
- if (hasSubclassFlush()) {
1493
- await new Promise((resolve, reject) => {
1494
- this._flush((err, data) => {
1495
- if (err) {
1496
- reject(err);
1497
- }
1498
- else {
1499
- if (data !== undefined) {
1500
- controller.enqueue(data);
1501
- }
1502
- resolve();
1503
- }
1504
- });
1505
- });
1506
- }
1507
- else if (userFlush) {
1508
- const flushParamCount = userFlush.length;
1509
- if (flushParamCount >= 1) {
1510
- // Node.js style: flush(callback)
1511
- await new Promise((resolve, reject) => {
1512
- userFlush.call(getInstance(), (err, data) => {
1513
- if (err) {
1514
- reject(err);
1515
- }
1516
- else {
1517
- if (data !== undefined) {
1518
- controller.enqueue(data);
1519
- }
1520
- resolve();
1521
- }
1522
- });
1523
- });
1681
+ return;
1682
+ }
1683
+ if (result !== undefined) {
1684
+ this.push(result);
1685
+ }
1686
+ }
1687
+ catch (err) {
1688
+ this._emitErrorOnce(err);
1689
+ throw err;
1690
+ }
1691
+ }
1692
+ async _runFlush() {
1693
+ if (this._destroyed || this._errored) {
1694
+ return;
1695
+ }
1696
+ try {
1697
+ if (this._hasSubclassFlush()) {
1698
+ await new Promise((resolve, reject) => {
1699
+ this._flush((err, data) => {
1700
+ if (err) {
1701
+ reject(err);
1702
+ return;
1524
1703
  }
1525
- else {
1526
- // Simple style: flush() => result
1527
- const result = userFlush.call(getInstance());
1528
- if (result && typeof result.then === "function") {
1529
- const awaitedResult = await result;
1530
- if (awaitedResult !== undefined && awaitedResult !== null) {
1531
- controller.enqueue(awaitedResult);
1532
- }
1533
- }
1534
- else {
1535
- if (result !== undefined && result !== null) {
1536
- controller.enqueue(result);
1537
- }
1538
- }
1704
+ if (data !== undefined) {
1705
+ this.push(data);
1539
1706
  }
1540
- }
1541
- // No flush defined - nothing to do
1542
- }
1543
- catch (err) {
1544
- controller.error(err);
1545
- emitEvent("error", err);
1546
- }
1547
- finally {
1548
- setController(null);
1549
- }
1707
+ resolve();
1708
+ });
1709
+ });
1710
+ return;
1550
1711
  }
1551
- });
1552
- this._readable = new Readable({
1553
- stream: this._stream.readable,
1554
- objectMode: this.objectMode
1555
- });
1556
- this._writable = new Writable({
1557
- stream: this._stream.writable,
1558
- objectMode: this.objectMode
1559
- });
1560
- // Forward non-data events (data forwarding is lazy to avoid premature flowing)
1561
- this._readable.on("end", () => this.emit("end"));
1562
- // Only forward errors if not already errored (to prevent duplicate events)
1563
- this._readable.on("error", err => {
1564
- if (!this._errored) {
1565
- this._errored = true;
1566
- this.emit("error", err);
1712
+ const userFlush = this._flushImpl;
1713
+ if (!userFlush) {
1714
+ return;
1567
1715
  }
1568
- });
1569
- this._writable.on("finish", () => this.emit("finish"));
1570
- this._writable.on("drain", () => this.emit("drain"));
1571
- // Only forward errors if not already errored (to prevent duplicate events)
1572
- this._writable.on("error", err => {
1573
- if (!this._errored) {
1574
- this._errored = true;
1575
- this.emit("error", err);
1716
+ const paramCount = userFlush.length;
1717
+ if (paramCount >= 1) {
1718
+ await new Promise((resolve, reject) => {
1719
+ userFlush.call(this, (err, data) => {
1720
+ if (err) {
1721
+ reject(err);
1722
+ return;
1723
+ }
1724
+ if (data !== undefined) {
1725
+ this.push(data);
1726
+ }
1727
+ resolve();
1728
+ });
1729
+ });
1730
+ return;
1576
1731
  }
1577
- });
1732
+ const result = userFlush.call(this);
1733
+ if (result && typeof result.then === "function") {
1734
+ const awaited = await result;
1735
+ if (awaited !== undefined && awaited !== null) {
1736
+ this.push(awaited);
1737
+ }
1738
+ return;
1739
+ }
1740
+ if (result !== undefined && result !== null) {
1741
+ this.push(result);
1742
+ }
1743
+ }
1744
+ catch (err) {
1745
+ this._emitErrorOnce(err);
1746
+ throw err;
1747
+ }
1578
1748
  }
1579
1749
  /**
1580
- * Override on to start flowing when data listener is added
1750
+ * Override on() to lazily forward readable 'data' events.
1751
+ * Avoids starting flowing mode unless requested.
1581
1752
  */
1582
1753
  on(event, listener) {
1583
- // Set up data forwarding when first external data listener is added
1584
1754
  if (event === "data" && !this._dataForwardingSetup) {
1585
1755
  this._dataForwardingSetup = true;
1586
- this._readable.on("data", data => this.emit("data", data));
1587
- }
1588
- super.on(event, listener);
1589
- // When data listener is added, mark as having consumer
1590
- // and start the readable in flowing mode
1591
- if (event === "data") {
1592
- this._hasDataConsumer = true;
1593
- this._readable.resume();
1756
+ this._readable.on("data", chunk => this.emit("data", chunk));
1594
1757
  }
1595
- return this;
1758
+ return super.on(event, listener);
1596
1759
  }
1597
1760
  write(chunk, encodingOrCallback, callback) {
1598
1761
  const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
1599
- if (this._destroyed || this._errored) {
1600
- const err = new Error(this._errored ? "Cannot write after stream errored" : "Cannot write after stream destroyed");
1601
- queueMicrotask(() => this.emit("error", err));
1602
- cb?.(err);
1603
- return false;
1604
- }
1605
- // Ensure readable is being consumed to allow transform to execute
1606
- // This matches Node.js behavior where transform executes immediately on write
1607
- // Only auto-consume if no explicit consumer (data listener or pipe)
1608
- if (!this._readableConsuming && !this._hasDataConsumer) {
1609
- this._readableConsuming = true;
1610
- this._startAutoConsume();
1611
- }
1612
- // If end() was called but writable not yet closed, buffer the write
1613
- // This allows writes during data event handlers to be processed
1614
- if (this._endPending) {
1615
- this._pendingEndWrites.push({ chunk, callback: cb });
1616
- return true;
1762
+ // If end() has been requested, keep the close deferred as long as writes continue.
1763
+ if (this._ended && !this._writable.writableEnded) {
1764
+ this._scheduleEnd();
1617
1765
  }
1618
1766
  return this._writable.write(chunk, cb);
1619
1767
  }
1620
- /** @internal - auto-consume readable to allow transform to execute */
1621
- _startAutoConsume() {
1622
- this._autoConsumePromise = (async () => {
1623
- try {
1624
- for await (const chunk of this._readable) {
1625
- // Buffer the data for later retrieval
1626
- this._autoConsumedBuffer.push(chunk);
1627
- // Also emit data event for listeners
1628
- this.emit("data", chunk);
1629
- }
1630
- this._autoConsumeEnded = true;
1631
- this.emit("end");
1632
- }
1633
- catch (err) {
1634
- this.emit("error", err);
1635
- }
1636
- })();
1637
- }
1638
1768
  end(chunkOrCallback, encodingOrCallback, callback) {
1639
1769
  if (this._ended) {
1640
1770
  return this;
1641
1771
  }
1642
1772
  this._ended = true;
1643
- this._endPending = true;
1644
1773
  const chunk = typeof chunkOrCallback === "function" ? undefined : chunkOrCallback;
1645
1774
  const cb = typeof chunkOrCallback === "function"
1646
1775
  ? chunkOrCallback
@@ -1653,18 +1782,7 @@ export class Transform extends EventEmitter {
1653
1782
  if (chunk !== undefined) {
1654
1783
  this._writable.write(chunk);
1655
1784
  }
1656
- // Use setTimeout(0) instead of queueMicrotask to ensure all transform
1657
- // processing and data events complete before we close the writable.
1658
- // Microtasks run before the TransformStream processes data.
1659
- setTimeout(() => {
1660
- // Process any writes that occurred during data events
1661
- for (const { chunk: pendingChunk, callback } of this._pendingEndWrites) {
1662
- this._writable.write(pendingChunk, callback);
1663
- }
1664
- this._pendingEndWrites = [];
1665
- this._endPending = false;
1666
- this._writable.end();
1667
- }, 0);
1785
+ this._scheduleEnd();
1668
1786
  return this;
1669
1787
  }
1670
1788
  /**
@@ -1674,11 +1792,9 @@ export class Transform extends EventEmitter {
1674
1792
  return this._readable.read(size);
1675
1793
  }
1676
1794
  /**
1677
- * Pipe to another stream (writable, transform, or duplex)
1795
+ * Pipe readable side to destination
1678
1796
  */
1679
1797
  pipe(destination) {
1680
- // Mark as having consumer to prevent auto-consume conflict
1681
- this._hasDataConsumer = true;
1682
1798
  return this._readable.pipe(destination);
1683
1799
  }
1684
1800
  /**
@@ -1716,6 +1832,10 @@ export class Transform extends EventEmitter {
1716
1832
  return;
1717
1833
  }
1718
1834
  this._destroyed = true;
1835
+ if (this._sideForwardingCleanup) {
1836
+ this._sideForwardingCleanup();
1837
+ this._sideForwardingCleanup = null;
1838
+ }
1719
1839
  this._readable.destroy(error);
1720
1840
  this._writable.destroy(error);
1721
1841
  queueMicrotask(() => this.emit("close"));
@@ -1724,7 +1844,44 @@ export class Transform extends EventEmitter {
1724
1844
  * Get the underlying Web TransformStream
1725
1845
  */
1726
1846
  get webStream() {
1727
- return this._stream;
1847
+ if (this._webStream) {
1848
+ return this._webStream;
1849
+ }
1850
+ // Web Streams interop layer.
1851
+ const iterator = this[Symbol.asyncIterator]();
1852
+ const readable = new ReadableStream({
1853
+ pull: async (controller) => {
1854
+ const { done, value } = await iterator.next();
1855
+ if (done) {
1856
+ controller.close();
1857
+ return;
1858
+ }
1859
+ controller.enqueue(value);
1860
+ },
1861
+ cancel: reason => {
1862
+ this.destroy(reason instanceof Error ? reason : new Error(String(reason)));
1863
+ }
1864
+ });
1865
+ const writable = new WritableStream({
1866
+ write: chunk => new Promise((resolve, reject) => {
1867
+ this.write(chunk, err => {
1868
+ if (err) {
1869
+ reject(err);
1870
+ }
1871
+ else {
1872
+ resolve();
1873
+ }
1874
+ });
1875
+ }),
1876
+ close: () => new Promise(resolve => {
1877
+ this.end(() => resolve());
1878
+ }),
1879
+ abort: reason => {
1880
+ this.destroy(reason instanceof Error ? reason : new Error(String(reason)));
1881
+ }
1882
+ });
1883
+ this._webStream = { readable, writable };
1884
+ return this._webStream;
1728
1885
  }
1729
1886
  get readable() {
1730
1887
  return this._readable.readable;
@@ -1766,19 +1923,6 @@ export class Transform extends EventEmitter {
1766
1923
  * Async iterator support
1767
1924
  */
1768
1925
  async *[Symbol.asyncIterator]() {
1769
- // If auto-consume is running, wait for it to finish and use its buffer
1770
- if (this._autoConsumePromise) {
1771
- await this._autoConsumePromise;
1772
- // Yield all buffered data
1773
- while (this._autoConsumedBufferIndex < this._autoConsumedBuffer.length) {
1774
- yield this._autoConsumedBuffer[this._autoConsumedBufferIndex++];
1775
- }
1776
- // Reset when drained to avoid prefix growth
1777
- this._autoConsumedBuffer.length = 0;
1778
- this._autoConsumedBufferIndex = 0;
1779
- return;
1780
- }
1781
- // Otherwise delegate to readable's iterator
1782
1926
  yield* this._readable[Symbol.asyncIterator]();
1783
1927
  }
1784
1928
  // =========================================================================
@@ -1789,23 +1933,18 @@ export class Transform extends EventEmitter {
1789
1933
  */
1790
1934
  static fromWeb(webStream, options) {
1791
1935
  const transform = new Transform(options);
1792
- // Connect the web stream - set the internal _stream property
1793
- transform._stream = webStream;
1936
+ transform._webStream = webStream;
1794
1937
  // Replace internal streams with the ones from the web stream
1795
1938
  const newReadable = Readable.fromWeb(webStream.readable, { objectMode: options?.objectMode });
1796
1939
  const newWritable = Writable.fromWeb(webStream.writable, { objectMode: options?.objectMode });
1797
- // Remove old event listeners before replacing
1798
- transform._readable.removeAllListeners();
1799
- transform._writable.removeAllListeners();
1940
+ if (transform._sideForwardingCleanup) {
1941
+ transform._sideForwardingCleanup();
1942
+ transform._sideForwardingCleanup = null;
1943
+ }
1800
1944
  transform._readable = newReadable;
1801
1945
  transform._writable = newWritable;
1802
- // Re-connect event forwarding
1803
- newReadable.on("data", (data) => transform.emit("data", data));
1804
- newReadable.on("end", () => transform.emit("end"));
1805
- newReadable.on("error", (err) => transform.emit("error", err));
1806
- newWritable.on("finish", () => transform.emit("finish"));
1807
- newWritable.on("drain", () => transform.emit("drain"));
1808
- newWritable.on("error", (err) => transform.emit("error", err));
1946
+ // Re-connect event forwarding (data forwarding remains lazy via Transform.on)
1947
+ transform._setupSideForwarding();
1809
1948
  return transform;
1810
1949
  }
1811
1950
  /**
@@ -1861,7 +2000,13 @@ export class Duplex extends EventEmitter {
1861
2000
  callback();
1862
2001
  }
1863
2002
  });
1864
- readable.on("error", err => duplex.emit("error", err));
2003
+ const onError = (err) => {
2004
+ duplex.emit("error", err);
2005
+ };
2006
+ const cleanupError = addEmitterListener(readable, "error", onError);
2007
+ addEmitterListener(readable, "end", cleanupError, { once: true });
2008
+ addEmitterListener(readable, "close", cleanupError, { once: true });
2009
+ addEmitterListener(sink, "finish", cleanupError, { once: true });
1865
2010
  readable.pipe(sink);
1866
2011
  };
1867
2012
  // If it has readable and/or writable properties
@@ -1869,21 +2014,25 @@ export class Duplex extends EventEmitter {
1869
2014
  source !== null &&
1870
2015
  "readable" in source &&
1871
2016
  "writable" in source) {
1872
- const duplex = new Duplex();
1873
2017
  const pair = source;
1874
- if (pair.readable) {
1875
- forwardReadableToDuplex(pair.readable, duplex);
1876
- }
1877
- if (pair.writable) {
1878
- return new Duplex({
1879
- objectMode: duplex.writableObjectMode,
1880
- write(chunk, encoding, callback) {
2018
+ // Create one duplex that can bridge both sides.
2019
+ // (Previous behavior returned a new writable-only Duplex and dropped the readable side.)
2020
+ const duplex = new Duplex({
2021
+ readableObjectMode: pair.readable?.readableObjectMode,
2022
+ writableObjectMode: pair.writable?.writableObjectMode,
2023
+ write: pair.writable
2024
+ ? (chunk, encoding, callback) => {
1881
2025
  pair.writable.write(chunk, encoding, callback);
1882
- },
1883
- final(callback) {
2026
+ }
2027
+ : undefined,
2028
+ final: pair.writable
2029
+ ? callback => {
1884
2030
  pair.writable.end(callback);
1885
2031
  }
1886
- });
2032
+ : undefined
2033
+ });
2034
+ if (pair.readable) {
2035
+ forwardReadableToDuplex(pair.readable, duplex);
1887
2036
  }
1888
2037
  return duplex;
1889
2038
  }
@@ -1921,9 +2070,22 @@ export class Duplex extends EventEmitter {
1921
2070
  */
1922
2071
  static fromWeb(pair, options) {
1923
2072
  const duplex = new Duplex(options);
1924
- // Replace internal streams
1925
- duplex._readable = new Readable({ stream: pair.readable });
1926
- duplex._writable = new Writable({ stream: pair.writable });
2073
+ const newReadable = new Readable({
2074
+ stream: pair.readable,
2075
+ objectMode: duplex.readableObjectMode
2076
+ });
2077
+ const newWritable = new Writable({
2078
+ stream: pair.writable,
2079
+ objectMode: duplex.writableObjectMode
2080
+ });
2081
+ if (duplex._sideForwardingCleanup) {
2082
+ duplex._sideForwardingCleanup();
2083
+ duplex._sideForwardingCleanup = null;
2084
+ }
2085
+ duplex._readable = newReadable;
2086
+ duplex._writable = newWritable;
2087
+ // Re-wire event forwarding (data forwarding remains lazy via Duplex.on)
2088
+ duplex._setupSideForwarding();
1927
2089
  return duplex;
1928
2090
  }
1929
2091
  /**
@@ -1939,6 +2101,7 @@ export class Duplex extends EventEmitter {
1939
2101
  super();
1940
2102
  // Track if we've already set up data forwarding
1941
2103
  this._dataForwardingSetup = false;
2104
+ this._sideForwardingCleanup = null;
1942
2105
  this.allowHalfOpen = options?.allowHalfOpen ?? true;
1943
2106
  // Support shorthand objectMode option
1944
2107
  const objectMode = options?.objectMode ?? false;
@@ -1955,23 +2118,31 @@ export class Duplex extends EventEmitter {
1955
2118
  write: options?.write?.bind(this),
1956
2119
  final: options?.final?.bind(this)
1957
2120
  });
2121
+ this._setupSideForwarding();
2122
+ }
2123
+ _setupSideForwarding() {
2124
+ if (this._sideForwardingCleanup) {
2125
+ this._sideForwardingCleanup();
2126
+ this._sideForwardingCleanup = null;
2127
+ }
2128
+ const registry = createListenerRegistry();
1958
2129
  // Forward non-data events (data forwarding is lazy to avoid premature flowing)
1959
- this._readable.on("end", () => {
2130
+ registry.once(this._readable, "end", () => {
1960
2131
  this.emit("end");
1961
- // If not allowHalfOpen, end the writable side too
1962
2132
  if (!this.allowHalfOpen) {
1963
2133
  this._writable.end();
1964
2134
  }
1965
2135
  });
1966
- this._readable.on("error", err => this.emit("error", err));
1967
- this._writable.on("finish", () => this.emit("finish"));
1968
- this._writable.on("drain", () => this.emit("drain"));
1969
- this._writable.on("close", () => {
1970
- // If not allowHalfOpen, destroy the readable side too
2136
+ registry.add(this._readable, "error", err => this.emit("error", err));
2137
+ registry.add(this._writable, "error", err => this.emit("error", err));
2138
+ registry.once(this._writable, "finish", () => this.emit("finish"));
2139
+ registry.add(this._writable, "drain", () => this.emit("drain"));
2140
+ registry.once(this._writable, "close", () => {
1971
2141
  if (!this.allowHalfOpen && !this._readable.destroyed) {
1972
2142
  this._readable.destroy();
1973
2143
  }
1974
2144
  });
2145
+ this._sideForwardingCleanup = () => registry.cleanup();
1975
2146
  }
1976
2147
  /**
1977
2148
  * Override on() to set up data forwarding lazily
@@ -2090,6 +2261,10 @@ export class Duplex extends EventEmitter {
2090
2261
  * Destroy both sides
2091
2262
  */
2092
2263
  destroy(error) {
2264
+ if (this._sideForwardingCleanup) {
2265
+ this._sideForwardingCleanup();
2266
+ this._sideForwardingCleanup = null;
2267
+ }
2093
2268
  this._readable.destroy(error);
2094
2269
  this._writable.destroy(error);
2095
2270
  return this;
@@ -2252,36 +2427,16 @@ export class BufferedStream extends StandaloneBufferedStream {
2252
2427
  * Create a readable stream with custom read implementation
2253
2428
  */
2254
2429
  export function createReadable(options) {
2255
- const readable = new Readable(options);
2256
- // Override read behavior if provided
2257
- if (options?.read) {
2258
- const originalRead = readable.read.bind(readable);
2259
- readable.read = function (size) {
2260
- options.read(size ?? 16384);
2261
- return originalRead(size);
2262
- };
2263
- }
2264
- return readable;
2430
+ // Readable already supports Node-style `read()` via the constructor option.
2431
+ // Keep this helper minimal to avoid accidental double-read behavior.
2432
+ return new Readable(options);
2265
2433
  }
2266
2434
  /**
2267
2435
  * Create a readable stream from an async iterable
2268
2436
  */
2269
2437
  export function createReadableFromAsyncIterable(iterable, options) {
2270
2438
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
2271
- (async () => {
2272
- try {
2273
- for await (const chunk of iterable) {
2274
- if (!readable.push(chunk)) {
2275
- // Backpressure - wait a bit
2276
- await new Promise(resolve => setTimeout(resolve, 0));
2277
- }
2278
- }
2279
- readable.push(null);
2280
- }
2281
- catch (err) {
2282
- readable.destroy(err);
2283
- }
2284
- })();
2439
+ pumpAsyncIterableToReadable(readable, iterable);
2285
2440
  return readable;
2286
2441
  }
2287
2442
  /**
@@ -2310,38 +2465,8 @@ export function createReadableFromArray(data, options) {
2310
2465
  * Create a writable stream with custom write implementation
2311
2466
  */
2312
2467
  export function createWritable(options) {
2313
- // Create a custom WritableStream with user's handlers
2314
- const stream = new WritableStream({
2315
- write: async (chunk) => {
2316
- if (options?.write) {
2317
- return new Promise((resolve, reject) => {
2318
- options.write(chunk, "utf8", err => {
2319
- if (err) {
2320
- reject(err);
2321
- }
2322
- else {
2323
- resolve();
2324
- }
2325
- });
2326
- });
2327
- }
2328
- },
2329
- close: async () => {
2330
- if (options?.final) {
2331
- return new Promise((resolve, reject) => {
2332
- options.final(err => {
2333
- if (err) {
2334
- reject(err);
2335
- }
2336
- else {
2337
- resolve();
2338
- }
2339
- });
2340
- });
2341
- }
2342
- }
2343
- });
2344
- return new Writable({ ...options, stream });
2468
+ // Writable already supports Node-style `write()` / `final()` via the constructor.
2469
+ return new Writable(options);
2345
2470
  }
2346
2471
  /**
2347
2472
  * Create a transform stream from a transform function
@@ -2476,11 +2601,17 @@ export function pipeline(...args) {
2476
2601
  const transforms = normalized.slice(1, -1);
2477
2602
  let completed = false;
2478
2603
  const allStreams = [source, ...transforms, destination];
2479
- const cleanup = (error) => {
2604
+ const registry = createListenerRegistry();
2605
+ let onAbort;
2606
+ const cleanupWithSignal = (error) => {
2480
2607
  if (completed) {
2481
2608
  return;
2482
2609
  }
2483
2610
  completed = true;
2611
+ registry.cleanup();
2612
+ if (onAbort && options.signal) {
2613
+ options.signal.removeEventListener("abort", onAbort);
2614
+ }
2484
2615
  // Destroy all streams on error
2485
2616
  if (error) {
2486
2617
  for (const stream of allStreams) {
@@ -2497,12 +2628,11 @@ export function pipeline(...args) {
2497
2628
  // Handle abort signal
2498
2629
  if (options.signal) {
2499
2630
  if (options.signal.aborted) {
2500
- cleanup(new Error("Pipeline aborted"));
2631
+ cleanupWithSignal(new Error("Pipeline aborted"));
2501
2632
  return;
2502
2633
  }
2503
- options.signal.addEventListener("abort", () => {
2504
- cleanup(new Error("Pipeline aborted"));
2505
- });
2634
+ onAbort = () => cleanupWithSignal(new Error("Pipeline aborted"));
2635
+ options.signal.addEventListener("abort", onAbort);
2506
2636
  }
2507
2637
  // Chain the streams
2508
2638
  let current = source;
@@ -2516,13 +2646,35 @@ export function pipeline(...args) {
2516
2646
  }
2517
2647
  else {
2518
2648
  // Don't end destination
2519
- current.on("data", chunk => destination.write(chunk));
2649
+ let paused = false;
2650
+ let waitingForDrain = false;
2651
+ const onDrain = () => {
2652
+ waitingForDrain = false;
2653
+ if (paused && typeof current.resume === "function") {
2654
+ paused = false;
2655
+ current.resume();
2656
+ }
2657
+ };
2658
+ const onData = (chunk) => {
2659
+ const ok = destination.write(chunk);
2660
+ if (!ok && !waitingForDrain) {
2661
+ waitingForDrain = true;
2662
+ if (!paused && typeof current.pause === "function") {
2663
+ paused = true;
2664
+ current.pause();
2665
+ }
2666
+ registry.once(destination, "drain", onDrain);
2667
+ }
2668
+ };
2669
+ const onEnd = () => cleanupWithSignal();
2670
+ registry.add(current, "data", onData);
2671
+ registry.once(current, "end", onEnd);
2520
2672
  }
2521
2673
  // Handle completion
2522
- destination.on("finish", () => cleanup());
2674
+ registry.once(destination, "finish", () => cleanupWithSignal());
2523
2675
  // Handle errors on all streams
2524
2676
  for (const stream of allStreams) {
2525
- stream.on("error", (err) => cleanup(err));
2677
+ registry.once(stream, "error", (err) => cleanupWithSignal(err));
2526
2678
  }
2527
2679
  });
2528
2680
  // If callback provided, use it
@@ -2562,11 +2714,20 @@ export function finished(stream, optionsOrCallback, callback) {
2562
2714
  const promise = new Promise((resolve, reject) => {
2563
2715
  const normalizedStream = toBrowserPipelineStream(stream);
2564
2716
  let resolved = false;
2717
+ const registry = createListenerRegistry();
2718
+ let onAbort;
2719
+ const cleanup = () => {
2720
+ registry.cleanup();
2721
+ if (onAbort && options.signal) {
2722
+ options.signal.removeEventListener("abort", onAbort);
2723
+ }
2724
+ };
2565
2725
  const done = (err) => {
2566
2726
  if (resolved) {
2567
2727
  return;
2568
2728
  }
2569
2729
  resolved = true;
2730
+ cleanup();
2570
2731
  if (err && !options.error) {
2571
2732
  reject(err);
2572
2733
  }
@@ -2580,9 +2741,8 @@ export function finished(stream, optionsOrCallback, callback) {
2580
2741
  done(new Error("Aborted"));
2581
2742
  return;
2582
2743
  }
2583
- options.signal.addEventListener("abort", () => {
2584
- done(new Error("Aborted"));
2585
- });
2744
+ onAbort = () => done(new Error("Aborted"));
2745
+ options.signal.addEventListener("abort", onAbort);
2586
2746
  }
2587
2747
  const checkReadable = options.readable !== false;
2588
2748
  const checkWritable = options.writable !== false;
@@ -2597,13 +2757,13 @@ export function finished(stream, optionsOrCallback, callback) {
2597
2757
  }
2598
2758
  // Listen for events
2599
2759
  if (checkWritable) {
2600
- normalizedStream.on("finish", () => done());
2760
+ registry.once(normalizedStream, "finish", () => done());
2601
2761
  }
2602
2762
  if (checkReadable) {
2603
- normalizedStream.on("end", () => done());
2763
+ registry.once(normalizedStream, "end", () => done());
2604
2764
  }
2605
- normalizedStream.on("error", (err) => done(err));
2606
- normalizedStream.on("close", () => done());
2765
+ registry.once(normalizedStream, "error", (err) => done(err));
2766
+ registry.once(normalizedStream, "close", () => done());
2607
2767
  });
2608
2768
  // If callback provided, use it
2609
2769
  if (cb) {
@@ -2625,38 +2785,8 @@ export async function streamToPromise(stream) {
2625
2785
  * (Browser equivalent of Node.js streamToBuffer)
2626
2786
  */
2627
2787
  export async function streamToUint8Array(stream) {
2628
- let iterable;
2629
- if (isReadableStream(stream)) {
2630
- iterable = Readable.fromWeb(stream);
2631
- }
2632
- else if (isAsyncIterable(stream)) {
2633
- iterable = stream;
2634
- }
2635
- else {
2636
- throw new Error("streamToUint8Array: unsupported stream type");
2637
- }
2638
- const chunks = [];
2639
- let totalLength = 0;
2640
- for await (const chunk of iterable) {
2641
- chunks.push(chunk);
2642
- totalLength += chunk.length;
2643
- }
2644
- // Fast paths
2645
- const len = chunks.length;
2646
- if (len === 0) {
2647
- return new Uint8Array(0);
2648
- }
2649
- if (len === 1) {
2650
- return chunks[0];
2651
- }
2652
- // Use precalculated total length
2653
- const result = new Uint8Array(totalLength);
2654
- let offset = 0;
2655
- for (let i = 0; i < len; i++) {
2656
- result.set(chunks[i], offset);
2657
- offset += chunks[i].length;
2658
- }
2659
- return result;
2788
+ const { chunks, totalLength } = await collectStreamChunks(stream);
2789
+ return concatWithLength(chunks, totalLength);
2660
2790
  }
2661
2791
  /**
2662
2792
  * Alias for streamToUint8Array (Node.js compatibility)
@@ -2666,8 +2796,10 @@ export const streamToBuffer = streamToUint8Array;
2666
2796
  * Collect all data from a readable stream into a string
2667
2797
  */
2668
2798
  export async function streamToString(stream, encoding) {
2669
- const buffer = await streamToUint8Array(stream);
2670
- return getTextDecoder(encoding).decode(buffer);
2799
+ const { chunks, totalLength } = await collectStreamChunks(stream);
2800
+ const combined = concatWithLength(chunks, totalLength);
2801
+ const decoder = encoding ? getTextDecoder(encoding) : textDecoder;
2802
+ return decoder.decode(combined);
2671
2803
  }
2672
2804
  /**
2673
2805
  * Drain a stream (consume all data without processing)
@@ -2755,14 +2887,19 @@ export function addAbortSignal(signal, stream) {
2755
2887
  stream.destroy(new Error("Aborted"));
2756
2888
  return stream;
2757
2889
  }
2890
+ const cleanup = () => {
2891
+ signal.removeEventListener("abort", onAbort);
2892
+ removeEmitterListener(stream, "close", onClose);
2893
+ };
2758
2894
  const onAbort = () => {
2895
+ cleanup();
2759
2896
  stream.destroy(new Error("Aborted"));
2760
2897
  };
2761
- signal.addEventListener("abort", onAbort, { once: true });
2762
- // Clean up when stream is destroyed
2763
- stream.on("close", () => {
2764
- signal.removeEventListener("abort", onAbort);
2765
- });
2898
+ const onClose = () => {
2899
+ cleanup();
2900
+ };
2901
+ signal.addEventListener("abort", onAbort);
2902
+ addEmitterListener(stream, "close", onClose, { once: true });
2766
2903
  return stream;
2767
2904
  }
2768
2905
  /**
@@ -2771,60 +2908,68 @@ export function addAbortSignal(signal, stream) {
2771
2908
  export function createDuplex(options) {
2772
2909
  const readableObjectMode = options?.readableObjectMode ?? options?.objectMode;
2773
2910
  const writableObjectMode = options?.writableObjectMode ?? options?.objectMode;
2911
+ const underlyingWritable = options?.writable;
2774
2912
  const duplex = new Duplex({
2775
2913
  allowHalfOpen: options?.allowHalfOpen,
2776
2914
  readableHighWaterMark: options?.readableHighWaterMark,
2777
2915
  writableHighWaterMark: options?.writableHighWaterMark,
2778
2916
  readableObjectMode,
2779
- writableObjectMode
2917
+ writableObjectMode,
2918
+ read: options?.read,
2919
+ write: options?.write ??
2920
+ (underlyingWritable
2921
+ ? (chunk, encoding, callback) => {
2922
+ if (typeof underlyingWritable.write === "function") {
2923
+ underlyingWritable.write(chunk, encoding, callback);
2924
+ return;
2925
+ }
2926
+ // Best-effort sync sink
2927
+ try {
2928
+ underlyingWritable.write?.(chunk);
2929
+ callback(null);
2930
+ }
2931
+ catch (err) {
2932
+ callback(err);
2933
+ }
2934
+ }
2935
+ : undefined),
2936
+ final: options?.final ??
2937
+ (underlyingWritable
2938
+ ? (callback) => {
2939
+ if (typeof underlyingWritable.end === "function") {
2940
+ underlyingWritable.end((err) => callback(err ?? null));
2941
+ }
2942
+ else {
2943
+ underlyingWritable.end?.();
2944
+ callback(null);
2945
+ }
2946
+ }
2947
+ : undefined)
2780
2948
  });
2781
- // If custom readable/writable provided, pipe them
2949
+ // If an underlying readable is provided, forward it into the duplex readable side.
2782
2950
  if (options?.readable) {
2783
2951
  const readable = options.readable;
2784
- readable.on?.("data", (chunk) => duplex.push(chunk));
2785
- readable.on?.("end", () => duplex.push(null));
2786
- readable.on?.("error", (err) => duplex.destroy(err));
2787
- }
2788
- if (options?.writable) {
2789
- const writable = options.writable;
2790
- duplex.on("data", (chunk) => writable.write?.(chunk));
2791
- duplex.on("finish", () => writable.end?.());
2792
- }
2793
- // If custom read/write/final provided, override methods
2794
- if (options?.write) {
2795
- const _originalWrite = duplex.write.bind(duplex); // Keep bound reference for potential future use
2796
- duplex.write = function (chunk, encodingOrCallback, callback) {
2797
- const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : "utf8";
2798
- const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : (callback ?? (() => { }));
2799
- options.write.call(duplex, chunk, encoding, cb);
2800
- return true;
2801
- };
2802
- }
2803
- if (options?.final) {
2804
- const originalEnd = duplex.end.bind(duplex);
2805
- duplex.end = function (chunkOrCallback, encodingOrCallback, callback) {
2806
- const cb = typeof chunkOrCallback === "function"
2807
- ? chunkOrCallback
2808
- : typeof encodingOrCallback === "function"
2809
- ? encodingOrCallback
2810
- : (callback ?? (() => { }));
2811
- if (chunkOrCallback !== undefined && typeof chunkOrCallback !== "function") {
2812
- duplex.write(chunkOrCallback);
2813
- }
2814
- // Call custom final handler
2815
- options.final.call(duplex, (err) => {
2816
- if (err) {
2817
- duplex.emit("error", err);
2818
- }
2819
- else {
2820
- duplex.emit("finish");
2821
- }
2822
- // Call original end to properly close writable side
2823
- originalEnd();
2824
- cb();
2825
- });
2826
- return duplex;
2827
- };
2952
+ const sink = new Writable({
2953
+ objectMode: duplex.readableObjectMode,
2954
+ write(chunk, _encoding, callback) {
2955
+ duplex.push(chunk);
2956
+ callback(null);
2957
+ },
2958
+ final(callback) {
2959
+ duplex.push(null);
2960
+ callback(null);
2961
+ }
2962
+ });
2963
+ if (typeof readable?.on === "function") {
2964
+ const onError = (err) => {
2965
+ duplex.destroy(err);
2966
+ };
2967
+ const cleanupError = addEmitterListener(readable, "error", onError);
2968
+ addEmitterListener(readable, "end", cleanupError, { once: true });
2969
+ addEmitterListener(readable, "close", cleanupError, { once: true });
2970
+ addEmitterListener(sink, "finish", cleanupError, { once: true });
2971
+ }
2972
+ readable.pipe?.(sink);
2828
2973
  }
2829
2974
  if (options?.destroy) {
2830
2975
  const originalDestroy = duplex.destroy.bind(duplex);
@@ -2848,20 +2993,7 @@ export function createDuplex(options) {
2848
2993
  */
2849
2994
  export function createReadableFromGenerator(generator, options) {
2850
2995
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
2851
- (async () => {
2852
- try {
2853
- for await (const chunk of generator()) {
2854
- if (!readable.push(chunk)) {
2855
- // Backpressure
2856
- await new Promise(resolve => setTimeout(resolve, 0));
2857
- }
2858
- }
2859
- readable.push(null);
2860
- }
2861
- catch (err) {
2862
- readable.destroy(err);
2863
- }
2864
- })();
2996
+ pumpAsyncIterableToReadable(readable, generator());
2865
2997
  return readable;
2866
2998
  }
2867
2999
  /**
@@ -2891,8 +3023,8 @@ export function compose(...transforms) {
2891
3023
  transform: chunk => chunk
2892
3024
  });
2893
3025
  }
2894
- const isNativeTransform = (stream) => stream instanceof Transform;
2895
- if (len === 1 && isNativeTransform(transforms[0])) {
3026
+ // Preserve identity: compose(single) returns the same transform.
3027
+ if (len === 1) {
2896
3028
  return transforms[0];
2897
3029
  }
2898
3030
  // Chain the transforms: first → second → ... → last
@@ -2903,19 +3035,34 @@ export function compose(...transforms) {
2903
3035
  transforms[i].pipe(transforms[i + 1]);
2904
3036
  }
2905
3037
  class ComposedTransform extends Transform {
2906
- constructor() {
2907
- super(...arguments);
3038
+ constructor(options) {
3039
+ super(options);
2908
3040
  this._dataForwarding = false;
2909
3041
  this._endForwarding = false;
3042
+ this._dataForwardCleanup = null;
3043
+ this._endForwardCleanup = null;
3044
+ this._errorForwardCleanup = [];
3045
+ for (const t of transforms) {
3046
+ const onError = (err) => {
3047
+ this.emit("error", err);
3048
+ };
3049
+ this._errorForwardCleanup.push(addEmitterListener(t, "error", onError));
3050
+ }
2910
3051
  }
2911
3052
  on(event, listener) {
2912
3053
  if (event === "data" && !this._dataForwarding) {
2913
3054
  this._dataForwarding = true;
2914
- last.on("data", (chunk) => this.emit("data", chunk));
3055
+ const onData = (chunk) => {
3056
+ this.emit("data", chunk);
3057
+ };
3058
+ this._dataForwardCleanup = addEmitterListener(last, "data", onData);
2915
3059
  }
2916
3060
  if (event === "end" && !this._endForwarding) {
2917
3061
  this._endForwarding = true;
2918
- last.on("end", () => this.emit("end"));
3062
+ const onEnd = () => {
3063
+ this.emit("end");
3064
+ };
3065
+ this._endForwardCleanup = addEmitterListener(last, "end", onEnd, { once: true });
2919
3066
  }
2920
3067
  return super.on(event, listener);
2921
3068
  }
@@ -2941,6 +3088,18 @@ export function compose(...transforms) {
2941
3088
  return last.pipe(destination);
2942
3089
  }
2943
3090
  destroy(error) {
3091
+ if (this._dataForwardCleanup) {
3092
+ this._dataForwardCleanup();
3093
+ this._dataForwardCleanup = null;
3094
+ }
3095
+ if (this._endForwardCleanup) {
3096
+ this._endForwardCleanup();
3097
+ this._endForwardCleanup = null;
3098
+ }
3099
+ for (let i = this._errorForwardCleanup.length - 1; i >= 0; i--) {
3100
+ this._errorForwardCleanup[i]();
3101
+ }
3102
+ this._errorForwardCleanup.length = 0;
2944
3103
  for (const t of transforms) {
2945
3104
  t.destroy(error);
2946
3105
  }
@@ -2964,12 +3123,6 @@ export function compose(...transforms) {
2964
3123
  objectMode: first?.objectMode ?? true,
2965
3124
  transform: chunk => chunk
2966
3125
  });
2967
- // Forward errors from any transform
2968
- for (const t of transforms) {
2969
- t.on("error", (err) => {
2970
- composed.emit("error", err);
2971
- });
2972
- }
2973
3126
  // Reflect underlying readability/writability like the previous duck-typed wrapper
2974
3127
  Object.defineProperty(composed, "readable", {
2975
3128
  get: () => last.readable