@cj-tech-master/excelts 4.2.0-canary.20260110111632.c88c61c → 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 (232) 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/{esm/modules/archive → browser/modules/archive/compression}/compress.base.js +1 -1
  5. package/dist/{types/modules/archive → browser/modules/archive/compression}/compress.browser.d.ts +2 -8
  6. package/dist/browser/modules/archive/{compress.browser.js → compression/compress.browser.js} +3 -11
  7. package/dist/browser/modules/archive/{compress.d.ts → compression/compress.d.ts} +2 -2
  8. package/dist/{esm/modules/archive → browser/modules/archive/compression}/compress.js +1 -1
  9. package/dist/browser/modules/archive/{crc32.browser.d.ts → compression/crc32.browser.d.ts} +1 -1
  10. package/dist/browser/modules/archive/{crc32.d.ts → compression/crc32.d.ts} +1 -1
  11. package/dist/browser/modules/archive/{crc32.js → compression/crc32.js} +1 -1
  12. package/dist/browser/modules/archive/{deflate-fallback.js → compression/deflate-fallback.js} +1 -1
  13. package/dist/browser/modules/archive/{streaming-compress.browser.d.ts → compression/streaming-compress.browser.d.ts} +2 -2
  14. package/dist/browser/modules/archive/{streaming-compress.browser.js → compression/streaming-compress.browser.js} +3 -3
  15. package/dist/browser/modules/archive/{streaming-compress.d.ts → compression/streaming-compress.d.ts} +2 -2
  16. package/dist/browser/modules/archive/{streaming-compress.js → compression/streaming-compress.js} +2 -2
  17. package/dist/browser/modules/archive/defaults.d.ts +1 -0
  18. package/dist/browser/modules/archive/defaults.js +6 -3
  19. package/dist/browser/modules/archive/index.base.d.ts +4 -4
  20. package/dist/browser/modules/archive/index.base.js +3 -6
  21. package/dist/browser/modules/archive/index.browser.d.ts +3 -4
  22. package/dist/browser/modules/archive/index.browser.js +3 -7
  23. package/dist/browser/modules/archive/index.d.ts +3 -4
  24. package/dist/browser/modules/archive/index.js +3 -5
  25. package/dist/browser/modules/archive/internal/byte-queue.d.ts +33 -0
  26. package/dist/browser/modules/archive/internal/byte-queue.js +407 -0
  27. package/dist/browser/modules/archive/io/archive-sink.d.ts +9 -0
  28. package/dist/browser/modules/archive/io/archive-sink.js +77 -0
  29. package/dist/browser/modules/archive/io/archive-source.d.ts +8 -0
  30. package/dist/browser/modules/archive/io/archive-source.js +107 -0
  31. package/dist/browser/modules/archive/{extract.d.ts → unzip/extract.d.ts} +2 -2
  32. package/dist/browser/modules/archive/unzip/index.d.ts +40 -0
  33. package/dist/browser/modules/archive/unzip/index.js +164 -0
  34. package/dist/browser/modules/archive/{parse.base.d.ts → unzip/stream.base.d.ts} +36 -2
  35. package/dist/browser/modules/archive/unzip/stream.base.js +1022 -0
  36. package/dist/browser/modules/archive/{parse.browser.d.ts → unzip/stream.browser.d.ts} +1 -1
  37. package/dist/browser/modules/archive/{parse.browser.js → unzip/stream.browser.js} +371 -110
  38. package/dist/browser/modules/archive/{parse.d.ts → unzip/stream.d.ts} +2 -2
  39. package/dist/{esm/modules/archive/parse.js → browser/modules/archive/unzip/stream.js} +6 -5
  40. package/dist/browser/modules/archive/{zip-parser.d.ts → unzip/zip-parser.d.ts} +1 -1
  41. package/dist/{esm/modules/archive → browser/modules/archive/unzip}/zip-parser.js +38 -24
  42. package/dist/browser/modules/archive/utils/async-queue.d.ts +7 -0
  43. package/dist/browser/modules/archive/utils/async-queue.js +103 -0
  44. package/dist/browser/modules/archive/utils/bytes.js +16 -16
  45. package/dist/browser/modules/archive/utils/compressibility.d.ts +10 -0
  46. package/dist/browser/modules/archive/utils/compressibility.js +57 -0
  47. package/dist/browser/modules/archive/utils/parse-buffer.js +21 -23
  48. package/dist/browser/modules/archive/utils/pattern-scanner.d.ts +21 -0
  49. package/dist/browser/modules/archive/utils/pattern-scanner.js +27 -0
  50. package/dist/browser/modules/archive/utils/timestamps.js +62 -1
  51. package/dist/browser/modules/archive/utils/zip-extra-fields.d.ts +1 -1
  52. package/dist/browser/modules/archive/utils/zip-extra-fields.js +26 -14
  53. package/dist/browser/modules/archive/zip/index.d.ts +42 -0
  54. package/dist/browser/modules/archive/zip/index.js +157 -0
  55. package/dist/browser/modules/archive/{streaming-zip.d.ts → zip/stream.d.ts} +28 -5
  56. package/dist/browser/modules/archive/{streaming-zip.js → zip/stream.js} +192 -48
  57. package/dist/browser/modules/archive/zip/zip-bytes.d.ts +73 -0
  58. package/dist/browser/modules/archive/zip/zip-bytes.js +239 -0
  59. package/dist/{esm/modules/archive → browser/modules/archive/zip}/zip-entry-metadata.js +3 -3
  60. package/dist/browser/modules/archive/{zip-records.d.ts → zip-spec/zip-records.d.ts} +20 -0
  61. package/dist/browser/modules/archive/zip-spec/zip-records.js +126 -0
  62. package/dist/browser/modules/excel/stream/workbook-reader.browser.js +1 -1
  63. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +1 -1
  64. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +1 -1
  65. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +3 -6
  66. package/dist/browser/modules/excel/xlsx/xlsx.js +1 -1
  67. package/dist/browser/modules/stream/streams.browser.d.ts +28 -30
  68. package/dist/browser/modules/stream/streams.browser.js +830 -710
  69. package/dist/browser/modules/stream/streams.js +140 -58
  70. package/dist/cjs/modules/archive/{compress.base.js → compression/compress.base.js} +1 -1
  71. package/dist/cjs/modules/archive/{compress.browser.js → compression/compress.browser.js} +3 -11
  72. package/dist/cjs/modules/archive/{compress.js → compression/compress.js} +1 -1
  73. package/dist/cjs/modules/archive/{crc32.js → compression/crc32.js} +1 -1
  74. package/dist/cjs/modules/archive/{deflate-fallback.js → compression/deflate-fallback.js} +1 -1
  75. package/dist/cjs/modules/archive/{streaming-compress.browser.js → compression/streaming-compress.browser.js} +3 -3
  76. package/dist/cjs/modules/archive/{streaming-compress.js → compression/streaming-compress.js} +2 -2
  77. package/dist/cjs/modules/archive/defaults.js +7 -4
  78. package/dist/cjs/modules/archive/index.base.js +9 -19
  79. package/dist/cjs/modules/archive/index.browser.js +4 -10
  80. package/dist/cjs/modules/archive/index.js +4 -8
  81. package/dist/cjs/modules/archive/internal/byte-queue.js +411 -0
  82. package/dist/cjs/modules/archive/io/archive-sink.js +82 -0
  83. package/dist/cjs/modules/archive/io/archive-source.js +114 -0
  84. package/dist/cjs/modules/archive/unzip/index.js +170 -0
  85. package/dist/cjs/modules/archive/unzip/stream.base.js +1044 -0
  86. package/dist/cjs/modules/archive/{parse.browser.js → unzip/stream.browser.js} +372 -111
  87. package/dist/cjs/modules/archive/{parse.js → unzip/stream.js} +9 -8
  88. package/dist/cjs/modules/archive/{zip-parser.js → unzip/zip-parser.js} +47 -33
  89. package/dist/cjs/modules/archive/utils/async-queue.js +106 -0
  90. package/dist/cjs/modules/archive/utils/bytes.js +16 -16
  91. package/dist/cjs/modules/archive/utils/compressibility.js +60 -0
  92. package/dist/cjs/modules/archive/utils/parse-buffer.js +21 -23
  93. package/dist/cjs/modules/archive/utils/pattern-scanner.js +31 -0
  94. package/dist/cjs/modules/archive/utils/timestamps.js +64 -3
  95. package/dist/cjs/modules/archive/utils/zip-extra-fields.js +26 -14
  96. package/dist/cjs/modules/archive/zip/index.js +162 -0
  97. package/dist/cjs/modules/archive/{streaming-zip.js → zip/stream.js} +194 -50
  98. package/dist/cjs/modules/archive/zip/zip-bytes.js +242 -0
  99. package/dist/cjs/modules/archive/{zip-entry-metadata.js → zip/zip-entry-metadata.js} +5 -5
  100. package/dist/cjs/modules/archive/zip-spec/zip-records.js +136 -0
  101. package/dist/cjs/modules/excel/stream/workbook-reader.browser.js +2 -2
  102. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +4 -4
  103. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +6 -9
  104. package/dist/cjs/modules/excel/xlsx/xlsx.js +2 -2
  105. package/dist/cjs/modules/stream/streams.browser.js +830 -710
  106. package/dist/cjs/modules/stream/streams.js +140 -58
  107. package/dist/esm/index.browser.js +12 -0
  108. package/dist/{browser/modules/archive → esm/modules/archive/compression}/compress.base.js +1 -1
  109. package/dist/esm/modules/archive/{compress.browser.js → compression/compress.browser.js} +3 -11
  110. package/dist/{browser/modules/archive → esm/modules/archive/compression}/compress.js +1 -1
  111. package/dist/esm/modules/archive/{crc32.js → compression/crc32.js} +1 -1
  112. package/dist/esm/modules/archive/{deflate-fallback.js → compression/deflate-fallback.js} +1 -1
  113. package/dist/esm/modules/archive/{streaming-compress.browser.js → compression/streaming-compress.browser.js} +3 -3
  114. package/dist/esm/modules/archive/{streaming-compress.js → compression/streaming-compress.js} +2 -2
  115. package/dist/esm/modules/archive/defaults.js +6 -3
  116. package/dist/esm/modules/archive/index.base.js +3 -6
  117. package/dist/esm/modules/archive/index.browser.js +3 -7
  118. package/dist/esm/modules/archive/index.js +3 -5
  119. package/dist/esm/modules/archive/internal/byte-queue.js +407 -0
  120. package/dist/esm/modules/archive/io/archive-sink.js +77 -0
  121. package/dist/esm/modules/archive/io/archive-source.js +107 -0
  122. package/dist/esm/modules/archive/unzip/index.js +164 -0
  123. package/dist/esm/modules/archive/unzip/stream.base.js +1022 -0
  124. package/dist/esm/modules/archive/{parse.browser.js → unzip/stream.browser.js} +371 -110
  125. package/dist/{browser/modules/archive/parse.js → esm/modules/archive/unzip/stream.js} +6 -5
  126. package/dist/{browser/modules/archive → esm/modules/archive/unzip}/zip-parser.js +38 -24
  127. package/dist/esm/modules/archive/utils/async-queue.js +103 -0
  128. package/dist/esm/modules/archive/utils/bytes.js +16 -16
  129. package/dist/esm/modules/archive/utils/compressibility.js +57 -0
  130. package/dist/esm/modules/archive/utils/parse-buffer.js +21 -23
  131. package/dist/esm/modules/archive/utils/pattern-scanner.js +27 -0
  132. package/dist/esm/modules/archive/utils/timestamps.js +62 -1
  133. package/dist/esm/modules/archive/utils/zip-extra-fields.js +26 -14
  134. package/dist/esm/modules/archive/zip/index.js +157 -0
  135. package/dist/esm/modules/archive/{streaming-zip.js → zip/stream.js} +192 -48
  136. package/dist/esm/modules/archive/zip/zip-bytes.js +239 -0
  137. package/dist/{browser/modules/archive → esm/modules/archive/zip}/zip-entry-metadata.js +3 -3
  138. package/dist/esm/modules/archive/zip-spec/zip-records.js +126 -0
  139. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +1 -1
  140. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +1 -1
  141. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +3 -6
  142. package/dist/esm/modules/excel/xlsx/xlsx.js +1 -1
  143. package/dist/esm/modules/stream/streams.browser.js +830 -710
  144. package/dist/esm/modules/stream/streams.js +140 -58
  145. package/dist/iife/THIRD_PARTY_NOTICES.md +0 -31
  146. package/dist/iife/excelts.iife.js +6190 -4400
  147. package/dist/iife/excelts.iife.js.map +1 -1
  148. package/dist/iife/excelts.iife.min.js +103 -31
  149. package/dist/types/index.browser.d.ts +1 -0
  150. package/dist/{browser/modules/archive → types/modules/archive/compression}/compress.browser.d.ts +2 -8
  151. package/dist/types/modules/archive/{streaming-compress.browser.d.ts → compression/streaming-compress.browser.d.ts} +1 -1
  152. package/dist/types/modules/archive/defaults.d.ts +1 -0
  153. package/dist/types/modules/archive/index.base.d.ts +4 -4
  154. package/dist/types/modules/archive/index.browser.d.ts +3 -4
  155. package/dist/types/modules/archive/index.d.ts +3 -4
  156. package/dist/types/modules/archive/internal/byte-queue.d.ts +33 -0
  157. package/dist/types/modules/archive/io/archive-sink.d.ts +9 -0
  158. package/dist/types/modules/archive/io/archive-source.d.ts +8 -0
  159. package/dist/types/modules/archive/unzip/index.d.ts +40 -0
  160. package/dist/types/modules/archive/{parse.base.d.ts → unzip/stream.base.d.ts} +38 -4
  161. package/dist/types/modules/archive/{parse.browser.d.ts → unzip/stream.browser.d.ts} +2 -2
  162. package/dist/types/modules/archive/{parse.d.ts → unzip/stream.d.ts} +3 -3
  163. package/dist/types/modules/archive/{zip-parser.d.ts → unzip/zip-parser.d.ts} +1 -1
  164. package/dist/types/modules/archive/utils/async-queue.d.ts +7 -0
  165. package/dist/types/modules/archive/utils/compressibility.d.ts +10 -0
  166. package/dist/types/modules/archive/utils/pattern-scanner.d.ts +21 -0
  167. package/dist/types/modules/archive/utils/zip-extra-fields.d.ts +1 -1
  168. package/dist/types/modules/archive/zip/index.d.ts +42 -0
  169. package/dist/types/modules/archive/{streaming-zip.d.ts → zip/stream.d.ts} +29 -6
  170. package/dist/types/modules/archive/zip/zip-bytes.d.ts +73 -0
  171. package/dist/types/modules/archive/{zip-entry-metadata.d.ts → zip/zip-entry-metadata.d.ts} +1 -1
  172. package/dist/types/modules/archive/{zip-records.d.ts → zip-spec/zip-records.d.ts} +20 -0
  173. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +1 -1
  174. package/dist/types/modules/stream/streams.browser.d.ts +28 -30
  175. package/package.json +5 -1
  176. package/dist/browser/modules/archive/byte-queue.d.ts +0 -18
  177. package/dist/browser/modules/archive/byte-queue.js +0 -125
  178. package/dist/browser/modules/archive/parse.base.js +0 -644
  179. package/dist/browser/modules/archive/utils/zip-extra.d.ts +0 -18
  180. package/dist/browser/modules/archive/utils/zip-extra.js +0 -68
  181. package/dist/browser/modules/archive/zip-builder.d.ts +0 -117
  182. package/dist/browser/modules/archive/zip-builder.js +0 -292
  183. package/dist/browser/modules/archive/zip-constants.d.ts +0 -18
  184. package/dist/browser/modules/archive/zip-constants.js +0 -23
  185. package/dist/browser/modules/archive/zip-records.js +0 -84
  186. package/dist/cjs/modules/archive/byte-queue.js +0 -129
  187. package/dist/cjs/modules/archive/parse.base.js +0 -666
  188. package/dist/cjs/modules/archive/utils/zip-extra.js +0 -74
  189. package/dist/cjs/modules/archive/zip-builder.js +0 -297
  190. package/dist/cjs/modules/archive/zip-constants.js +0 -26
  191. package/dist/cjs/modules/archive/zip-records.js +0 -90
  192. package/dist/esm/modules/archive/byte-queue.js +0 -125
  193. package/dist/esm/modules/archive/parse.base.js +0 -644
  194. package/dist/esm/modules/archive/utils/zip-extra.js +0 -68
  195. package/dist/esm/modules/archive/zip-builder.js +0 -292
  196. package/dist/esm/modules/archive/zip-constants.js +0 -23
  197. package/dist/esm/modules/archive/zip-records.js +0 -84
  198. package/dist/types/modules/archive/byte-queue.d.ts +0 -18
  199. package/dist/types/modules/archive/utils/zip-extra.d.ts +0 -18
  200. package/dist/types/modules/archive/zip-builder.d.ts +0 -117
  201. package/dist/types/modules/archive/zip-constants.d.ts +0 -18
  202. /package/dist/browser/modules/archive/{compress.base.d.ts → compression/compress.base.d.ts} +0 -0
  203. /package/dist/browser/modules/archive/{crc32.base.d.ts → compression/crc32.base.d.ts} +0 -0
  204. /package/dist/browser/modules/archive/{crc32.base.js → compression/crc32.base.js} +0 -0
  205. /package/dist/browser/modules/archive/{crc32.browser.js → compression/crc32.browser.js} +0 -0
  206. /package/dist/browser/modules/archive/{deflate-fallback.d.ts → compression/deflate-fallback.d.ts} +0 -0
  207. /package/dist/browser/modules/archive/{streaming-compress.base.d.ts → compression/streaming-compress.base.d.ts} +0 -0
  208. /package/dist/browser/modules/archive/{streaming-compress.base.js → compression/streaming-compress.base.js} +0 -0
  209. /package/dist/browser/modules/archive/{extract.js → unzip/extract.js} +0 -0
  210. /package/dist/browser/modules/archive/{zip-entry-metadata.d.ts → zip/zip-entry-metadata.d.ts} +0 -0
  211. /package/dist/browser/modules/archive/{zip-entry-info.d.ts → zip-spec/zip-entry-info.d.ts} +0 -0
  212. /package/dist/browser/modules/archive/{zip-entry-info.js → zip-spec/zip-entry-info.js} +0 -0
  213. /package/dist/cjs/modules/archive/{crc32.base.js → compression/crc32.base.js} +0 -0
  214. /package/dist/cjs/modules/archive/{crc32.browser.js → compression/crc32.browser.js} +0 -0
  215. /package/dist/cjs/modules/archive/{streaming-compress.base.js → compression/streaming-compress.base.js} +0 -0
  216. /package/dist/cjs/modules/archive/{extract.js → unzip/extract.js} +0 -0
  217. /package/dist/cjs/modules/archive/{zip-entry-info.js → zip-spec/zip-entry-info.js} +0 -0
  218. /package/dist/esm/modules/archive/{crc32.base.js → compression/crc32.base.js} +0 -0
  219. /package/dist/esm/modules/archive/{crc32.browser.js → compression/crc32.browser.js} +0 -0
  220. /package/dist/esm/modules/archive/{streaming-compress.base.js → compression/streaming-compress.base.js} +0 -0
  221. /package/dist/esm/modules/archive/{extract.js → unzip/extract.js} +0 -0
  222. /package/dist/esm/modules/archive/{zip-entry-info.js → zip-spec/zip-entry-info.js} +0 -0
  223. /package/dist/types/modules/archive/{compress.base.d.ts → compression/compress.base.d.ts} +0 -0
  224. /package/dist/types/modules/archive/{compress.d.ts → compression/compress.d.ts} +0 -0
  225. /package/dist/types/modules/archive/{crc32.base.d.ts → compression/crc32.base.d.ts} +0 -0
  226. /package/dist/types/modules/archive/{crc32.browser.d.ts → compression/crc32.browser.d.ts} +0 -0
  227. /package/dist/types/modules/archive/{crc32.d.ts → compression/crc32.d.ts} +0 -0
  228. /package/dist/types/modules/archive/{deflate-fallback.d.ts → compression/deflate-fallback.d.ts} +0 -0
  229. /package/dist/types/modules/archive/{streaming-compress.base.d.ts → compression/streaming-compress.base.d.ts} +0 -0
  230. /package/dist/types/modules/archive/{streaming-compress.d.ts → compression/streaming-compress.d.ts} +0 -0
  231. /package/dist/types/modules/archive/{extract.d.ts → unzip/extract.d.ts} +0 -0
  232. /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
@@ -399,26 +448,36 @@ export class Readable extends EventEmitter {
399
448
  }
400
449
  this._pipeTo.push(dest);
401
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
+ };
402
464
  const dataListener = (chunk) => {
403
465
  // Call destination's write() method (not internal _writable.write())
404
466
  // This ensures Transform.write() logic runs properly
405
467
  const canWrite = dest.write(chunk);
406
468
  if (!canWrite) {
407
469
  this.pause();
408
- if (typeof eventTarget.once === "function") {
409
- eventTarget.once("drain", () => this.resume());
410
- }
411
- else {
412
- const resumeOnce = () => {
413
- if (typeof eventTarget.off === "function") {
414
- eventTarget.off("drain", resumeOnce);
415
- }
416
- else if (typeof eventTarget.removeListener === "function") {
417
- eventTarget.removeListener("drain", resumeOnce);
418
- }
470
+ // Install a removable, once-style drain listener.
471
+ if (!drainListener) {
472
+ drainListener = () => {
473
+ removeDrainListener();
419
474
  this.resume();
420
475
  };
421
- eventTarget.on("drain", resumeOnce);
476
+ eventTarget.on("drain", drainListener);
477
+ const entry = this._pipeListeners.get(dest);
478
+ if (entry) {
479
+ entry.drain = drainListener;
480
+ }
422
481
  }
423
482
  }
424
483
  };
@@ -438,7 +497,8 @@ export class Readable extends EventEmitter {
438
497
  this._pipeListeners.set(dest, {
439
498
  data: dataListener,
440
499
  end: endListener,
441
- error: errorListener
500
+ error: errorListener,
501
+ eventTarget
442
502
  });
443
503
  this.on("data", dataListener);
444
504
  this.once("end", endListener);
@@ -461,6 +521,14 @@ export class Readable extends EventEmitter {
461
521
  this.off("data", listeners.data);
462
522
  this.off("end", listeners.end);
463
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
+ }
464
532
  this._pipeListeners.delete(destination);
465
533
  }
466
534
  }
@@ -472,6 +540,14 @@ export class Readable extends EventEmitter {
472
540
  this.off("data", listeners.data);
473
541
  this.off("end", listeners.end);
474
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
+ }
475
551
  this._pipeListeners.delete(target);
476
552
  }
477
553
  }
@@ -490,12 +566,26 @@ export class Readable extends EventEmitter {
490
566
  }
491
567
  this._destroyed = true;
492
568
  this._ended = true;
569
+ // Ensure we detach from destinations to avoid leaking listeners.
570
+ this.unpipe();
493
571
  if (error) {
494
572
  this._errored = error;
495
573
  this.emit("error", error);
496
574
  }
497
575
  if (this._reader) {
498
- 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
+ });
499
589
  }
500
590
  this._closed = true;
501
591
  this.emit("close");
@@ -574,18 +664,38 @@ export class Readable extends EventEmitter {
574
664
  const { done, value } = await this._reader.read();
575
665
  // Check _pushMode again after async read - if push() was called, stop reading
576
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
+ }
577
677
  break;
578
678
  }
579
679
  if (done) {
580
680
  this._ended = true;
581
- 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
+ }
582
692
  break;
583
693
  }
584
694
  if (value !== undefined) {
585
695
  // In flowing mode, emit data directly without buffering
586
696
  // Only buffer if not flowing (paused mode)
587
697
  if (this._flowing) {
588
- this.emit("data", value);
698
+ this.emit("data", this._applyEncoding(value));
589
699
  }
590
700
  else {
591
701
  this._buffer.push(value);
@@ -598,6 +708,16 @@ export class Readable extends EventEmitter {
598
708
  }
599
709
  catch (err) {
600
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
+ }
601
721
  }
602
722
  finally {
603
723
  this._reading = false;
@@ -605,7 +725,8 @@ export class Readable extends EventEmitter {
605
725
  }
606
726
  /**
607
727
  * Async iterator support
608
- * 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).
609
730
  */
610
731
  async *[Symbol.asyncIterator]() {
611
732
  // First yield any buffered data
@@ -614,117 +735,124 @@ export class Readable extends EventEmitter {
614
735
  if (!this.objectMode) {
615
736
  this._bufferSize -= this._getChunkSize(chunk);
616
737
  }
617
- yield chunk;
738
+ yield this._applyEncoding(chunk);
618
739
  }
619
- // If already ended, we're done
620
740
  if (this._ended) {
621
741
  return;
622
742
  }
623
- // For controllable streams (not created from external Web Stream),
624
- // use event-based iteration since data comes from push() calls
625
- if (!this._webStreamMode) {
626
- // Create a promise-based queue for incoming data
627
- const dataQueue = [];
628
- let resolveNext = null;
629
- let rejectNext = null;
630
- let done = false;
631
- let streamError = null;
632
- let dataQueueIndex = 0;
633
- const dataHandler = (chunk) => {
634
- if (resolveNext) {
635
- resolveNext(chunk);
636
- resolveNext = null;
637
- rejectNext = null;
638
- }
639
- else {
640
- dataQueue.push(chunk);
641
- }
642
- };
643
- const endHandler = () => {
644
- done = true;
645
- if (resolveNext) {
646
- resolveNext(null);
647
- resolveNext = null;
648
- rejectNext = null;
649
- }
650
- };
651
- const errorHandler = (err) => {
652
- done = true;
653
- streamError = err;
654
- if (rejectNext) {
655
- rejectNext(err);
656
- resolveNext = null;
657
- rejectNext = null;
658
- }
659
- };
660
- const closeHandler = () => {
661
- // If stream closed without end event (e.g., after destroy()),
662
- // treat it as done
663
- done = true;
664
- if (resolveNext) {
665
- resolveNext(null);
666
- resolveNext = null;
667
- rejectNext = null;
668
- }
669
- };
670
- this.on("data", dataHandler);
671
- this.on("end", endHandler);
672
- this.on("error", errorHandler);
673
- this.on("close", closeHandler);
674
- try {
675
- // Enter flowing mode
676
- this.resume();
677
- while (!done || dataQueueIndex < dataQueue.length) {
678
- // Check for error before processing
679
- if (streamError) {
680
- throw streamError;
681
- }
682
- if (dataQueueIndex < dataQueue.length) {
683
- const chunk = dataQueue[dataQueueIndex++];
684
- if (dataQueueIndex >= 1024 && dataQueueIndex * 2 >= dataQueue.length) {
685
- dataQueue.splice(0, dataQueueIndex);
686
- dataQueueIndex = 0;
687
- }
688
- yield chunk;
689
- }
690
- else if (!done) {
691
- const chunk = await new Promise((resolve, reject) => {
692
- resolveNext = resolve;
693
- rejectNext = reject;
694
- });
695
- if (chunk !== null) {
696
- yield chunk;
697
- }
698
- }
699
- }
700
- // Check for error after loop
701
- if (streamError) {
702
- throw streamError;
703
- }
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;
704
748
  }
705
- finally {
706
- this.off("data", dataHandler);
707
- this.off("end", endHandler);
708
- this.off("error", errorHandler);
709
- this.off("close", closeHandler);
749
+ if (chunk instanceof Uint8Array) {
750
+ return chunk.byteLength;
710
751
  }
711
- return;
712
- }
713
- // For Web Stream mode, use the underlying reader
714
- if (!this._reader) {
715
- this._reader = this._stream.getReader();
716
- }
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);
717
810
  try {
811
+ // Iterator consumption should drive the stream.
812
+ this.resume();
718
813
  while (true) {
719
- 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
+ }
720
831
  if (done) {
721
832
  break;
722
833
  }
723
- 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;
724
849
  }
725
850
  }
726
851
  finally {
727
- this._reader.releaseLock();
852
+ this.off("data", dataHandler);
853
+ this.off("end", endHandler);
854
+ this.off("error", errorHandler);
855
+ this.off("close", closeHandler);
728
856
  }
729
857
  }
730
858
  /**
@@ -943,6 +1071,32 @@ export class Readable extends EventEmitter {
943
1071
  return stream;
944
1072
  }
945
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
+ }
946
1100
  // =============================================================================
947
1101
  // Writable Stream Wrapper
948
1102
  // =============================================================================
@@ -960,10 +1114,12 @@ export class Writable extends EventEmitter {
960
1114
  this._closed = false;
961
1115
  this._pendingWrites = 0;
962
1116
  this._writableLength = 0;
1117
+ this._needDrain = false;
963
1118
  this._corked = 0;
964
1119
  this._corkedChunks = [];
965
1120
  this._defaultEncoding = "utf8";
966
1121
  this._aborted = false;
1122
+ this._ownsStream = false;
967
1123
  this.objectMode = options?.objectMode ?? false;
968
1124
  this.writableHighWaterMark = options?.highWaterMark ?? 16384;
969
1125
  this.autoDestroy = options?.autoDestroy ?? true;
@@ -979,8 +1135,10 @@ export class Writable extends EventEmitter {
979
1135
  }
980
1136
  if (options?.stream) {
981
1137
  this._stream = options.stream;
1138
+ this._ownsStream = false;
982
1139
  }
983
1140
  else {
1141
+ this._ownsStream = true;
984
1142
  // Create bound references to instance properties/methods for use in WritableStream callbacks
985
1143
  const getWriteFunc = () => this._writeFunc;
986
1144
  const getFinalFunc = () => this._finalFunc;
@@ -1087,25 +1245,37 @@ export class Writable extends EventEmitter {
1087
1245
  this._writableLength += chunkSize;
1088
1246
  return this._writableLength < this.writableHighWaterMark;
1089
1247
  }
1090
- return this._doWrite(chunk, cb);
1248
+ const ok = this._doWrite(chunk, cb);
1249
+ if (!ok) {
1250
+ this._needDrain = true;
1251
+ }
1252
+ return ok;
1091
1253
  }
1092
1254
  _doWrite(chunk, callback) {
1093
1255
  // Track pending writes for writableLength
1094
1256
  const chunkSize = this._getChunkSize(chunk);
1095
1257
  this._pendingWrites++;
1096
1258
  this._writableLength += chunkSize;
1097
- this._getWriter()
1259
+ const writer = this._getWriter();
1260
+ writer
1098
1261
  .write(chunk)
1099
1262
  .then(() => {
1100
1263
  this._pendingWrites--;
1101
1264
  this._writableLength -= chunkSize;
1102
- this.emit("drain");
1265
+ if (this._needDrain && this._writableLength < this.writableHighWaterMark) {
1266
+ this._needDrain = false;
1267
+ this.emit("drain");
1268
+ }
1103
1269
  callback?.(null);
1104
1270
  })
1105
1271
  .catch(err => {
1106
1272
  this._pendingWrites--;
1107
1273
  this._writableLength -= chunkSize;
1108
- 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
+ }
1109
1279
  callback?.(err);
1110
1280
  });
1111
1281
  // Return false if we've exceeded high water mark (for backpressure)
@@ -1136,12 +1306,29 @@ export class Writable extends EventEmitter {
1136
1306
  : callback;
1137
1307
  const finish = async () => {
1138
1308
  try {
1309
+ const writer = this._getWriter();
1139
1310
  if (chunk !== undefined) {
1140
- 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
+ }
1141
1331
  }
1142
- await this._getWriter().close();
1143
- this._finished = true;
1144
- this.emit("finish");
1145
1332
  if (cb) {
1146
1333
  cb();
1147
1334
  }
@@ -1162,12 +1349,24 @@ export class Writable extends EventEmitter {
1162
1349
  }
1163
1350
  this._destroyed = true;
1164
1351
  this._ended = true;
1165
- if (error) {
1352
+ if (error && !this._errored) {
1166
1353
  this._errored = error;
1167
1354
  this.emit("error", error);
1168
1355
  }
1169
1356
  if (this._writer) {
1170
- 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
+ });
1171
1370
  }
1172
1371
  this._closed = true;
1173
1372
  this.emit("close");
@@ -1318,342 +1517,259 @@ export function normalizeWritable(stream) {
1318
1517
  */
1319
1518
  export class Transform extends EventEmitter {
1320
1519
  /**
1321
- * Push data to the readable side (Node.js compatibility)
1322
- * 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.
1323
1522
  */
1324
1523
  push(chunk) {
1325
- if (chunk === null) {
1326
- return false;
1327
- }
1328
- if (this._transformController) {
1329
- // If we're in a transform callback, enqueue directly
1330
- this._transformController.enqueue(chunk);
1331
- }
1332
- else {
1333
- // Otherwise buffer for later
1334
- this._pushBuffer.push(chunk);
1335
- }
1336
- return true;
1524
+ return this._readable.push(chunk);
1337
1525
  }
1338
1526
  constructor(options) {
1339
1527
  super();
1340
- this._ended = false;
1341
1528
  this._destroyed = false;
1529
+ this._ended = false;
1342
1530
  this._errored = false;
1343
- // Buffer for Node.js style push() calls during transform
1344
- this._pushBuffer = [];
1345
- // Controller for enqueueing pushed data (set during transform execution)
1346
- this._transformController = null;
1347
- // Buffer for writes that occur after end() but before writable is closed
1348
- this._pendingEndWrites = [];
1349
- // Whether end() has been called but writable not yet closed
1350
- this._endPending = false;
1351
- // Track if we've already set up data forwarding
1352
1531
  this._dataForwardingSetup = false;
1353
- /** @internal - whether we have a data event consumer */
1354
- this._hasDataConsumer = false;
1355
- /** @internal - whether we're auto-consuming the readable */
1356
- this._readableConsuming = false;
1357
- /** @internal - buffer for auto-consumed data */
1358
- this._autoConsumedBuffer = [];
1359
- this._autoConsumedBufferIndex = 0;
1360
- /** @internal - whether auto-consume has ended */
1361
- this._autoConsumeEnded = false;
1362
- /** @internal - promise that resolves when auto-consume finishes */
1363
- this._autoConsumePromise = null;
1364
- /** @internal - list of piped destinations for forwarding auto-consumed data */
1365
- this._pipeDestinations = [];
1532
+ this._endTimer = null;
1533
+ this._webStream = null;
1534
+ this._sideForwardingCleanup = null;
1366
1535
  this.objectMode = options?.objectMode ?? false;
1367
- const userTransform = options?.transform;
1368
- const userFlush = options?.flush;
1369
- // Create bound references for use in TransformStream callbacks
1370
- const setController = (ctrl) => {
1371
- this._transformController = ctrl;
1372
- };
1373
- const emitEvent = (event, ...args) => {
1374
- if (event === "error") {
1375
- // Only emit error once to prevent duplicate events
1376
- if (this._errored) {
1377
- return false;
1378
- }
1379
- this._errored = true;
1380
- // Also destroy the writable to prevent further writes
1381
- 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));
1382
1555
  }
1383
- return this.emit(event, ...args);
1384
- };
1385
- const getInstance = () => this;
1386
- // Check if subclass overrides _transform (for Node.js compatibility)
1387
- // We need to check this at runtime since the subclass constructor runs after super()
1388
- const hasSubclassTransform = () => {
1389
- // If userTransform was provided in options, use that
1390
- if (userTransform) {
1391
- 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;
1392
1587
  }
1393
- // Check if _transform is overridden (not the base class no-op)
1394
- const proto = Object.getPrototypeOf(this);
1395
- return proto._transform !== Transform.prototype._transform;
1396
- };
1397
- const hasSubclassFlush = () => {
1398
- if (userFlush) {
1399
- 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;
1400
1638
  }
1401
- const proto = Object.getPrototypeOf(this);
1402
- return proto._flush !== Transform.prototype._flush;
1403
- };
1404
- this._stream = new TransformStream({
1405
- transform: async (chunk, controller) => {
1406
- // Skip processing if already errored
1407
- if (this._errored) {
1408
- return;
1409
- }
1410
- try {
1411
- // Set controller for push() to use
1412
- setController(controller);
1413
- // Check for subclass _transform override first
1414
- if (hasSubclassTransform()) {
1415
- // Call subclass _transform method (Node.js style)
1416
- // _transform signature is (chunk, encoding, callback)
1417
- await new Promise((resolve, reject) => {
1418
- this._transform(chunk, "utf8", (err, data) => {
1419
- if (err) {
1420
- reject(err);
1421
- }
1422
- else {
1423
- if (data !== undefined) {
1424
- controller.enqueue(data);
1425
- }
1426
- resolve();
1427
- }
1428
- });
1429
- });
1430
- }
1431
- else if (userTransform) {
1432
- const transformParamCount = userTransform.length;
1433
- if (transformParamCount >= 3) {
1434
- // Node.js style: transform(chunk, encoding, callback)
1435
- await new Promise((resolve, reject) => {
1436
- userTransform.call(getInstance(), chunk, "utf8", (err, data) => {
1437
- if (err) {
1438
- reject(err);
1439
- }
1440
- else {
1441
- if (data !== undefined) {
1442
- controller.enqueue(data);
1443
- }
1444
- resolve();
1445
- }
1446
- });
1447
- });
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;
1448
1651
  }
1449
- else if (transformParamCount === 2) {
1450
- await new Promise((resolve, reject) => {
1451
- userTransform.call(getInstance(), chunk, (err, data) => {
1452
- if (err) {
1453
- reject(err);
1454
- }
1455
- else {
1456
- if (data !== undefined) {
1457
- controller.enqueue(data);
1458
- }
1459
- resolve();
1460
- }
1461
- });
1462
- });
1652
+ if (data !== undefined) {
1653
+ this.push(data);
1463
1654
  }
1464
- else {
1465
- // Simple style: transform(chunk) => result
1466
- const result = userTransform.call(getInstance(), chunk);
1467
- if (result && typeof result.then === "function") {
1468
- const awaitedResult = await result;
1469
- if (awaitedResult !== undefined) {
1470
- controller.enqueue(awaitedResult);
1471
- }
1472
- }
1473
- else {
1474
- if (result !== undefined) {
1475
- controller.enqueue(result);
1476
- }
1477
- }
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;
1478
1666
  }
1479
- }
1480
- else {
1481
- // Default: pass through
1482
- controller.enqueue(chunk);
1483
- }
1484
- }
1485
- catch (err) {
1486
- controller.error(err);
1487
- emitEvent("error", err);
1488
- }
1489
- finally {
1490
- 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);
1491
1680
  }
1492
- },
1493
- flush: async (controller) => {
1494
- try {
1495
- setController(controller);
1496
- // Check for subclass _flush override first
1497
- if (hasSubclassFlush()) {
1498
- await new Promise((resolve, reject) => {
1499
- this._flush((err, data) => {
1500
- if (err) {
1501
- reject(err);
1502
- }
1503
- else {
1504
- if (data !== undefined) {
1505
- controller.enqueue(data);
1506
- }
1507
- resolve();
1508
- }
1509
- });
1510
- });
1511
- }
1512
- else if (userFlush) {
1513
- const flushParamCount = userFlush.length;
1514
- if (flushParamCount >= 1) {
1515
- // Node.js style: flush(callback)
1516
- await new Promise((resolve, reject) => {
1517
- userFlush.call(getInstance(), (err, data) => {
1518
- if (err) {
1519
- reject(err);
1520
- }
1521
- else {
1522
- if (data !== undefined) {
1523
- controller.enqueue(data);
1524
- }
1525
- resolve();
1526
- }
1527
- });
1528
- });
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;
1529
1703
  }
1530
- else {
1531
- // Simple style: flush() => result
1532
- const result = userFlush.call(getInstance());
1533
- if (result && typeof result.then === "function") {
1534
- const awaitedResult = await result;
1535
- if (awaitedResult !== undefined && awaitedResult !== null) {
1536
- controller.enqueue(awaitedResult);
1537
- }
1538
- }
1539
- else {
1540
- if (result !== undefined && result !== null) {
1541
- controller.enqueue(result);
1542
- }
1543
- }
1704
+ if (data !== undefined) {
1705
+ this.push(data);
1544
1706
  }
1545
- }
1546
- // No flush defined - nothing to do
1547
- }
1548
- catch (err) {
1549
- controller.error(err);
1550
- emitEvent("error", err);
1551
- }
1552
- finally {
1553
- setController(null);
1554
- }
1707
+ resolve();
1708
+ });
1709
+ });
1710
+ return;
1555
1711
  }
1556
- });
1557
- this._readable = new Readable({
1558
- stream: this._stream.readable,
1559
- objectMode: this.objectMode
1560
- });
1561
- this._writable = new Writable({
1562
- stream: this._stream.writable,
1563
- objectMode: this.objectMode
1564
- });
1565
- // Forward non-data events (data forwarding is lazy to avoid premature flowing)
1566
- this._readable.on("end", () => this.emit("end"));
1567
- // Only forward errors if not already errored (to prevent duplicate events)
1568
- this._readable.on("error", err => {
1569
- if (!this._errored) {
1570
- this._errored = true;
1571
- this.emit("error", err);
1712
+ const userFlush = this._flushImpl;
1713
+ if (!userFlush) {
1714
+ return;
1572
1715
  }
1573
- });
1574
- this._writable.on("finish", () => this.emit("finish"));
1575
- this._writable.on("drain", () => this.emit("drain"));
1576
- // Only forward errors if not already errored (to prevent duplicate events)
1577
- this._writable.on("error", err => {
1578
- if (!this._errored) {
1579
- this._errored = true;
1580
- 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;
1581
1731
  }
1582
- });
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
+ }
1583
1748
  }
1584
1749
  /**
1585
- * 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.
1586
1752
  */
1587
1753
  on(event, listener) {
1588
- // Set up data forwarding when first external data listener is added
1589
1754
  if (event === "data" && !this._dataForwardingSetup) {
1590
1755
  this._dataForwardingSetup = true;
1591
- this._readable.on("data", data => this.emit("data", data));
1592
- }
1593
- super.on(event, listener);
1594
- // When data listener is added, mark as having consumer
1595
- // and start the readable in flowing mode
1596
- if (event === "data") {
1597
- this._hasDataConsumer = true;
1598
- this._readable.resume();
1756
+ this._readable.on("data", chunk => this.emit("data", chunk));
1599
1757
  }
1600
- return this;
1758
+ return super.on(event, listener);
1601
1759
  }
1602
1760
  write(chunk, encodingOrCallback, callback) {
1603
1761
  const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
1604
- if (this._destroyed || this._errored) {
1605
- const err = new Error(this._errored ? "Cannot write after stream errored" : "Cannot write after stream destroyed");
1606
- queueMicrotask(() => this.emit("error", err));
1607
- cb?.(err);
1608
- return false;
1609
- }
1610
- // Ensure readable is being consumed to allow transform to execute
1611
- // This matches Node.js behavior where transform executes immediately on write
1612
- // Only auto-consume if no explicit consumer (data listener or pipe)
1613
- if (!this._readableConsuming && !this._hasDataConsumer) {
1614
- this._readableConsuming = true;
1615
- this._startAutoConsume();
1616
- }
1617
- // If end() was called but writable not yet closed, buffer the write
1618
- // This allows writes during data event handlers to be processed
1619
- if (this._endPending) {
1620
- this._pendingEndWrites.push({ chunk, callback: cb });
1621
- 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();
1622
1765
  }
1623
1766
  return this._writable.write(chunk, cb);
1624
1767
  }
1625
- /** @internal - auto-consume readable to allow transform to execute */
1626
- _startAutoConsume() {
1627
- this._autoConsumePromise = (async () => {
1628
- try {
1629
- for await (const chunk of this._readable) {
1630
- // Buffer the data for later retrieval
1631
- this._autoConsumedBuffer.push(chunk);
1632
- // Forward to any piped destinations
1633
- for (const dest of this._pipeDestinations) {
1634
- dest.write(chunk);
1635
- }
1636
- // Also emit data event for listeners
1637
- this.emit("data", chunk);
1638
- }
1639
- this._autoConsumeEnded = true;
1640
- // End all piped destinations
1641
- for (const dest of this._pipeDestinations) {
1642
- dest.end();
1643
- }
1644
- this.emit("end");
1645
- }
1646
- catch (err) {
1647
- this.emit("error", err);
1648
- }
1649
- })();
1650
- }
1651
1768
  end(chunkOrCallback, encodingOrCallback, callback) {
1652
1769
  if (this._ended) {
1653
1770
  return this;
1654
1771
  }
1655
1772
  this._ended = true;
1656
- this._endPending = true;
1657
1773
  const chunk = typeof chunkOrCallback === "function" ? undefined : chunkOrCallback;
1658
1774
  const cb = typeof chunkOrCallback === "function"
1659
1775
  ? chunkOrCallback
@@ -1666,18 +1782,7 @@ export class Transform extends EventEmitter {
1666
1782
  if (chunk !== undefined) {
1667
1783
  this._writable.write(chunk);
1668
1784
  }
1669
- // Use setTimeout(0) instead of queueMicrotask to ensure all transform
1670
- // processing and data events complete before we close the writable.
1671
- // Microtasks run before the TransformStream processes data.
1672
- setTimeout(() => {
1673
- // Process any writes that occurred during data events
1674
- for (const { chunk: pendingChunk, callback } of this._pendingEndWrites) {
1675
- this._writable.write(pendingChunk, callback);
1676
- }
1677
- this._pendingEndWrites = [];
1678
- this._endPending = false;
1679
- this._writable.end();
1680
- }, 0);
1785
+ this._scheduleEnd();
1681
1786
  return this;
1682
1787
  }
1683
1788
  /**
@@ -1687,31 +1792,9 @@ export class Transform extends EventEmitter {
1687
1792
  return this._readable.read(size);
1688
1793
  }
1689
1794
  /**
1690
- * Pipe to another stream (writable, transform, or duplex)
1795
+ * Pipe readable side to destination
1691
1796
  */
1692
1797
  pipe(destination) {
1693
- // Mark as having consumer to prevent new auto-consume from starting
1694
- this._hasDataConsumer = true;
1695
- // Get the writable target - handle both Transform (with internal _writable) and plain Writable
1696
- const dest = destination;
1697
- const target = dest?._writable ?? dest;
1698
- // Register destination for forwarding
1699
- this._pipeDestinations.push(target);
1700
- // If auto-consume is running or has run, we need to handle buffered data ourselves
1701
- if (this._readableConsuming) {
1702
- // Forward any buffered data from auto-consume to the destination
1703
- for (let i = 0; i < this._autoConsumedBuffer.length; i++) {
1704
- target.write(this._autoConsumedBuffer[i]);
1705
- }
1706
- // If auto-consume has ended, end the destination too
1707
- if (this._autoConsumeEnded) {
1708
- target.end();
1709
- }
1710
- // Don't call _readable.pipe() - auto-consume already consumed _readable
1711
- // Future data will be forwarded via the 'data' event listener below
1712
- return destination;
1713
- }
1714
- // No auto-consume running - use normal pipe through _readable
1715
1798
  return this._readable.pipe(destination);
1716
1799
  }
1717
1800
  /**
@@ -1749,6 +1832,10 @@ export class Transform extends EventEmitter {
1749
1832
  return;
1750
1833
  }
1751
1834
  this._destroyed = true;
1835
+ if (this._sideForwardingCleanup) {
1836
+ this._sideForwardingCleanup();
1837
+ this._sideForwardingCleanup = null;
1838
+ }
1752
1839
  this._readable.destroy(error);
1753
1840
  this._writable.destroy(error);
1754
1841
  queueMicrotask(() => this.emit("close"));
@@ -1757,7 +1844,44 @@ export class Transform extends EventEmitter {
1757
1844
  * Get the underlying Web TransformStream
1758
1845
  */
1759
1846
  get webStream() {
1760
- 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;
1761
1885
  }
1762
1886
  get readable() {
1763
1887
  return this._readable.readable;
@@ -1799,19 +1923,6 @@ export class Transform extends EventEmitter {
1799
1923
  * Async iterator support
1800
1924
  */
1801
1925
  async *[Symbol.asyncIterator]() {
1802
- // If auto-consume is running, wait for it to finish and use its buffer
1803
- if (this._autoConsumePromise) {
1804
- await this._autoConsumePromise;
1805
- // Yield all buffered data
1806
- while (this._autoConsumedBufferIndex < this._autoConsumedBuffer.length) {
1807
- yield this._autoConsumedBuffer[this._autoConsumedBufferIndex++];
1808
- }
1809
- // Reset when drained to avoid prefix growth
1810
- this._autoConsumedBuffer.length = 0;
1811
- this._autoConsumedBufferIndex = 0;
1812
- return;
1813
- }
1814
- // Otherwise delegate to readable's iterator
1815
1926
  yield* this._readable[Symbol.asyncIterator]();
1816
1927
  }
1817
1928
  // =========================================================================
@@ -1822,23 +1933,18 @@ export class Transform extends EventEmitter {
1822
1933
  */
1823
1934
  static fromWeb(webStream, options) {
1824
1935
  const transform = new Transform(options);
1825
- // Connect the web stream - set the internal _stream property
1826
- transform._stream = webStream;
1936
+ transform._webStream = webStream;
1827
1937
  // Replace internal streams with the ones from the web stream
1828
1938
  const newReadable = Readable.fromWeb(webStream.readable, { objectMode: options?.objectMode });
1829
1939
  const newWritable = Writable.fromWeb(webStream.writable, { objectMode: options?.objectMode });
1830
- // Remove old event listeners before replacing
1831
- transform._readable.removeAllListeners();
1832
- transform._writable.removeAllListeners();
1940
+ if (transform._sideForwardingCleanup) {
1941
+ transform._sideForwardingCleanup();
1942
+ transform._sideForwardingCleanup = null;
1943
+ }
1833
1944
  transform._readable = newReadable;
1834
1945
  transform._writable = newWritable;
1835
- // Re-connect event forwarding
1836
- newReadable.on("data", (data) => transform.emit("data", data));
1837
- newReadable.on("end", () => transform.emit("end"));
1838
- newReadable.on("error", (err) => transform.emit("error", err));
1839
- newWritable.on("finish", () => transform.emit("finish"));
1840
- newWritable.on("drain", () => transform.emit("drain"));
1841
- newWritable.on("error", (err) => transform.emit("error", err));
1946
+ // Re-connect event forwarding (data forwarding remains lazy via Transform.on)
1947
+ transform._setupSideForwarding();
1842
1948
  return transform;
1843
1949
  }
1844
1950
  /**
@@ -1894,7 +2000,13 @@ export class Duplex extends EventEmitter {
1894
2000
  callback();
1895
2001
  }
1896
2002
  });
1897
- 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 });
1898
2010
  readable.pipe(sink);
1899
2011
  };
1900
2012
  // If it has readable and/or writable properties
@@ -1902,21 +2014,25 @@ export class Duplex extends EventEmitter {
1902
2014
  source !== null &&
1903
2015
  "readable" in source &&
1904
2016
  "writable" in source) {
1905
- const duplex = new Duplex();
1906
2017
  const pair = source;
1907
- if (pair.readable) {
1908
- forwardReadableToDuplex(pair.readable, duplex);
1909
- }
1910
- if (pair.writable) {
1911
- return new Duplex({
1912
- objectMode: duplex.writableObjectMode,
1913
- 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) => {
1914
2025
  pair.writable.write(chunk, encoding, callback);
1915
- },
1916
- final(callback) {
2026
+ }
2027
+ : undefined,
2028
+ final: pair.writable
2029
+ ? callback => {
1917
2030
  pair.writable.end(callback);
1918
2031
  }
1919
- });
2032
+ : undefined
2033
+ });
2034
+ if (pair.readable) {
2035
+ forwardReadableToDuplex(pair.readable, duplex);
1920
2036
  }
1921
2037
  return duplex;
1922
2038
  }
@@ -1954,9 +2070,22 @@ export class Duplex extends EventEmitter {
1954
2070
  */
1955
2071
  static fromWeb(pair, options) {
1956
2072
  const duplex = new Duplex(options);
1957
- // Replace internal streams
1958
- duplex._readable = new Readable({ stream: pair.readable });
1959
- 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();
1960
2089
  return duplex;
1961
2090
  }
1962
2091
  /**
@@ -1972,6 +2101,7 @@ export class Duplex extends EventEmitter {
1972
2101
  super();
1973
2102
  // Track if we've already set up data forwarding
1974
2103
  this._dataForwardingSetup = false;
2104
+ this._sideForwardingCleanup = null;
1975
2105
  this.allowHalfOpen = options?.allowHalfOpen ?? true;
1976
2106
  // Support shorthand objectMode option
1977
2107
  const objectMode = options?.objectMode ?? false;
@@ -1988,23 +2118,31 @@ export class Duplex extends EventEmitter {
1988
2118
  write: options?.write?.bind(this),
1989
2119
  final: options?.final?.bind(this)
1990
2120
  });
2121
+ this._setupSideForwarding();
2122
+ }
2123
+ _setupSideForwarding() {
2124
+ if (this._sideForwardingCleanup) {
2125
+ this._sideForwardingCleanup();
2126
+ this._sideForwardingCleanup = null;
2127
+ }
2128
+ const registry = createListenerRegistry();
1991
2129
  // Forward non-data events (data forwarding is lazy to avoid premature flowing)
1992
- this._readable.on("end", () => {
2130
+ registry.once(this._readable, "end", () => {
1993
2131
  this.emit("end");
1994
- // If not allowHalfOpen, end the writable side too
1995
2132
  if (!this.allowHalfOpen) {
1996
2133
  this._writable.end();
1997
2134
  }
1998
2135
  });
1999
- this._readable.on("error", err => this.emit("error", err));
2000
- this._writable.on("finish", () => this.emit("finish"));
2001
- this._writable.on("drain", () => this.emit("drain"));
2002
- this._writable.on("close", () => {
2003
- // 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", () => {
2004
2141
  if (!this.allowHalfOpen && !this._readable.destroyed) {
2005
2142
  this._readable.destroy();
2006
2143
  }
2007
2144
  });
2145
+ this._sideForwardingCleanup = () => registry.cleanup();
2008
2146
  }
2009
2147
  /**
2010
2148
  * Override on() to set up data forwarding lazily
@@ -2123,6 +2261,10 @@ export class Duplex extends EventEmitter {
2123
2261
  * Destroy both sides
2124
2262
  */
2125
2263
  destroy(error) {
2264
+ if (this._sideForwardingCleanup) {
2265
+ this._sideForwardingCleanup();
2266
+ this._sideForwardingCleanup = null;
2267
+ }
2126
2268
  this._readable.destroy(error);
2127
2269
  this._writable.destroy(error);
2128
2270
  return this;
@@ -2285,36 +2427,16 @@ export class BufferedStream extends StandaloneBufferedStream {
2285
2427
  * Create a readable stream with custom read implementation
2286
2428
  */
2287
2429
  export function createReadable(options) {
2288
- const readable = new Readable(options);
2289
- // Override read behavior if provided
2290
- if (options?.read) {
2291
- const originalRead = readable.read.bind(readable);
2292
- readable.read = function (size) {
2293
- options.read(size ?? 16384);
2294
- return originalRead(size);
2295
- };
2296
- }
2297
- 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);
2298
2433
  }
2299
2434
  /**
2300
2435
  * Create a readable stream from an async iterable
2301
2436
  */
2302
2437
  export function createReadableFromAsyncIterable(iterable, options) {
2303
2438
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
2304
- (async () => {
2305
- try {
2306
- for await (const chunk of iterable) {
2307
- if (!readable.push(chunk)) {
2308
- // Backpressure - wait a bit
2309
- await new Promise(resolve => setTimeout(resolve, 0));
2310
- }
2311
- }
2312
- readable.push(null);
2313
- }
2314
- catch (err) {
2315
- readable.destroy(err);
2316
- }
2317
- })();
2439
+ pumpAsyncIterableToReadable(readable, iterable);
2318
2440
  return readable;
2319
2441
  }
2320
2442
  /**
@@ -2343,38 +2465,8 @@ export function createReadableFromArray(data, options) {
2343
2465
  * Create a writable stream with custom write implementation
2344
2466
  */
2345
2467
  export function createWritable(options) {
2346
- // Create a custom WritableStream with user's handlers
2347
- const stream = new WritableStream({
2348
- write: async (chunk) => {
2349
- if (options?.write) {
2350
- return new Promise((resolve, reject) => {
2351
- options.write(chunk, "utf8", err => {
2352
- if (err) {
2353
- reject(err);
2354
- }
2355
- else {
2356
- resolve();
2357
- }
2358
- });
2359
- });
2360
- }
2361
- },
2362
- close: async () => {
2363
- if (options?.final) {
2364
- return new Promise((resolve, reject) => {
2365
- options.final(err => {
2366
- if (err) {
2367
- reject(err);
2368
- }
2369
- else {
2370
- resolve();
2371
- }
2372
- });
2373
- });
2374
- }
2375
- }
2376
- });
2377
- return new Writable({ ...options, stream });
2468
+ // Writable already supports Node-style `write()` / `final()` via the constructor.
2469
+ return new Writable(options);
2378
2470
  }
2379
2471
  /**
2380
2472
  * Create a transform stream from a transform function
@@ -2509,11 +2601,17 @@ export function pipeline(...args) {
2509
2601
  const transforms = normalized.slice(1, -1);
2510
2602
  let completed = false;
2511
2603
  const allStreams = [source, ...transforms, destination];
2512
- const cleanup = (error) => {
2604
+ const registry = createListenerRegistry();
2605
+ let onAbort;
2606
+ const cleanupWithSignal = (error) => {
2513
2607
  if (completed) {
2514
2608
  return;
2515
2609
  }
2516
2610
  completed = true;
2611
+ registry.cleanup();
2612
+ if (onAbort && options.signal) {
2613
+ options.signal.removeEventListener("abort", onAbort);
2614
+ }
2517
2615
  // Destroy all streams on error
2518
2616
  if (error) {
2519
2617
  for (const stream of allStreams) {
@@ -2530,12 +2628,11 @@ export function pipeline(...args) {
2530
2628
  // Handle abort signal
2531
2629
  if (options.signal) {
2532
2630
  if (options.signal.aborted) {
2533
- cleanup(new Error("Pipeline aborted"));
2631
+ cleanupWithSignal(new Error("Pipeline aborted"));
2534
2632
  return;
2535
2633
  }
2536
- options.signal.addEventListener("abort", () => {
2537
- cleanup(new Error("Pipeline aborted"));
2538
- });
2634
+ onAbort = () => cleanupWithSignal(new Error("Pipeline aborted"));
2635
+ options.signal.addEventListener("abort", onAbort);
2539
2636
  }
2540
2637
  // Chain the streams
2541
2638
  let current = source;
@@ -2549,13 +2646,35 @@ export function pipeline(...args) {
2549
2646
  }
2550
2647
  else {
2551
2648
  // Don't end destination
2552
- 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);
2553
2672
  }
2554
2673
  // Handle completion
2555
- destination.on("finish", () => cleanup());
2674
+ registry.once(destination, "finish", () => cleanupWithSignal());
2556
2675
  // Handle errors on all streams
2557
2676
  for (const stream of allStreams) {
2558
- stream.on("error", (err) => cleanup(err));
2677
+ registry.once(stream, "error", (err) => cleanupWithSignal(err));
2559
2678
  }
2560
2679
  });
2561
2680
  // If callback provided, use it
@@ -2595,11 +2714,20 @@ export function finished(stream, optionsOrCallback, callback) {
2595
2714
  const promise = new Promise((resolve, reject) => {
2596
2715
  const normalizedStream = toBrowserPipelineStream(stream);
2597
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
+ };
2598
2725
  const done = (err) => {
2599
2726
  if (resolved) {
2600
2727
  return;
2601
2728
  }
2602
2729
  resolved = true;
2730
+ cleanup();
2603
2731
  if (err && !options.error) {
2604
2732
  reject(err);
2605
2733
  }
@@ -2613,9 +2741,8 @@ export function finished(stream, optionsOrCallback, callback) {
2613
2741
  done(new Error("Aborted"));
2614
2742
  return;
2615
2743
  }
2616
- options.signal.addEventListener("abort", () => {
2617
- done(new Error("Aborted"));
2618
- });
2744
+ onAbort = () => done(new Error("Aborted"));
2745
+ options.signal.addEventListener("abort", onAbort);
2619
2746
  }
2620
2747
  const checkReadable = options.readable !== false;
2621
2748
  const checkWritable = options.writable !== false;
@@ -2630,13 +2757,13 @@ export function finished(stream, optionsOrCallback, callback) {
2630
2757
  }
2631
2758
  // Listen for events
2632
2759
  if (checkWritable) {
2633
- normalizedStream.on("finish", () => done());
2760
+ registry.once(normalizedStream, "finish", () => done());
2634
2761
  }
2635
2762
  if (checkReadable) {
2636
- normalizedStream.on("end", () => done());
2763
+ registry.once(normalizedStream, "end", () => done());
2637
2764
  }
2638
- normalizedStream.on("error", (err) => done(err));
2639
- normalizedStream.on("close", () => done());
2765
+ registry.once(normalizedStream, "error", (err) => done(err));
2766
+ registry.once(normalizedStream, "close", () => done());
2640
2767
  });
2641
2768
  // If callback provided, use it
2642
2769
  if (cb) {
@@ -2658,38 +2785,8 @@ export async function streamToPromise(stream) {
2658
2785
  * (Browser equivalent of Node.js streamToBuffer)
2659
2786
  */
2660
2787
  export async function streamToUint8Array(stream) {
2661
- let iterable;
2662
- if (isReadableStream(stream)) {
2663
- iterable = Readable.fromWeb(stream);
2664
- }
2665
- else if (isAsyncIterable(stream)) {
2666
- iterable = stream;
2667
- }
2668
- else {
2669
- throw new Error("streamToUint8Array: unsupported stream type");
2670
- }
2671
- const chunks = [];
2672
- let totalLength = 0;
2673
- for await (const chunk of iterable) {
2674
- chunks.push(chunk);
2675
- totalLength += chunk.length;
2676
- }
2677
- // Fast paths
2678
- const len = chunks.length;
2679
- if (len === 0) {
2680
- return new Uint8Array(0);
2681
- }
2682
- if (len === 1) {
2683
- return chunks[0];
2684
- }
2685
- // Use precalculated total length
2686
- const result = new Uint8Array(totalLength);
2687
- let offset = 0;
2688
- for (let i = 0; i < len; i++) {
2689
- result.set(chunks[i], offset);
2690
- offset += chunks[i].length;
2691
- }
2692
- return result;
2788
+ const { chunks, totalLength } = await collectStreamChunks(stream);
2789
+ return concatWithLength(chunks, totalLength);
2693
2790
  }
2694
2791
  /**
2695
2792
  * Alias for streamToUint8Array (Node.js compatibility)
@@ -2699,8 +2796,10 @@ export const streamToBuffer = streamToUint8Array;
2699
2796
  * Collect all data from a readable stream into a string
2700
2797
  */
2701
2798
  export async function streamToString(stream, encoding) {
2702
- const buffer = await streamToUint8Array(stream);
2703
- 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);
2704
2803
  }
2705
2804
  /**
2706
2805
  * Drain a stream (consume all data without processing)
@@ -2788,14 +2887,19 @@ export function addAbortSignal(signal, stream) {
2788
2887
  stream.destroy(new Error("Aborted"));
2789
2888
  return stream;
2790
2889
  }
2890
+ const cleanup = () => {
2891
+ signal.removeEventListener("abort", onAbort);
2892
+ removeEmitterListener(stream, "close", onClose);
2893
+ };
2791
2894
  const onAbort = () => {
2895
+ cleanup();
2792
2896
  stream.destroy(new Error("Aborted"));
2793
2897
  };
2794
- signal.addEventListener("abort", onAbort, { once: true });
2795
- // Clean up when stream is destroyed
2796
- stream.on("close", () => {
2797
- signal.removeEventListener("abort", onAbort);
2798
- });
2898
+ const onClose = () => {
2899
+ cleanup();
2900
+ };
2901
+ signal.addEventListener("abort", onAbort);
2902
+ addEmitterListener(stream, "close", onClose, { once: true });
2799
2903
  return stream;
2800
2904
  }
2801
2905
  /**
@@ -2804,60 +2908,68 @@ export function addAbortSignal(signal, stream) {
2804
2908
  export function createDuplex(options) {
2805
2909
  const readableObjectMode = options?.readableObjectMode ?? options?.objectMode;
2806
2910
  const writableObjectMode = options?.writableObjectMode ?? options?.objectMode;
2911
+ const underlyingWritable = options?.writable;
2807
2912
  const duplex = new Duplex({
2808
2913
  allowHalfOpen: options?.allowHalfOpen,
2809
2914
  readableHighWaterMark: options?.readableHighWaterMark,
2810
2915
  writableHighWaterMark: options?.writableHighWaterMark,
2811
2916
  readableObjectMode,
2812
- 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)
2813
2948
  });
2814
- // If custom readable/writable provided, pipe them
2949
+ // If an underlying readable is provided, forward it into the duplex readable side.
2815
2950
  if (options?.readable) {
2816
2951
  const readable = options.readable;
2817
- readable.on?.("data", (chunk) => duplex.push(chunk));
2818
- readable.on?.("end", () => duplex.push(null));
2819
- readable.on?.("error", (err) => duplex.destroy(err));
2820
- }
2821
- if (options?.writable) {
2822
- const writable = options.writable;
2823
- duplex.on("data", (chunk) => writable.write?.(chunk));
2824
- duplex.on("finish", () => writable.end?.());
2825
- }
2826
- // If custom read/write/final provided, override methods
2827
- if (options?.write) {
2828
- const _originalWrite = duplex.write.bind(duplex); // Keep bound reference for potential future use
2829
- duplex.write = function (chunk, encodingOrCallback, callback) {
2830
- const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : "utf8";
2831
- const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : (callback ?? (() => { }));
2832
- options.write.call(duplex, chunk, encoding, cb);
2833
- return true;
2834
- };
2835
- }
2836
- if (options?.final) {
2837
- const originalEnd = duplex.end.bind(duplex);
2838
- duplex.end = function (chunkOrCallback, encodingOrCallback, callback) {
2839
- const cb = typeof chunkOrCallback === "function"
2840
- ? chunkOrCallback
2841
- : typeof encodingOrCallback === "function"
2842
- ? encodingOrCallback
2843
- : (callback ?? (() => { }));
2844
- if (chunkOrCallback !== undefined && typeof chunkOrCallback !== "function") {
2845
- duplex.write(chunkOrCallback);
2846
- }
2847
- // Call custom final handler
2848
- options.final.call(duplex, (err) => {
2849
- if (err) {
2850
- duplex.emit("error", err);
2851
- }
2852
- else {
2853
- duplex.emit("finish");
2854
- }
2855
- // Call original end to properly close writable side
2856
- originalEnd();
2857
- cb();
2858
- });
2859
- return duplex;
2860
- };
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);
2861
2973
  }
2862
2974
  if (options?.destroy) {
2863
2975
  const originalDestroy = duplex.destroy.bind(duplex);
@@ -2881,20 +2993,7 @@ export function createDuplex(options) {
2881
2993
  */
2882
2994
  export function createReadableFromGenerator(generator, options) {
2883
2995
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
2884
- (async () => {
2885
- try {
2886
- for await (const chunk of generator()) {
2887
- if (!readable.push(chunk)) {
2888
- // Backpressure
2889
- await new Promise(resolve => setTimeout(resolve, 0));
2890
- }
2891
- }
2892
- readable.push(null);
2893
- }
2894
- catch (err) {
2895
- readable.destroy(err);
2896
- }
2897
- })();
2996
+ pumpAsyncIterableToReadable(readable, generator());
2898
2997
  return readable;
2899
2998
  }
2900
2999
  /**
@@ -2924,8 +3023,8 @@ export function compose(...transforms) {
2924
3023
  transform: chunk => chunk
2925
3024
  });
2926
3025
  }
2927
- const isNativeTransform = (stream) => stream instanceof Transform;
2928
- if (len === 1 && isNativeTransform(transforms[0])) {
3026
+ // Preserve identity: compose(single) returns the same transform.
3027
+ if (len === 1) {
2929
3028
  return transforms[0];
2930
3029
  }
2931
3030
  // Chain the transforms: first → second → ... → last
@@ -2936,19 +3035,34 @@ export function compose(...transforms) {
2936
3035
  transforms[i].pipe(transforms[i + 1]);
2937
3036
  }
2938
3037
  class ComposedTransform extends Transform {
2939
- constructor() {
2940
- super(...arguments);
3038
+ constructor(options) {
3039
+ super(options);
2941
3040
  this._dataForwarding = false;
2942
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
+ }
2943
3051
  }
2944
3052
  on(event, listener) {
2945
3053
  if (event === "data" && !this._dataForwarding) {
2946
3054
  this._dataForwarding = true;
2947
- 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);
2948
3059
  }
2949
3060
  if (event === "end" && !this._endForwarding) {
2950
3061
  this._endForwarding = true;
2951
- last.on("end", () => this.emit("end"));
3062
+ const onEnd = () => {
3063
+ this.emit("end");
3064
+ };
3065
+ this._endForwardCleanup = addEmitterListener(last, "end", onEnd, { once: true });
2952
3066
  }
2953
3067
  return super.on(event, listener);
2954
3068
  }
@@ -2974,6 +3088,18 @@ export function compose(...transforms) {
2974
3088
  return last.pipe(destination);
2975
3089
  }
2976
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;
2977
3103
  for (const t of transforms) {
2978
3104
  t.destroy(error);
2979
3105
  }
@@ -2997,12 +3123,6 @@ export function compose(...transforms) {
2997
3123
  objectMode: first?.objectMode ?? true,
2998
3124
  transform: chunk => chunk
2999
3125
  });
3000
- // Forward errors from any transform
3001
- for (const t of transforms) {
3002
- t.on("error", (err) => {
3003
- composed.emit("error", err);
3004
- });
3005
- }
3006
3126
  // Reflect underlying readability/writability like the previous duck-typed wrapper
3007
3127
  Object.defineProperty(composed, "readable", {
3008
3128
  get: () => last.readable