@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
@@ -55,6 +55,40 @@ const event_emitter_1 = require("./event-emitter.js");
55
55
  const pull_stream_1 = require("./pull-stream.js");
56
56
  const buffered_stream_1 = require("./buffered-stream.js");
57
57
  const shared_1 = require("./shared.js");
58
+ const removeEmitterListener = (emitter, event, listener) => {
59
+ if (typeof emitter.off === "function") {
60
+ emitter.off(event, listener);
61
+ }
62
+ else if (typeof emitter.removeListener === "function") {
63
+ emitter.removeListener(event, listener);
64
+ }
65
+ };
66
+ const addEmitterListener = (emitter, event, listener, options) => {
67
+ if (options?.once && typeof emitter.once === "function") {
68
+ emitter.once(event, listener);
69
+ }
70
+ else {
71
+ emitter.on(event, listener);
72
+ }
73
+ return () => removeEmitterListener(emitter, event, listener);
74
+ };
75
+ const createListenerRegistry = () => {
76
+ const listeners = [];
77
+ return {
78
+ add: (emitter, event, listener) => {
79
+ listeners.push(addEmitterListener(emitter, event, listener));
80
+ },
81
+ once: (emitter, event, listener) => {
82
+ listeners.push(addEmitterListener(emitter, event, listener, { once: true }));
83
+ },
84
+ cleanup: () => {
85
+ for (let i = listeners.length - 1; i >= 0; i--) {
86
+ listeners[i]();
87
+ }
88
+ listeners.length = 0;
89
+ }
90
+ };
91
+ };
58
92
  // =============================================================================
59
93
  // Readable Stream Wrapper
60
94
  // =============================================================================
@@ -71,6 +105,7 @@ class Readable extends event_emitter_1.EventEmitter {
71
105
  this._bufferSize = 0;
72
106
  this._reading = false;
73
107
  this._ended = false;
108
+ this._endEmitted = false;
74
109
  this._destroyed = false;
75
110
  this._errored = null;
76
111
  this._closed = false;
@@ -127,20 +162,7 @@ class Readable extends event_emitter_1.EventEmitter {
127
162
  */
128
163
  static from(iterable, options) {
129
164
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
130
- (async () => {
131
- try {
132
- for await (const chunk of iterable) {
133
- if (!readable.push(chunk)) {
134
- // Backpressure
135
- await new Promise(resolve => setTimeout(resolve, 0));
136
- }
137
- }
138
- readable.push(null);
139
- }
140
- catch (err) {
141
- readable.destroy(err);
142
- }
143
- })();
165
+ pumpAsyncIterableToReadable(readable, toAsyncIterable(iterable));
144
166
  return readable;
145
167
  }
146
168
  /**
@@ -185,16 +207,30 @@ class Readable extends event_emitter_1.EventEmitter {
185
207
  // Controller may already be closed
186
208
  }
187
209
  }
188
- this.emit("end");
210
+ // Emit 'end' only after buffered data is fully drained.
211
+ // This avoids premature 'end' when producers push null while paused.
212
+ if (this._bufferedLength() === 0) {
213
+ this._emitEndOnce();
214
+ }
189
215
  // Note: Don't call destroy() here, let the stream be consumed naturally
190
216
  // The reader will return done:true when it finishes reading
191
217
  return false;
192
218
  }
219
+ // Keep the internal Web ReadableStream in sync for controllable streams.
220
+ // For external Web streams (_webStreamMode=true), push() is not the data source.
221
+ if (controller && !this._webStreamMode) {
222
+ try {
223
+ controller.enqueue(chunk);
224
+ }
225
+ catch {
226
+ // Controller may be closed/errored; Node-side buffering/events still work.
227
+ }
228
+ }
193
229
  if (this._flowing) {
194
230
  // In flowing mode, emit data directly without buffering or enqueueing
195
231
  // const chunkStr = chunk instanceof Uint8Array ? new TextDecoder().decode(chunk.slice(0, 50)) : String(chunk).slice(0, 50);
196
232
  // console.log(`[Readable#${this._id}.push FLOWING] emit data size:${(chunk as any).length || (chunk as any).byteLength} start:"${chunkStr}"`);
197
- this.emit("data", chunk);
233
+ this.emit("data", this._applyEncoding(chunk));
198
234
  // Check if stream was paused during emit (backpressure from consumer)
199
235
  if (!this._flowing) {
200
236
  return false;
@@ -219,10 +255,8 @@ class Readable extends event_emitter_1.EventEmitter {
219
255
  if (!this.objectMode) {
220
256
  this._bufferSize += this._getChunkSize(chunk);
221
257
  }
222
- // NOTE: Do NOT enqueue to Web Stream controller here!
223
- // In push mode, _buffer is the only source of data for data events.
224
- // Web Stream is only used for async iteration when not in push mode.
225
- // Enqueueing here would cause data duplication when _startReading is also running.
258
+ // NOTE: We still buffer for Node-style read()/data semantics.
259
+ // The internal Web ReadableStream is also fed via controller.enqueue() above.
226
260
  // Emit readable event when buffer goes from empty to having data
227
261
  if (wasEmpty) {
228
262
  queueMicrotask(() => this.emit("readable"));
@@ -236,6 +270,13 @@ class Readable extends event_emitter_1.EventEmitter {
236
270
  return this._bufferSize < this.readableHighWaterMark;
237
271
  }
238
272
  }
273
+ _emitEndOnce() {
274
+ if (this._endEmitted) {
275
+ return;
276
+ }
277
+ this._endEmitted = true;
278
+ this.emit("end");
279
+ }
239
280
  /**
240
281
  * Put a chunk back at the front of the buffer
241
282
  * Note: unshift is allowed even after end, as it's used to put back already read data
@@ -260,14 +301,22 @@ class Readable extends event_emitter_1.EventEmitter {
260
301
  if (!this.objectMode) {
261
302
  this._bufferSize -= this._getChunkSize(chunk);
262
303
  }
263
- return this._applyEncoding(chunk);
304
+ const decoded = this._applyEncoding(chunk);
305
+ if (this._ended && this._bufferedLength() === 0) {
306
+ queueMicrotask(() => this._emitEndOnce());
307
+ }
308
+ return decoded;
264
309
  }
265
310
  // For binary mode, handle size
266
311
  const chunk = this._bufferShift();
267
312
  if (!this.objectMode) {
268
313
  this._bufferSize -= this._getChunkSize(chunk);
269
314
  }
270
- return this._applyEncoding(chunk);
315
+ const decoded = this._applyEncoding(chunk);
316
+ if (this._ended && this._bufferedLength() === 0) {
317
+ queueMicrotask(() => this._emitEndOnce());
318
+ }
319
+ return decoded;
271
320
  }
272
321
  return null;
273
322
  }
@@ -373,11 +422,11 @@ class Readable extends event_emitter_1.EventEmitter {
373
422
  if (!this.objectMode) {
374
423
  this._bufferSize -= this._getChunkSize(chunk);
375
424
  }
376
- this.emit("data", chunk);
425
+ this.emit("data", this._applyEncoding(chunk));
377
426
  }
378
427
  // If already ended, emit end event
379
428
  if (this._ended && this._bufferedLength() === 0) {
380
- this.emit("end");
429
+ this._emitEndOnce();
381
430
  }
382
431
  else if (this._read) {
383
432
  // Call user-provided read function asynchronously
@@ -440,26 +489,36 @@ class Readable extends event_emitter_1.EventEmitter {
440
489
  }
441
490
  this._pipeTo.push(dest);
442
491
  // Create listeners that we can later remove
492
+ let drainListener;
493
+ const removeDrainListener = () => {
494
+ if (!drainListener) {
495
+ return;
496
+ }
497
+ if (typeof eventTarget.off === "function") {
498
+ eventTarget.off("drain", drainListener);
499
+ }
500
+ else if (typeof eventTarget.removeListener === "function") {
501
+ eventTarget.removeListener("drain", drainListener);
502
+ }
503
+ drainListener = undefined;
504
+ };
443
505
  const dataListener = (chunk) => {
444
506
  // Call destination's write() method (not internal _writable.write())
445
507
  // This ensures Transform.write() logic runs properly
446
508
  const canWrite = dest.write(chunk);
447
509
  if (!canWrite) {
448
510
  this.pause();
449
- if (typeof eventTarget.once === "function") {
450
- eventTarget.once("drain", () => this.resume());
451
- }
452
- else {
453
- const resumeOnce = () => {
454
- if (typeof eventTarget.off === "function") {
455
- eventTarget.off("drain", resumeOnce);
456
- }
457
- else if (typeof eventTarget.removeListener === "function") {
458
- eventTarget.removeListener("drain", resumeOnce);
459
- }
511
+ // Install a removable, once-style drain listener.
512
+ if (!drainListener) {
513
+ drainListener = () => {
514
+ removeDrainListener();
460
515
  this.resume();
461
516
  };
462
- eventTarget.on("drain", resumeOnce);
517
+ eventTarget.on("drain", drainListener);
518
+ const entry = this._pipeListeners.get(dest);
519
+ if (entry) {
520
+ entry.drain = drainListener;
521
+ }
463
522
  }
464
523
  }
465
524
  };
@@ -479,7 +538,8 @@ class Readable extends event_emitter_1.EventEmitter {
479
538
  this._pipeListeners.set(dest, {
480
539
  data: dataListener,
481
540
  end: endListener,
482
- error: errorListener
541
+ error: errorListener,
542
+ eventTarget
483
543
  });
484
544
  this.on("data", dataListener);
485
545
  this.once("end", endListener);
@@ -502,6 +562,14 @@ class Readable extends event_emitter_1.EventEmitter {
502
562
  this.off("data", listeners.data);
503
563
  this.off("end", listeners.end);
504
564
  this.off("error", listeners.error);
565
+ if (listeners.drain) {
566
+ if (typeof listeners.eventTarget?.off === "function") {
567
+ listeners.eventTarget.off("drain", listeners.drain);
568
+ }
569
+ else if (typeof listeners.eventTarget?.removeListener === "function") {
570
+ listeners.eventTarget.removeListener("drain", listeners.drain);
571
+ }
572
+ }
505
573
  this._pipeListeners.delete(destination);
506
574
  }
507
575
  }
@@ -513,6 +581,14 @@ class Readable extends event_emitter_1.EventEmitter {
513
581
  this.off("data", listeners.data);
514
582
  this.off("end", listeners.end);
515
583
  this.off("error", listeners.error);
584
+ if (listeners.drain) {
585
+ if (typeof listeners.eventTarget?.off === "function") {
586
+ listeners.eventTarget.off("drain", listeners.drain);
587
+ }
588
+ else if (typeof listeners.eventTarget?.removeListener === "function") {
589
+ listeners.eventTarget.removeListener("drain", listeners.drain);
590
+ }
591
+ }
516
592
  this._pipeListeners.delete(target);
517
593
  }
518
594
  }
@@ -531,12 +607,26 @@ class Readable extends event_emitter_1.EventEmitter {
531
607
  }
532
608
  this._destroyed = true;
533
609
  this._ended = true;
610
+ // Ensure we detach from destinations to avoid leaking listeners.
611
+ this.unpipe();
534
612
  if (error) {
535
613
  this._errored = error;
536
614
  this.emit("error", error);
537
615
  }
538
616
  if (this._reader) {
539
- this._reader.cancel().catch(() => { });
617
+ const reader = this._reader;
618
+ this._reader = null;
619
+ reader
620
+ .cancel()
621
+ .catch(() => { })
622
+ .finally(() => {
623
+ try {
624
+ reader.releaseLock();
625
+ }
626
+ catch {
627
+ // Ignore if a read is still pending
628
+ }
629
+ });
540
630
  }
541
631
  this._closed = true;
542
632
  this.emit("close");
@@ -615,18 +705,38 @@ class Readable extends event_emitter_1.EventEmitter {
615
705
  const { done, value } = await this._reader.read();
616
706
  // Check _pushMode again after async read - if push() was called, stop reading
617
707
  if (this._pushMode) {
708
+ if (this._reader) {
709
+ const reader = this._reader;
710
+ this._reader = null;
711
+ try {
712
+ reader.releaseLock();
713
+ }
714
+ catch {
715
+ // Ignore if a read is still pending
716
+ }
717
+ }
618
718
  break;
619
719
  }
620
720
  if (done) {
621
721
  this._ended = true;
622
- this.emit("end");
722
+ this._emitEndOnce();
723
+ if (this._reader) {
724
+ const reader = this._reader;
725
+ this._reader = null;
726
+ try {
727
+ reader.releaseLock();
728
+ }
729
+ catch {
730
+ // Ignore if a read is still pending
731
+ }
732
+ }
623
733
  break;
624
734
  }
625
735
  if (value !== undefined) {
626
736
  // In flowing mode, emit data directly without buffering
627
737
  // Only buffer if not flowing (paused mode)
628
738
  if (this._flowing) {
629
- this.emit("data", value);
739
+ this.emit("data", this._applyEncoding(value));
630
740
  }
631
741
  else {
632
742
  this._buffer.push(value);
@@ -639,6 +749,16 @@ class Readable extends event_emitter_1.EventEmitter {
639
749
  }
640
750
  catch (err) {
641
751
  this.emit("error", err);
752
+ if (this._reader) {
753
+ const reader = this._reader;
754
+ this._reader = null;
755
+ try {
756
+ reader.releaseLock();
757
+ }
758
+ catch {
759
+ // Ignore if a read is still pending
760
+ }
761
+ }
642
762
  }
643
763
  finally {
644
764
  this._reading = false;
@@ -646,7 +766,8 @@ class Readable extends event_emitter_1.EventEmitter {
646
766
  }
647
767
  /**
648
768
  * Async iterator support
649
- * Uses Web Stream reader for non-push mode, event-based for push mode
769
+ * Uses a unified event-queue iterator with simple backpressure.
770
+ * This matches Node's behavior more closely (iterator drives flowing mode).
650
771
  */
651
772
  async *[Symbol.asyncIterator]() {
652
773
  // First yield any buffered data
@@ -655,117 +776,124 @@ class Readable extends event_emitter_1.EventEmitter {
655
776
  if (!this.objectMode) {
656
777
  this._bufferSize -= this._getChunkSize(chunk);
657
778
  }
658
- yield chunk;
779
+ yield this._applyEncoding(chunk);
659
780
  }
660
- // If already ended, we're done
661
781
  if (this._ended) {
662
782
  return;
663
783
  }
664
- // For controllable streams (not created from external Web Stream),
665
- // use event-based iteration since data comes from push() calls
666
- if (!this._webStreamMode) {
667
- // Create a promise-based queue for incoming data
668
- const dataQueue = [];
669
- let resolveNext = null;
670
- let rejectNext = null;
671
- let done = false;
672
- let streamError = null;
673
- let dataQueueIndex = 0;
674
- const dataHandler = (chunk) => {
675
- if (resolveNext) {
676
- resolveNext(chunk);
677
- resolveNext = null;
678
- rejectNext = null;
679
- }
680
- else {
681
- dataQueue.push(chunk);
682
- }
683
- };
684
- const endHandler = () => {
685
- done = true;
686
- if (resolveNext) {
687
- resolveNext(null);
688
- resolveNext = null;
689
- rejectNext = null;
690
- }
691
- };
692
- const errorHandler = (err) => {
693
- done = true;
694
- streamError = err;
695
- if (rejectNext) {
696
- rejectNext(err);
697
- resolveNext = null;
698
- rejectNext = null;
699
- }
700
- };
701
- const closeHandler = () => {
702
- // If stream closed without end event (e.g., after destroy()),
703
- // treat it as done
704
- done = true;
705
- if (resolveNext) {
706
- resolveNext(null);
707
- resolveNext = null;
708
- rejectNext = null;
709
- }
710
- };
711
- this.on("data", dataHandler);
712
- this.on("end", endHandler);
713
- this.on("error", errorHandler);
714
- this.on("close", closeHandler);
715
- try {
716
- // Enter flowing mode
717
- this.resume();
718
- while (!done || dataQueueIndex < dataQueue.length) {
719
- // Check for error before processing
720
- if (streamError) {
721
- throw streamError;
722
- }
723
- if (dataQueueIndex < dataQueue.length) {
724
- const chunk = dataQueue[dataQueueIndex++];
725
- if (dataQueueIndex >= 1024 && dataQueueIndex * 2 >= dataQueue.length) {
726
- dataQueue.splice(0, dataQueueIndex);
727
- dataQueueIndex = 0;
728
- }
729
- yield chunk;
730
- }
731
- else if (!done) {
732
- const chunk = await new Promise((resolve, reject) => {
733
- resolveNext = resolve;
734
- rejectNext = reject;
735
- });
736
- if (chunk !== null) {
737
- yield chunk;
738
- }
739
- }
740
- }
741
- // Check for error after loop
742
- if (streamError) {
743
- throw streamError;
744
- }
784
+ const highWaterMark = this.readableHighWaterMark;
785
+ const lowWaterMark = Math.max(0, Math.floor(highWaterMark / 2));
786
+ const chunkSizeForBackpressure = (chunk) => {
787
+ if (this.objectMode) {
788
+ return 1;
745
789
  }
746
- finally {
747
- this.off("data", dataHandler);
748
- this.off("end", endHandler);
749
- this.off("error", errorHandler);
750
- this.off("close", closeHandler);
790
+ if (chunk instanceof Uint8Array) {
791
+ return chunk.byteLength;
751
792
  }
752
- return;
753
- }
754
- // For Web Stream mode, use the underlying reader
755
- if (!this._reader) {
756
- this._reader = this._stream.getReader();
757
- }
793
+ if (typeof chunk === "string") {
794
+ return chunk.length;
795
+ }
796
+ return 1;
797
+ };
798
+ const dataQueue = [];
799
+ let dataQueueIndex = 0;
800
+ let queuedSize = 0;
801
+ let resolveNext = null;
802
+ let rejectNext = null;
803
+ let done = false;
804
+ let pausedByIterator = false;
805
+ let streamError = null;
806
+ const dataHandler = (chunk) => {
807
+ // data events are already encoding-aware; do not decode again here.
808
+ if (resolveNext) {
809
+ resolveNext(chunk);
810
+ resolveNext = null;
811
+ rejectNext = null;
812
+ }
813
+ else {
814
+ dataQueue.push(chunk);
815
+ }
816
+ queuedSize += chunkSizeForBackpressure(chunk);
817
+ if (!pausedByIterator && queuedSize >= highWaterMark) {
818
+ pausedByIterator = true;
819
+ this.pause();
820
+ }
821
+ };
822
+ const endHandler = () => {
823
+ done = true;
824
+ if (resolveNext) {
825
+ resolveNext(null);
826
+ resolveNext = null;
827
+ rejectNext = null;
828
+ }
829
+ };
830
+ const closeHandler = () => {
831
+ done = true;
832
+ if (resolveNext) {
833
+ resolveNext(null);
834
+ resolveNext = null;
835
+ rejectNext = null;
836
+ }
837
+ };
838
+ const errorHandler = (err) => {
839
+ done = true;
840
+ streamError = err;
841
+ if (rejectNext) {
842
+ rejectNext(err);
843
+ resolveNext = null;
844
+ rejectNext = null;
845
+ }
846
+ };
847
+ this.on("data", dataHandler);
848
+ this.on("end", endHandler);
849
+ this.on("error", errorHandler);
850
+ this.on("close", closeHandler);
758
851
  try {
852
+ // Iterator consumption should drive the stream.
853
+ this.resume();
759
854
  while (true) {
760
- const { done, value } = await this._reader.read();
855
+ if (streamError) {
856
+ throw streamError;
857
+ }
858
+ if (dataQueueIndex < dataQueue.length) {
859
+ const chunk = dataQueue[dataQueueIndex++];
860
+ queuedSize -= chunkSizeForBackpressure(chunk);
861
+ if (dataQueueIndex >= 1024 && dataQueueIndex * 2 >= dataQueue.length) {
862
+ dataQueue.splice(0, dataQueueIndex);
863
+ dataQueueIndex = 0;
864
+ }
865
+ if (pausedByIterator && queuedSize <= lowWaterMark && !done && !this._destroyed) {
866
+ pausedByIterator = false;
867
+ this.resume();
868
+ }
869
+ yield chunk;
870
+ continue;
871
+ }
761
872
  if (done) {
762
873
  break;
763
874
  }
764
- yield value;
875
+ const chunk = await new Promise((resolve, reject) => {
876
+ resolveNext = resolve;
877
+ rejectNext = reject;
878
+ });
879
+ if (chunk !== null) {
880
+ queuedSize -= chunkSizeForBackpressure(chunk);
881
+ if (pausedByIterator && queuedSize <= lowWaterMark && !done && !this._destroyed) {
882
+ pausedByIterator = false;
883
+ this.resume();
884
+ }
885
+ yield chunk;
886
+ }
887
+ }
888
+ if (streamError) {
889
+ throw streamError;
765
890
  }
766
891
  }
767
892
  finally {
768
- this._reader.releaseLock();
893
+ this.off("data", dataHandler);
894
+ this.off("end", endHandler);
895
+ this.off("error", errorHandler);
896
+ this.off("close", closeHandler);
769
897
  }
770
898
  }
771
899
  /**
@@ -985,6 +1113,32 @@ class Readable extends event_emitter_1.EventEmitter {
985
1113
  }
986
1114
  }
987
1115
  exports.Readable = Readable;
1116
+ function toAsyncIterable(iterable) {
1117
+ if (iterable && typeof iterable[Symbol.asyncIterator] === "function") {
1118
+ return iterable;
1119
+ }
1120
+ return (async function* () {
1121
+ for (const item of iterable) {
1122
+ yield item;
1123
+ }
1124
+ })();
1125
+ }
1126
+ function pumpAsyncIterableToReadable(readable, iterable) {
1127
+ (async () => {
1128
+ try {
1129
+ for await (const chunk of iterable) {
1130
+ if (!readable.push(chunk)) {
1131
+ // Simple backpressure: yield to consumer.
1132
+ await new Promise(resolve => setTimeout(resolve, 0));
1133
+ }
1134
+ }
1135
+ readable.push(null);
1136
+ }
1137
+ catch (err) {
1138
+ readable.destroy(err);
1139
+ }
1140
+ })();
1141
+ }
988
1142
  // =============================================================================
989
1143
  // Writable Stream Wrapper
990
1144
  // =============================================================================
@@ -1002,10 +1156,12 @@ class Writable extends event_emitter_1.EventEmitter {
1002
1156
  this._closed = false;
1003
1157
  this._pendingWrites = 0;
1004
1158
  this._writableLength = 0;
1159
+ this._needDrain = false;
1005
1160
  this._corked = 0;
1006
1161
  this._corkedChunks = [];
1007
1162
  this._defaultEncoding = "utf8";
1008
1163
  this._aborted = false;
1164
+ this._ownsStream = false;
1009
1165
  this.objectMode = options?.objectMode ?? false;
1010
1166
  this.writableHighWaterMark = options?.highWaterMark ?? 16384;
1011
1167
  this.autoDestroy = options?.autoDestroy ?? true;
@@ -1021,8 +1177,10 @@ class Writable extends event_emitter_1.EventEmitter {
1021
1177
  }
1022
1178
  if (options?.stream) {
1023
1179
  this._stream = options.stream;
1180
+ this._ownsStream = false;
1024
1181
  }
1025
1182
  else {
1183
+ this._ownsStream = true;
1026
1184
  // Create bound references to instance properties/methods for use in WritableStream callbacks
1027
1185
  const getWriteFunc = () => this._writeFunc;
1028
1186
  const getFinalFunc = () => this._finalFunc;
@@ -1129,25 +1287,37 @@ class Writable extends event_emitter_1.EventEmitter {
1129
1287
  this._writableLength += chunkSize;
1130
1288
  return this._writableLength < this.writableHighWaterMark;
1131
1289
  }
1132
- return this._doWrite(chunk, cb);
1290
+ const ok = this._doWrite(chunk, cb);
1291
+ if (!ok) {
1292
+ this._needDrain = true;
1293
+ }
1294
+ return ok;
1133
1295
  }
1134
1296
  _doWrite(chunk, callback) {
1135
1297
  // Track pending writes for writableLength
1136
1298
  const chunkSize = this._getChunkSize(chunk);
1137
1299
  this._pendingWrites++;
1138
1300
  this._writableLength += chunkSize;
1139
- this._getWriter()
1301
+ const writer = this._getWriter();
1302
+ writer
1140
1303
  .write(chunk)
1141
1304
  .then(() => {
1142
1305
  this._pendingWrites--;
1143
1306
  this._writableLength -= chunkSize;
1144
- this.emit("drain");
1307
+ if (this._needDrain && this._writableLength < this.writableHighWaterMark) {
1308
+ this._needDrain = false;
1309
+ this.emit("drain");
1310
+ }
1145
1311
  callback?.(null);
1146
1312
  })
1147
1313
  .catch(err => {
1148
1314
  this._pendingWrites--;
1149
1315
  this._writableLength -= chunkSize;
1150
- this.emit("error", err);
1316
+ // Avoid double-emitting if we're already in an errored/destroyed state.
1317
+ if (!this._destroyed) {
1318
+ this._errored = err;
1319
+ this.emit("error", err);
1320
+ }
1151
1321
  callback?.(err);
1152
1322
  });
1153
1323
  // Return false if we've exceeded high water mark (for backpressure)
@@ -1178,12 +1348,29 @@ class Writable extends event_emitter_1.EventEmitter {
1178
1348
  : callback;
1179
1349
  const finish = async () => {
1180
1350
  try {
1351
+ const writer = this._getWriter();
1181
1352
  if (chunk !== undefined) {
1182
- await this._getWriter().write(chunk);
1353
+ await writer.write(chunk);
1354
+ }
1355
+ await writer.close();
1356
+ if (this._writer === writer) {
1357
+ this._writer = null;
1358
+ try {
1359
+ writer.releaseLock();
1360
+ }
1361
+ catch {
1362
+ // Ignore
1363
+ }
1364
+ }
1365
+ // If we own the underlying Web WritableStream, its `close()` handler already
1366
+ // emits finish/close. For external streams, we must emit finish ourselves.
1367
+ if (!this._ownsStream) {
1368
+ this._finished = true;
1369
+ this.emit("finish");
1370
+ if (this.emitClose) {
1371
+ this.emit("close");
1372
+ }
1183
1373
  }
1184
- await this._getWriter().close();
1185
- this._finished = true;
1186
- this.emit("finish");
1187
1374
  if (cb) {
1188
1375
  cb();
1189
1376
  }
@@ -1204,12 +1391,24 @@ class Writable extends event_emitter_1.EventEmitter {
1204
1391
  }
1205
1392
  this._destroyed = true;
1206
1393
  this._ended = true;
1207
- if (error) {
1394
+ if (error && !this._errored) {
1208
1395
  this._errored = error;
1209
1396
  this.emit("error", error);
1210
1397
  }
1211
1398
  if (this._writer) {
1212
- this._writer.abort(error).catch(() => { });
1399
+ const writer = this._writer;
1400
+ this._writer = null;
1401
+ writer
1402
+ .abort(error)
1403
+ .catch(() => { })
1404
+ .finally(() => {
1405
+ try {
1406
+ writer.releaseLock();
1407
+ }
1408
+ catch {
1409
+ // Ignore
1410
+ }
1411
+ });
1213
1412
  }
1214
1413
  this._closed = true;
1215
1414
  this.emit("close");
@@ -1361,342 +1560,259 @@ function normalizeWritable(stream) {
1361
1560
  */
1362
1561
  class Transform extends event_emitter_1.EventEmitter {
1363
1562
  /**
1364
- * Push data to the readable side (Node.js compatibility)
1365
- * Can be called from within transform callback
1563
+ * Push data to the readable side (Node.js compatibility).
1564
+ * Intended to be called from within transform/flush.
1366
1565
  */
1367
1566
  push(chunk) {
1368
- if (chunk === null) {
1369
- return false;
1370
- }
1371
- if (this._transformController) {
1372
- // If we're in a transform callback, enqueue directly
1373
- this._transformController.enqueue(chunk);
1374
- }
1375
- else {
1376
- // Otherwise buffer for later
1377
- this._pushBuffer.push(chunk);
1378
- }
1379
- return true;
1567
+ return this._readable.push(chunk);
1380
1568
  }
1381
1569
  constructor(options) {
1382
1570
  super();
1383
- this._ended = false;
1384
1571
  this._destroyed = false;
1572
+ this._ended = false;
1385
1573
  this._errored = false;
1386
- // Buffer for Node.js style push() calls during transform
1387
- this._pushBuffer = [];
1388
- // Controller for enqueueing pushed data (set during transform execution)
1389
- this._transformController = null;
1390
- // Buffer for writes that occur after end() but before writable is closed
1391
- this._pendingEndWrites = [];
1392
- // Whether end() has been called but writable not yet closed
1393
- this._endPending = false;
1394
- // Track if we've already set up data forwarding
1395
1574
  this._dataForwardingSetup = false;
1396
- /** @internal - whether we have a data event consumer */
1397
- this._hasDataConsumer = false;
1398
- /** @internal - whether we're auto-consuming the readable */
1399
- this._readableConsuming = false;
1400
- /** @internal - buffer for auto-consumed data */
1401
- this._autoConsumedBuffer = [];
1402
- this._autoConsumedBufferIndex = 0;
1403
- /** @internal - whether auto-consume has ended */
1404
- this._autoConsumeEnded = false;
1405
- /** @internal - promise that resolves when auto-consume finishes */
1406
- this._autoConsumePromise = null;
1407
- /** @internal - list of piped destinations for forwarding auto-consumed data */
1408
- this._pipeDestinations = [];
1575
+ this._endTimer = null;
1576
+ this._webStream = null;
1577
+ this._sideForwardingCleanup = null;
1409
1578
  this.objectMode = options?.objectMode ?? false;
1410
- const userTransform = options?.transform;
1411
- const userFlush = options?.flush;
1412
- // Create bound references for use in TransformStream callbacks
1413
- const setController = (ctrl) => {
1414
- this._transformController = ctrl;
1415
- };
1416
- const emitEvent = (event, ...args) => {
1417
- if (event === "error") {
1418
- // Only emit error once to prevent duplicate events
1419
- if (this._errored) {
1420
- return false;
1421
- }
1422
- this._errored = true;
1423
- // Also destroy the writable to prevent further writes
1424
- this._writable.destroy(args[0]);
1579
+ this._transformImpl = options?.transform;
1580
+ this._flushImpl = options?.flush;
1581
+ this._readable = new Readable({
1582
+ objectMode: this.objectMode
1583
+ });
1584
+ this._writable = new Writable({
1585
+ objectMode: this.objectMode,
1586
+ write: (chunk, _encoding, callback) => {
1587
+ this._runTransform(chunk)
1588
+ .then(() => callback(null))
1589
+ .catch(err => callback(err));
1590
+ },
1591
+ final: callback => {
1592
+ this._runFlush()
1593
+ .then(() => {
1594
+ this._readable.push(null);
1595
+ callback(null);
1596
+ })
1597
+ .catch(err => callback(err));
1425
1598
  }
1426
- return this.emit(event, ...args);
1427
- };
1428
- const getInstance = () => this;
1429
- // Check if subclass overrides _transform (for Node.js compatibility)
1430
- // We need to check this at runtime since the subclass constructor runs after super()
1431
- const hasSubclassTransform = () => {
1432
- // If userTransform was provided in options, use that
1433
- if (userTransform) {
1434
- return false;
1599
+ });
1600
+ this._setupSideForwarding();
1601
+ }
1602
+ _setupSideForwarding() {
1603
+ if (this._sideForwardingCleanup) {
1604
+ this._sideForwardingCleanup();
1605
+ this._sideForwardingCleanup = null;
1606
+ }
1607
+ const registry = createListenerRegistry();
1608
+ registry.once(this._readable, "end", () => this.emit("end"));
1609
+ registry.add(this._readable, "error", err => this._emitErrorOnce(err));
1610
+ registry.once(this._writable, "finish", () => this.emit("finish"));
1611
+ registry.add(this._writable, "drain", () => this.emit("drain"));
1612
+ registry.add(this._writable, "error", err => this._emitErrorOnce(err));
1613
+ this._sideForwardingCleanup = () => registry.cleanup();
1614
+ }
1615
+ _scheduleEnd() {
1616
+ if (this._destroyed || this._errored) {
1617
+ return;
1618
+ }
1619
+ if (this._writable.writableEnded) {
1620
+ return;
1621
+ }
1622
+ if (this._endTimer) {
1623
+ clearTimeout(this._endTimer);
1624
+ }
1625
+ // Defer closing to allow writes triggered during 'data' callbacks.
1626
+ this._endTimer = setTimeout(() => {
1627
+ this._endTimer = null;
1628
+ if (this._destroyed || this._errored || this._writable.writableEnded) {
1629
+ return;
1435
1630
  }
1436
- // Check if _transform is overridden (not the base class no-op)
1437
- const proto = Object.getPrototypeOf(this);
1438
- return proto._transform !== Transform.prototype._transform;
1439
- };
1440
- const hasSubclassFlush = () => {
1441
- if (userFlush) {
1442
- return false;
1631
+ this._writable.end();
1632
+ }, 0);
1633
+ }
1634
+ _emitErrorOnce(err) {
1635
+ if (this._errored) {
1636
+ return;
1637
+ }
1638
+ this._errored = true;
1639
+ const error = err instanceof Error ? err : new Error(String(err));
1640
+ this.emit("error", error);
1641
+ if (!this._destroyed) {
1642
+ this._destroyed = true;
1643
+ this._readable.destroy(error);
1644
+ this._writable.destroy(error);
1645
+ queueMicrotask(() => this.emit("close"));
1646
+ }
1647
+ }
1648
+ _hasSubclassTransform() {
1649
+ if (this._transformImpl) {
1650
+ return false;
1651
+ }
1652
+ const proto = Object.getPrototypeOf(this);
1653
+ return proto._transform !== Transform.prototype._transform;
1654
+ }
1655
+ _hasSubclassFlush() {
1656
+ if (this._flushImpl) {
1657
+ return false;
1658
+ }
1659
+ const proto = Object.getPrototypeOf(this);
1660
+ return proto._flush !== Transform.prototype._flush;
1661
+ }
1662
+ async _runTransform(chunk) {
1663
+ if (this._destroyed || this._errored) {
1664
+ throw new Error(this._errored ? "Cannot write after stream errored" : "Cannot write after stream destroyed");
1665
+ }
1666
+ try {
1667
+ if (this._hasSubclassTransform()) {
1668
+ await new Promise((resolve, reject) => {
1669
+ this._transform(chunk, "utf8", (err, data) => {
1670
+ if (err) {
1671
+ reject(err);
1672
+ return;
1673
+ }
1674
+ if (data !== undefined) {
1675
+ this.push(data);
1676
+ }
1677
+ resolve();
1678
+ });
1679
+ });
1680
+ return;
1443
1681
  }
1444
- const proto = Object.getPrototypeOf(this);
1445
- return proto._flush !== Transform.prototype._flush;
1446
- };
1447
- this._stream = new TransformStream({
1448
- transform: async (chunk, controller) => {
1449
- // Skip processing if already errored
1450
- if (this._errored) {
1451
- return;
1452
- }
1453
- try {
1454
- // Set controller for push() to use
1455
- setController(controller);
1456
- // Check for subclass _transform override first
1457
- if (hasSubclassTransform()) {
1458
- // Call subclass _transform method (Node.js style)
1459
- // _transform signature is (chunk, encoding, callback)
1460
- await new Promise((resolve, reject) => {
1461
- this._transform(chunk, "utf8", (err, data) => {
1462
- if (err) {
1463
- reject(err);
1464
- }
1465
- else {
1466
- if (data !== undefined) {
1467
- controller.enqueue(data);
1468
- }
1469
- resolve();
1470
- }
1471
- });
1472
- });
1473
- }
1474
- else if (userTransform) {
1475
- const transformParamCount = userTransform.length;
1476
- if (transformParamCount >= 3) {
1477
- // Node.js style: transform(chunk, encoding, callback)
1478
- await new Promise((resolve, reject) => {
1479
- userTransform.call(getInstance(), chunk, "utf8", (err, data) => {
1480
- if (err) {
1481
- reject(err);
1482
- }
1483
- else {
1484
- if (data !== undefined) {
1485
- controller.enqueue(data);
1486
- }
1487
- resolve();
1488
- }
1489
- });
1490
- });
1682
+ const userTransform = this._transformImpl;
1683
+ if (!userTransform) {
1684
+ this.push(chunk);
1685
+ return;
1686
+ }
1687
+ const paramCount = userTransform.length;
1688
+ if (paramCount >= 3) {
1689
+ await new Promise((resolve, reject) => {
1690
+ userTransform.call(this, chunk, "utf8", (err, data) => {
1691
+ if (err) {
1692
+ reject(err);
1693
+ return;
1491
1694
  }
1492
- else if (transformParamCount === 2) {
1493
- await new Promise((resolve, reject) => {
1494
- userTransform.call(getInstance(), chunk, (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
- });
1695
+ if (data !== undefined) {
1696
+ this.push(data);
1506
1697
  }
1507
- else {
1508
- // Simple style: transform(chunk) => result
1509
- const result = userTransform.call(getInstance(), chunk);
1510
- if (result && typeof result.then === "function") {
1511
- const awaitedResult = await result;
1512
- if (awaitedResult !== undefined) {
1513
- controller.enqueue(awaitedResult);
1514
- }
1515
- }
1516
- else {
1517
- if (result !== undefined) {
1518
- controller.enqueue(result);
1519
- }
1520
- }
1698
+ resolve();
1699
+ });
1700
+ });
1701
+ return;
1702
+ }
1703
+ if (paramCount === 2) {
1704
+ await new Promise((resolve, reject) => {
1705
+ userTransform.call(this, chunk, (err, data) => {
1706
+ if (err) {
1707
+ reject(err);
1708
+ return;
1521
1709
  }
1522
- }
1523
- else {
1524
- // Default: pass through
1525
- controller.enqueue(chunk);
1526
- }
1527
- }
1528
- catch (err) {
1529
- controller.error(err);
1530
- emitEvent("error", err);
1531
- }
1532
- finally {
1533
- setController(null);
1710
+ if (data !== undefined) {
1711
+ this.push(data);
1712
+ }
1713
+ resolve();
1714
+ });
1715
+ });
1716
+ return;
1717
+ }
1718
+ const result = userTransform.call(this, chunk);
1719
+ if (result && typeof result.then === "function") {
1720
+ const awaited = await result;
1721
+ if (awaited !== undefined) {
1722
+ this.push(awaited);
1534
1723
  }
1535
- },
1536
- flush: async (controller) => {
1537
- try {
1538
- setController(controller);
1539
- // Check for subclass _flush override first
1540
- if (hasSubclassFlush()) {
1541
- await new Promise((resolve, reject) => {
1542
- this._flush((err, data) => {
1543
- if (err) {
1544
- reject(err);
1545
- }
1546
- else {
1547
- if (data !== undefined) {
1548
- controller.enqueue(data);
1549
- }
1550
- resolve();
1551
- }
1552
- });
1553
- });
1554
- }
1555
- else if (userFlush) {
1556
- const flushParamCount = userFlush.length;
1557
- if (flushParamCount >= 1) {
1558
- // Node.js style: flush(callback)
1559
- await new Promise((resolve, reject) => {
1560
- userFlush.call(getInstance(), (err, data) => {
1561
- if (err) {
1562
- reject(err);
1563
- }
1564
- else {
1565
- if (data !== undefined) {
1566
- controller.enqueue(data);
1567
- }
1568
- resolve();
1569
- }
1570
- });
1571
- });
1724
+ return;
1725
+ }
1726
+ if (result !== undefined) {
1727
+ this.push(result);
1728
+ }
1729
+ }
1730
+ catch (err) {
1731
+ this._emitErrorOnce(err);
1732
+ throw err;
1733
+ }
1734
+ }
1735
+ async _runFlush() {
1736
+ if (this._destroyed || this._errored) {
1737
+ return;
1738
+ }
1739
+ try {
1740
+ if (this._hasSubclassFlush()) {
1741
+ await new Promise((resolve, reject) => {
1742
+ this._flush((err, data) => {
1743
+ if (err) {
1744
+ reject(err);
1745
+ return;
1572
1746
  }
1573
- else {
1574
- // Simple style: flush() => result
1575
- const result = userFlush.call(getInstance());
1576
- if (result && typeof result.then === "function") {
1577
- const awaitedResult = await result;
1578
- if (awaitedResult !== undefined && awaitedResult !== null) {
1579
- controller.enqueue(awaitedResult);
1580
- }
1581
- }
1582
- else {
1583
- if (result !== undefined && result !== null) {
1584
- controller.enqueue(result);
1585
- }
1586
- }
1747
+ if (data !== undefined) {
1748
+ this.push(data);
1587
1749
  }
1588
- }
1589
- // No flush defined - nothing to do
1590
- }
1591
- catch (err) {
1592
- controller.error(err);
1593
- emitEvent("error", err);
1594
- }
1595
- finally {
1596
- setController(null);
1597
- }
1750
+ resolve();
1751
+ });
1752
+ });
1753
+ return;
1598
1754
  }
1599
- });
1600
- this._readable = new Readable({
1601
- stream: this._stream.readable,
1602
- objectMode: this.objectMode
1603
- });
1604
- this._writable = new Writable({
1605
- stream: this._stream.writable,
1606
- objectMode: this.objectMode
1607
- });
1608
- // Forward non-data events (data forwarding is lazy to avoid premature flowing)
1609
- this._readable.on("end", () => this.emit("end"));
1610
- // Only forward errors if not already errored (to prevent duplicate events)
1611
- this._readable.on("error", err => {
1612
- if (!this._errored) {
1613
- this._errored = true;
1614
- this.emit("error", err);
1755
+ const userFlush = this._flushImpl;
1756
+ if (!userFlush) {
1757
+ return;
1615
1758
  }
1616
- });
1617
- this._writable.on("finish", () => this.emit("finish"));
1618
- this._writable.on("drain", () => this.emit("drain"));
1619
- // Only forward errors if not already errored (to prevent duplicate events)
1620
- this._writable.on("error", err => {
1621
- if (!this._errored) {
1622
- this._errored = true;
1623
- this.emit("error", err);
1759
+ const paramCount = userFlush.length;
1760
+ if (paramCount >= 1) {
1761
+ await new Promise((resolve, reject) => {
1762
+ userFlush.call(this, (err, data) => {
1763
+ if (err) {
1764
+ reject(err);
1765
+ return;
1766
+ }
1767
+ if (data !== undefined) {
1768
+ this.push(data);
1769
+ }
1770
+ resolve();
1771
+ });
1772
+ });
1773
+ return;
1624
1774
  }
1625
- });
1775
+ const result = userFlush.call(this);
1776
+ if (result && typeof result.then === "function") {
1777
+ const awaited = await result;
1778
+ if (awaited !== undefined && awaited !== null) {
1779
+ this.push(awaited);
1780
+ }
1781
+ return;
1782
+ }
1783
+ if (result !== undefined && result !== null) {
1784
+ this.push(result);
1785
+ }
1786
+ }
1787
+ catch (err) {
1788
+ this._emitErrorOnce(err);
1789
+ throw err;
1790
+ }
1626
1791
  }
1627
1792
  /**
1628
- * Override on to start flowing when data listener is added
1793
+ * Override on() to lazily forward readable 'data' events.
1794
+ * Avoids starting flowing mode unless requested.
1629
1795
  */
1630
1796
  on(event, listener) {
1631
- // Set up data forwarding when first external data listener is added
1632
1797
  if (event === "data" && !this._dataForwardingSetup) {
1633
1798
  this._dataForwardingSetup = true;
1634
- this._readable.on("data", data => this.emit("data", data));
1635
- }
1636
- super.on(event, listener);
1637
- // When data listener is added, mark as having consumer
1638
- // and start the readable in flowing mode
1639
- if (event === "data") {
1640
- this._hasDataConsumer = true;
1641
- this._readable.resume();
1799
+ this._readable.on("data", chunk => this.emit("data", chunk));
1642
1800
  }
1643
- return this;
1801
+ return super.on(event, listener);
1644
1802
  }
1645
1803
  write(chunk, encodingOrCallback, callback) {
1646
1804
  const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
1647
- if (this._destroyed || this._errored) {
1648
- const err = new Error(this._errored ? "Cannot write after stream errored" : "Cannot write after stream destroyed");
1649
- queueMicrotask(() => this.emit("error", err));
1650
- cb?.(err);
1651
- return false;
1652
- }
1653
- // Ensure readable is being consumed to allow transform to execute
1654
- // This matches Node.js behavior where transform executes immediately on write
1655
- // Only auto-consume if no explicit consumer (data listener or pipe)
1656
- if (!this._readableConsuming && !this._hasDataConsumer) {
1657
- this._readableConsuming = true;
1658
- this._startAutoConsume();
1659
- }
1660
- // If end() was called but writable not yet closed, buffer the write
1661
- // This allows writes during data event handlers to be processed
1662
- if (this._endPending) {
1663
- this._pendingEndWrites.push({ chunk, callback: cb });
1664
- return true;
1805
+ // If end() has been requested, keep the close deferred as long as writes continue.
1806
+ if (this._ended && !this._writable.writableEnded) {
1807
+ this._scheduleEnd();
1665
1808
  }
1666
1809
  return this._writable.write(chunk, cb);
1667
1810
  }
1668
- /** @internal - auto-consume readable to allow transform to execute */
1669
- _startAutoConsume() {
1670
- this._autoConsumePromise = (async () => {
1671
- try {
1672
- for await (const chunk of this._readable) {
1673
- // Buffer the data for later retrieval
1674
- this._autoConsumedBuffer.push(chunk);
1675
- // Forward to any piped destinations
1676
- for (const dest of this._pipeDestinations) {
1677
- dest.write(chunk);
1678
- }
1679
- // Also emit data event for listeners
1680
- this.emit("data", chunk);
1681
- }
1682
- this._autoConsumeEnded = true;
1683
- // End all piped destinations
1684
- for (const dest of this._pipeDestinations) {
1685
- dest.end();
1686
- }
1687
- this.emit("end");
1688
- }
1689
- catch (err) {
1690
- this.emit("error", err);
1691
- }
1692
- })();
1693
- }
1694
1811
  end(chunkOrCallback, encodingOrCallback, callback) {
1695
1812
  if (this._ended) {
1696
1813
  return this;
1697
1814
  }
1698
1815
  this._ended = true;
1699
- this._endPending = true;
1700
1816
  const chunk = typeof chunkOrCallback === "function" ? undefined : chunkOrCallback;
1701
1817
  const cb = typeof chunkOrCallback === "function"
1702
1818
  ? chunkOrCallback
@@ -1709,18 +1825,7 @@ class Transform extends event_emitter_1.EventEmitter {
1709
1825
  if (chunk !== undefined) {
1710
1826
  this._writable.write(chunk);
1711
1827
  }
1712
- // Use setTimeout(0) instead of queueMicrotask to ensure all transform
1713
- // processing and data events complete before we close the writable.
1714
- // Microtasks run before the TransformStream processes data.
1715
- setTimeout(() => {
1716
- // Process any writes that occurred during data events
1717
- for (const { chunk: pendingChunk, callback } of this._pendingEndWrites) {
1718
- this._writable.write(pendingChunk, callback);
1719
- }
1720
- this._pendingEndWrites = [];
1721
- this._endPending = false;
1722
- this._writable.end();
1723
- }, 0);
1828
+ this._scheduleEnd();
1724
1829
  return this;
1725
1830
  }
1726
1831
  /**
@@ -1730,31 +1835,9 @@ class Transform extends event_emitter_1.EventEmitter {
1730
1835
  return this._readable.read(size);
1731
1836
  }
1732
1837
  /**
1733
- * Pipe to another stream (writable, transform, or duplex)
1838
+ * Pipe readable side to destination
1734
1839
  */
1735
1840
  pipe(destination) {
1736
- // Mark as having consumer to prevent new auto-consume from starting
1737
- this._hasDataConsumer = true;
1738
- // Get the writable target - handle both Transform (with internal _writable) and plain Writable
1739
- const dest = destination;
1740
- const target = dest?._writable ?? dest;
1741
- // Register destination for forwarding
1742
- this._pipeDestinations.push(target);
1743
- // If auto-consume is running or has run, we need to handle buffered data ourselves
1744
- if (this._readableConsuming) {
1745
- // Forward any buffered data from auto-consume to the destination
1746
- for (let i = 0; i < this._autoConsumedBuffer.length; i++) {
1747
- target.write(this._autoConsumedBuffer[i]);
1748
- }
1749
- // If auto-consume has ended, end the destination too
1750
- if (this._autoConsumeEnded) {
1751
- target.end();
1752
- }
1753
- // Don't call _readable.pipe() - auto-consume already consumed _readable
1754
- // Future data will be forwarded via the 'data' event listener below
1755
- return destination;
1756
- }
1757
- // No auto-consume running - use normal pipe through _readable
1758
1841
  return this._readable.pipe(destination);
1759
1842
  }
1760
1843
  /**
@@ -1792,6 +1875,10 @@ class Transform extends event_emitter_1.EventEmitter {
1792
1875
  return;
1793
1876
  }
1794
1877
  this._destroyed = true;
1878
+ if (this._sideForwardingCleanup) {
1879
+ this._sideForwardingCleanup();
1880
+ this._sideForwardingCleanup = null;
1881
+ }
1795
1882
  this._readable.destroy(error);
1796
1883
  this._writable.destroy(error);
1797
1884
  queueMicrotask(() => this.emit("close"));
@@ -1800,7 +1887,44 @@ class Transform extends event_emitter_1.EventEmitter {
1800
1887
  * Get the underlying Web TransformStream
1801
1888
  */
1802
1889
  get webStream() {
1803
- return this._stream;
1890
+ if (this._webStream) {
1891
+ return this._webStream;
1892
+ }
1893
+ // Web Streams interop layer.
1894
+ const iterator = this[Symbol.asyncIterator]();
1895
+ const readable = new ReadableStream({
1896
+ pull: async (controller) => {
1897
+ const { done, value } = await iterator.next();
1898
+ if (done) {
1899
+ controller.close();
1900
+ return;
1901
+ }
1902
+ controller.enqueue(value);
1903
+ },
1904
+ cancel: reason => {
1905
+ this.destroy(reason instanceof Error ? reason : new Error(String(reason)));
1906
+ }
1907
+ });
1908
+ const writable = new WritableStream({
1909
+ write: chunk => new Promise((resolve, reject) => {
1910
+ this.write(chunk, err => {
1911
+ if (err) {
1912
+ reject(err);
1913
+ }
1914
+ else {
1915
+ resolve();
1916
+ }
1917
+ });
1918
+ }),
1919
+ close: () => new Promise(resolve => {
1920
+ this.end(() => resolve());
1921
+ }),
1922
+ abort: reason => {
1923
+ this.destroy(reason instanceof Error ? reason : new Error(String(reason)));
1924
+ }
1925
+ });
1926
+ this._webStream = { readable, writable };
1927
+ return this._webStream;
1804
1928
  }
1805
1929
  get readable() {
1806
1930
  return this._readable.readable;
@@ -1842,19 +1966,6 @@ class Transform extends event_emitter_1.EventEmitter {
1842
1966
  * Async iterator support
1843
1967
  */
1844
1968
  async *[Symbol.asyncIterator]() {
1845
- // If auto-consume is running, wait for it to finish and use its buffer
1846
- if (this._autoConsumePromise) {
1847
- await this._autoConsumePromise;
1848
- // Yield all buffered data
1849
- while (this._autoConsumedBufferIndex < this._autoConsumedBuffer.length) {
1850
- yield this._autoConsumedBuffer[this._autoConsumedBufferIndex++];
1851
- }
1852
- // Reset when drained to avoid prefix growth
1853
- this._autoConsumedBuffer.length = 0;
1854
- this._autoConsumedBufferIndex = 0;
1855
- return;
1856
- }
1857
- // Otherwise delegate to readable's iterator
1858
1969
  yield* this._readable[Symbol.asyncIterator]();
1859
1970
  }
1860
1971
  // =========================================================================
@@ -1865,23 +1976,18 @@ class Transform extends event_emitter_1.EventEmitter {
1865
1976
  */
1866
1977
  static fromWeb(webStream, options) {
1867
1978
  const transform = new Transform(options);
1868
- // Connect the web stream - set the internal _stream property
1869
- transform._stream = webStream;
1979
+ transform._webStream = webStream;
1870
1980
  // Replace internal streams with the ones from the web stream
1871
1981
  const newReadable = Readable.fromWeb(webStream.readable, { objectMode: options?.objectMode });
1872
1982
  const newWritable = Writable.fromWeb(webStream.writable, { objectMode: options?.objectMode });
1873
- // Remove old event listeners before replacing
1874
- transform._readable.removeAllListeners();
1875
- transform._writable.removeAllListeners();
1983
+ if (transform._sideForwardingCleanup) {
1984
+ transform._sideForwardingCleanup();
1985
+ transform._sideForwardingCleanup = null;
1986
+ }
1876
1987
  transform._readable = newReadable;
1877
1988
  transform._writable = newWritable;
1878
- // Re-connect event forwarding
1879
- newReadable.on("data", (data) => transform.emit("data", data));
1880
- newReadable.on("end", () => transform.emit("end"));
1881
- newReadable.on("error", (err) => transform.emit("error", err));
1882
- newWritable.on("finish", () => transform.emit("finish"));
1883
- newWritable.on("drain", () => transform.emit("drain"));
1884
- newWritable.on("error", (err) => transform.emit("error", err));
1989
+ // Re-connect event forwarding (data forwarding remains lazy via Transform.on)
1990
+ transform._setupSideForwarding();
1885
1991
  return transform;
1886
1992
  }
1887
1993
  /**
@@ -1938,7 +2044,13 @@ class Duplex extends event_emitter_1.EventEmitter {
1938
2044
  callback();
1939
2045
  }
1940
2046
  });
1941
- readable.on("error", err => duplex.emit("error", err));
2047
+ const onError = (err) => {
2048
+ duplex.emit("error", err);
2049
+ };
2050
+ const cleanupError = addEmitterListener(readable, "error", onError);
2051
+ addEmitterListener(readable, "end", cleanupError, { once: true });
2052
+ addEmitterListener(readable, "close", cleanupError, { once: true });
2053
+ addEmitterListener(sink, "finish", cleanupError, { once: true });
1942
2054
  readable.pipe(sink);
1943
2055
  };
1944
2056
  // If it has readable and/or writable properties
@@ -1946,21 +2058,25 @@ class Duplex extends event_emitter_1.EventEmitter {
1946
2058
  source !== null &&
1947
2059
  "readable" in source &&
1948
2060
  "writable" in source) {
1949
- const duplex = new Duplex();
1950
2061
  const pair = source;
1951
- if (pair.readable) {
1952
- forwardReadableToDuplex(pair.readable, duplex);
1953
- }
1954
- if (pair.writable) {
1955
- return new Duplex({
1956
- objectMode: duplex.writableObjectMode,
1957
- write(chunk, encoding, callback) {
2062
+ // Create one duplex that can bridge both sides.
2063
+ // (Previous behavior returned a new writable-only Duplex and dropped the readable side.)
2064
+ const duplex = new Duplex({
2065
+ readableObjectMode: pair.readable?.readableObjectMode,
2066
+ writableObjectMode: pair.writable?.writableObjectMode,
2067
+ write: pair.writable
2068
+ ? (chunk, encoding, callback) => {
1958
2069
  pair.writable.write(chunk, encoding, callback);
1959
- },
1960
- final(callback) {
2070
+ }
2071
+ : undefined,
2072
+ final: pair.writable
2073
+ ? callback => {
1961
2074
  pair.writable.end(callback);
1962
2075
  }
1963
- });
2076
+ : undefined
2077
+ });
2078
+ if (pair.readable) {
2079
+ forwardReadableToDuplex(pair.readable, duplex);
1964
2080
  }
1965
2081
  return duplex;
1966
2082
  }
@@ -1998,9 +2114,22 @@ class Duplex extends event_emitter_1.EventEmitter {
1998
2114
  */
1999
2115
  static fromWeb(pair, options) {
2000
2116
  const duplex = new Duplex(options);
2001
- // Replace internal streams
2002
- duplex._readable = new Readable({ stream: pair.readable });
2003
- duplex._writable = new Writable({ stream: pair.writable });
2117
+ const newReadable = new Readable({
2118
+ stream: pair.readable,
2119
+ objectMode: duplex.readableObjectMode
2120
+ });
2121
+ const newWritable = new Writable({
2122
+ stream: pair.writable,
2123
+ objectMode: duplex.writableObjectMode
2124
+ });
2125
+ if (duplex._sideForwardingCleanup) {
2126
+ duplex._sideForwardingCleanup();
2127
+ duplex._sideForwardingCleanup = null;
2128
+ }
2129
+ duplex._readable = newReadable;
2130
+ duplex._writable = newWritable;
2131
+ // Re-wire event forwarding (data forwarding remains lazy via Duplex.on)
2132
+ duplex._setupSideForwarding();
2004
2133
  return duplex;
2005
2134
  }
2006
2135
  /**
@@ -2016,6 +2145,7 @@ class Duplex extends event_emitter_1.EventEmitter {
2016
2145
  super();
2017
2146
  // Track if we've already set up data forwarding
2018
2147
  this._dataForwardingSetup = false;
2148
+ this._sideForwardingCleanup = null;
2019
2149
  this.allowHalfOpen = options?.allowHalfOpen ?? true;
2020
2150
  // Support shorthand objectMode option
2021
2151
  const objectMode = options?.objectMode ?? false;
@@ -2032,23 +2162,31 @@ class Duplex extends event_emitter_1.EventEmitter {
2032
2162
  write: options?.write?.bind(this),
2033
2163
  final: options?.final?.bind(this)
2034
2164
  });
2165
+ this._setupSideForwarding();
2166
+ }
2167
+ _setupSideForwarding() {
2168
+ if (this._sideForwardingCleanup) {
2169
+ this._sideForwardingCleanup();
2170
+ this._sideForwardingCleanup = null;
2171
+ }
2172
+ const registry = createListenerRegistry();
2035
2173
  // Forward non-data events (data forwarding is lazy to avoid premature flowing)
2036
- this._readable.on("end", () => {
2174
+ registry.once(this._readable, "end", () => {
2037
2175
  this.emit("end");
2038
- // If not allowHalfOpen, end the writable side too
2039
2176
  if (!this.allowHalfOpen) {
2040
2177
  this._writable.end();
2041
2178
  }
2042
2179
  });
2043
- this._readable.on("error", err => this.emit("error", err));
2044
- this._writable.on("finish", () => this.emit("finish"));
2045
- this._writable.on("drain", () => this.emit("drain"));
2046
- this._writable.on("close", () => {
2047
- // If not allowHalfOpen, destroy the readable side too
2180
+ registry.add(this._readable, "error", err => this.emit("error", err));
2181
+ registry.add(this._writable, "error", err => this.emit("error", err));
2182
+ registry.once(this._writable, "finish", () => this.emit("finish"));
2183
+ registry.add(this._writable, "drain", () => this.emit("drain"));
2184
+ registry.once(this._writable, "close", () => {
2048
2185
  if (!this.allowHalfOpen && !this._readable.destroyed) {
2049
2186
  this._readable.destroy();
2050
2187
  }
2051
2188
  });
2189
+ this._sideForwardingCleanup = () => registry.cleanup();
2052
2190
  }
2053
2191
  /**
2054
2192
  * Override on() to set up data forwarding lazily
@@ -2167,6 +2305,10 @@ class Duplex extends event_emitter_1.EventEmitter {
2167
2305
  * Destroy both sides
2168
2306
  */
2169
2307
  destroy(error) {
2308
+ if (this._sideForwardingCleanup) {
2309
+ this._sideForwardingCleanup();
2310
+ this._sideForwardingCleanup = null;
2311
+ }
2170
2312
  this._readable.destroy(error);
2171
2313
  this._writable.destroy(error);
2172
2314
  return this;
@@ -2336,36 +2478,16 @@ exports.BufferedStream = BufferedStream;
2336
2478
  * Create a readable stream with custom read implementation
2337
2479
  */
2338
2480
  function createReadable(options) {
2339
- const readable = new Readable(options);
2340
- // Override read behavior if provided
2341
- if (options?.read) {
2342
- const originalRead = readable.read.bind(readable);
2343
- readable.read = function (size) {
2344
- options.read(size ?? 16384);
2345
- return originalRead(size);
2346
- };
2347
- }
2348
- return readable;
2481
+ // Readable already supports Node-style `read()` via the constructor option.
2482
+ // Keep this helper minimal to avoid accidental double-read behavior.
2483
+ return new Readable(options);
2349
2484
  }
2350
2485
  /**
2351
2486
  * Create a readable stream from an async iterable
2352
2487
  */
2353
2488
  function createReadableFromAsyncIterable(iterable, options) {
2354
2489
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
2355
- (async () => {
2356
- try {
2357
- for await (const chunk of iterable) {
2358
- if (!readable.push(chunk)) {
2359
- // Backpressure - wait a bit
2360
- await new Promise(resolve => setTimeout(resolve, 0));
2361
- }
2362
- }
2363
- readable.push(null);
2364
- }
2365
- catch (err) {
2366
- readable.destroy(err);
2367
- }
2368
- })();
2490
+ pumpAsyncIterableToReadable(readable, iterable);
2369
2491
  return readable;
2370
2492
  }
2371
2493
  /**
@@ -2394,38 +2516,8 @@ function createReadableFromArray(data, options) {
2394
2516
  * Create a writable stream with custom write implementation
2395
2517
  */
2396
2518
  function createWritable(options) {
2397
- // Create a custom WritableStream with user's handlers
2398
- const stream = new WritableStream({
2399
- write: async (chunk) => {
2400
- if (options?.write) {
2401
- return new Promise((resolve, reject) => {
2402
- options.write(chunk, "utf8", err => {
2403
- if (err) {
2404
- reject(err);
2405
- }
2406
- else {
2407
- resolve();
2408
- }
2409
- });
2410
- });
2411
- }
2412
- },
2413
- close: async () => {
2414
- if (options?.final) {
2415
- return new Promise((resolve, reject) => {
2416
- options.final(err => {
2417
- if (err) {
2418
- reject(err);
2419
- }
2420
- else {
2421
- resolve();
2422
- }
2423
- });
2424
- });
2425
- }
2426
- }
2427
- });
2428
- return new Writable({ ...options, stream });
2519
+ // Writable already supports Node-style `write()` / `final()` via the constructor.
2520
+ return new Writable(options);
2429
2521
  }
2430
2522
  /**
2431
2523
  * Create a transform stream from a transform function
@@ -2560,11 +2652,17 @@ function pipeline(...args) {
2560
2652
  const transforms = normalized.slice(1, -1);
2561
2653
  let completed = false;
2562
2654
  const allStreams = [source, ...transforms, destination];
2563
- const cleanup = (error) => {
2655
+ const registry = createListenerRegistry();
2656
+ let onAbort;
2657
+ const cleanupWithSignal = (error) => {
2564
2658
  if (completed) {
2565
2659
  return;
2566
2660
  }
2567
2661
  completed = true;
2662
+ registry.cleanup();
2663
+ if (onAbort && options.signal) {
2664
+ options.signal.removeEventListener("abort", onAbort);
2665
+ }
2568
2666
  // Destroy all streams on error
2569
2667
  if (error) {
2570
2668
  for (const stream of allStreams) {
@@ -2581,12 +2679,11 @@ function pipeline(...args) {
2581
2679
  // Handle abort signal
2582
2680
  if (options.signal) {
2583
2681
  if (options.signal.aborted) {
2584
- cleanup(new Error("Pipeline aborted"));
2682
+ cleanupWithSignal(new Error("Pipeline aborted"));
2585
2683
  return;
2586
2684
  }
2587
- options.signal.addEventListener("abort", () => {
2588
- cleanup(new Error("Pipeline aborted"));
2589
- });
2685
+ onAbort = () => cleanupWithSignal(new Error("Pipeline aborted"));
2686
+ options.signal.addEventListener("abort", onAbort);
2590
2687
  }
2591
2688
  // Chain the streams
2592
2689
  let current = source;
@@ -2600,13 +2697,35 @@ function pipeline(...args) {
2600
2697
  }
2601
2698
  else {
2602
2699
  // Don't end destination
2603
- current.on("data", chunk => destination.write(chunk));
2700
+ let paused = false;
2701
+ let waitingForDrain = false;
2702
+ const onDrain = () => {
2703
+ waitingForDrain = false;
2704
+ if (paused && typeof current.resume === "function") {
2705
+ paused = false;
2706
+ current.resume();
2707
+ }
2708
+ };
2709
+ const onData = (chunk) => {
2710
+ const ok = destination.write(chunk);
2711
+ if (!ok && !waitingForDrain) {
2712
+ waitingForDrain = true;
2713
+ if (!paused && typeof current.pause === "function") {
2714
+ paused = true;
2715
+ current.pause();
2716
+ }
2717
+ registry.once(destination, "drain", onDrain);
2718
+ }
2719
+ };
2720
+ const onEnd = () => cleanupWithSignal();
2721
+ registry.add(current, "data", onData);
2722
+ registry.once(current, "end", onEnd);
2604
2723
  }
2605
2724
  // Handle completion
2606
- destination.on("finish", () => cleanup());
2725
+ registry.once(destination, "finish", () => cleanupWithSignal());
2607
2726
  // Handle errors on all streams
2608
2727
  for (const stream of allStreams) {
2609
- stream.on("error", (err) => cleanup(err));
2728
+ registry.once(stream, "error", (err) => cleanupWithSignal(err));
2610
2729
  }
2611
2730
  });
2612
2731
  // If callback provided, use it
@@ -2646,11 +2765,20 @@ function finished(stream, optionsOrCallback, callback) {
2646
2765
  const promise = new Promise((resolve, reject) => {
2647
2766
  const normalizedStream = toBrowserPipelineStream(stream);
2648
2767
  let resolved = false;
2768
+ const registry = createListenerRegistry();
2769
+ let onAbort;
2770
+ const cleanup = () => {
2771
+ registry.cleanup();
2772
+ if (onAbort && options.signal) {
2773
+ options.signal.removeEventListener("abort", onAbort);
2774
+ }
2775
+ };
2649
2776
  const done = (err) => {
2650
2777
  if (resolved) {
2651
2778
  return;
2652
2779
  }
2653
2780
  resolved = true;
2781
+ cleanup();
2654
2782
  if (err && !options.error) {
2655
2783
  reject(err);
2656
2784
  }
@@ -2664,9 +2792,8 @@ function finished(stream, optionsOrCallback, callback) {
2664
2792
  done(new Error("Aborted"));
2665
2793
  return;
2666
2794
  }
2667
- options.signal.addEventListener("abort", () => {
2668
- done(new Error("Aborted"));
2669
- });
2795
+ onAbort = () => done(new Error("Aborted"));
2796
+ options.signal.addEventListener("abort", onAbort);
2670
2797
  }
2671
2798
  const checkReadable = options.readable !== false;
2672
2799
  const checkWritable = options.writable !== false;
@@ -2681,13 +2808,13 @@ function finished(stream, optionsOrCallback, callback) {
2681
2808
  }
2682
2809
  // Listen for events
2683
2810
  if (checkWritable) {
2684
- normalizedStream.on("finish", () => done());
2811
+ registry.once(normalizedStream, "finish", () => done());
2685
2812
  }
2686
2813
  if (checkReadable) {
2687
- normalizedStream.on("end", () => done());
2814
+ registry.once(normalizedStream, "end", () => done());
2688
2815
  }
2689
- normalizedStream.on("error", (err) => done(err));
2690
- normalizedStream.on("close", () => done());
2816
+ registry.once(normalizedStream, "error", (err) => done(err));
2817
+ registry.once(normalizedStream, "close", () => done());
2691
2818
  });
2692
2819
  // If callback provided, use it
2693
2820
  if (cb) {
@@ -2709,38 +2836,8 @@ async function streamToPromise(stream) {
2709
2836
  * (Browser equivalent of Node.js streamToBuffer)
2710
2837
  */
2711
2838
  async function streamToUint8Array(stream) {
2712
- let iterable;
2713
- if (isReadableStream(stream)) {
2714
- iterable = Readable.fromWeb(stream);
2715
- }
2716
- else if (isAsyncIterable(stream)) {
2717
- iterable = stream;
2718
- }
2719
- else {
2720
- throw new Error("streamToUint8Array: unsupported stream type");
2721
- }
2722
- const chunks = [];
2723
- let totalLength = 0;
2724
- for await (const chunk of iterable) {
2725
- chunks.push(chunk);
2726
- totalLength += chunk.length;
2727
- }
2728
- // Fast paths
2729
- const len = chunks.length;
2730
- if (len === 0) {
2731
- return new Uint8Array(0);
2732
- }
2733
- if (len === 1) {
2734
- return chunks[0];
2735
- }
2736
- // Use precalculated total length
2737
- const result = new Uint8Array(totalLength);
2738
- let offset = 0;
2739
- for (let i = 0; i < len; i++) {
2740
- result.set(chunks[i], offset);
2741
- offset += chunks[i].length;
2742
- }
2743
- return result;
2839
+ const { chunks, totalLength } = await collectStreamChunks(stream);
2840
+ return concatWithLength(chunks, totalLength);
2744
2841
  }
2745
2842
  /**
2746
2843
  * Alias for streamToUint8Array (Node.js compatibility)
@@ -2750,8 +2847,10 @@ exports.streamToBuffer = streamToUint8Array;
2750
2847
  * Collect all data from a readable stream into a string
2751
2848
  */
2752
2849
  async function streamToString(stream, encoding) {
2753
- const buffer = await streamToUint8Array(stream);
2754
- return (0, shared_1.getTextDecoder)(encoding).decode(buffer);
2850
+ const { chunks, totalLength } = await collectStreamChunks(stream);
2851
+ const combined = concatWithLength(chunks, totalLength);
2852
+ const decoder = encoding ? (0, shared_1.getTextDecoder)(encoding) : shared_1.textDecoder;
2853
+ return decoder.decode(combined);
2755
2854
  }
2756
2855
  /**
2757
2856
  * Drain a stream (consume all data without processing)
@@ -2839,14 +2938,19 @@ function addAbortSignal(signal, stream) {
2839
2938
  stream.destroy(new Error("Aborted"));
2840
2939
  return stream;
2841
2940
  }
2941
+ const cleanup = () => {
2942
+ signal.removeEventListener("abort", onAbort);
2943
+ removeEmitterListener(stream, "close", onClose);
2944
+ };
2842
2945
  const onAbort = () => {
2946
+ cleanup();
2843
2947
  stream.destroy(new Error("Aborted"));
2844
2948
  };
2845
- signal.addEventListener("abort", onAbort, { once: true });
2846
- // Clean up when stream is destroyed
2847
- stream.on("close", () => {
2848
- signal.removeEventListener("abort", onAbort);
2849
- });
2949
+ const onClose = () => {
2950
+ cleanup();
2951
+ };
2952
+ signal.addEventListener("abort", onAbort);
2953
+ addEmitterListener(stream, "close", onClose, { once: true });
2850
2954
  return stream;
2851
2955
  }
2852
2956
  /**
@@ -2855,60 +2959,68 @@ function addAbortSignal(signal, stream) {
2855
2959
  function createDuplex(options) {
2856
2960
  const readableObjectMode = options?.readableObjectMode ?? options?.objectMode;
2857
2961
  const writableObjectMode = options?.writableObjectMode ?? options?.objectMode;
2962
+ const underlyingWritable = options?.writable;
2858
2963
  const duplex = new Duplex({
2859
2964
  allowHalfOpen: options?.allowHalfOpen,
2860
2965
  readableHighWaterMark: options?.readableHighWaterMark,
2861
2966
  writableHighWaterMark: options?.writableHighWaterMark,
2862
2967
  readableObjectMode,
2863
- writableObjectMode
2968
+ writableObjectMode,
2969
+ read: options?.read,
2970
+ write: options?.write ??
2971
+ (underlyingWritable
2972
+ ? (chunk, encoding, callback) => {
2973
+ if (typeof underlyingWritable.write === "function") {
2974
+ underlyingWritable.write(chunk, encoding, callback);
2975
+ return;
2976
+ }
2977
+ // Best-effort sync sink
2978
+ try {
2979
+ underlyingWritable.write?.(chunk);
2980
+ callback(null);
2981
+ }
2982
+ catch (err) {
2983
+ callback(err);
2984
+ }
2985
+ }
2986
+ : undefined),
2987
+ final: options?.final ??
2988
+ (underlyingWritable
2989
+ ? (callback) => {
2990
+ if (typeof underlyingWritable.end === "function") {
2991
+ underlyingWritable.end((err) => callback(err ?? null));
2992
+ }
2993
+ else {
2994
+ underlyingWritable.end?.();
2995
+ callback(null);
2996
+ }
2997
+ }
2998
+ : undefined)
2864
2999
  });
2865
- // If custom readable/writable provided, pipe them
3000
+ // If an underlying readable is provided, forward it into the duplex readable side.
2866
3001
  if (options?.readable) {
2867
3002
  const readable = options.readable;
2868
- readable.on?.("data", (chunk) => duplex.push(chunk));
2869
- readable.on?.("end", () => duplex.push(null));
2870
- readable.on?.("error", (err) => duplex.destroy(err));
2871
- }
2872
- if (options?.writable) {
2873
- const writable = options.writable;
2874
- duplex.on("data", (chunk) => writable.write?.(chunk));
2875
- duplex.on("finish", () => writable.end?.());
2876
- }
2877
- // If custom read/write/final provided, override methods
2878
- if (options?.write) {
2879
- const _originalWrite = duplex.write.bind(duplex); // Keep bound reference for potential future use
2880
- duplex.write = function (chunk, encodingOrCallback, callback) {
2881
- const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : "utf8";
2882
- const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : (callback ?? (() => { }));
2883
- options.write.call(duplex, chunk, encoding, cb);
2884
- return true;
2885
- };
2886
- }
2887
- if (options?.final) {
2888
- const originalEnd = duplex.end.bind(duplex);
2889
- duplex.end = function (chunkOrCallback, encodingOrCallback, callback) {
2890
- const cb = typeof chunkOrCallback === "function"
2891
- ? chunkOrCallback
2892
- : typeof encodingOrCallback === "function"
2893
- ? encodingOrCallback
2894
- : (callback ?? (() => { }));
2895
- if (chunkOrCallback !== undefined && typeof chunkOrCallback !== "function") {
2896
- duplex.write(chunkOrCallback);
2897
- }
2898
- // Call custom final handler
2899
- options.final.call(duplex, (err) => {
2900
- if (err) {
2901
- duplex.emit("error", err);
2902
- }
2903
- else {
2904
- duplex.emit("finish");
2905
- }
2906
- // Call original end to properly close writable side
2907
- originalEnd();
2908
- cb();
2909
- });
2910
- return duplex;
2911
- };
3003
+ const sink = new Writable({
3004
+ objectMode: duplex.readableObjectMode,
3005
+ write(chunk, _encoding, callback) {
3006
+ duplex.push(chunk);
3007
+ callback(null);
3008
+ },
3009
+ final(callback) {
3010
+ duplex.push(null);
3011
+ callback(null);
3012
+ }
3013
+ });
3014
+ if (typeof readable?.on === "function") {
3015
+ const onError = (err) => {
3016
+ duplex.destroy(err);
3017
+ };
3018
+ const cleanupError = addEmitterListener(readable, "error", onError);
3019
+ addEmitterListener(readable, "end", cleanupError, { once: true });
3020
+ addEmitterListener(readable, "close", cleanupError, { once: true });
3021
+ addEmitterListener(sink, "finish", cleanupError, { once: true });
3022
+ }
3023
+ readable.pipe?.(sink);
2912
3024
  }
2913
3025
  if (options?.destroy) {
2914
3026
  const originalDestroy = duplex.destroy.bind(duplex);
@@ -2932,20 +3044,7 @@ function createDuplex(options) {
2932
3044
  */
2933
3045
  function createReadableFromGenerator(generator, options) {
2934
3046
  const readable = new Readable({ ...options, objectMode: options?.objectMode ?? true });
2935
- (async () => {
2936
- try {
2937
- for await (const chunk of generator()) {
2938
- if (!readable.push(chunk)) {
2939
- // Backpressure
2940
- await new Promise(resolve => setTimeout(resolve, 0));
2941
- }
2942
- }
2943
- readable.push(null);
2944
- }
2945
- catch (err) {
2946
- readable.destroy(err);
2947
- }
2948
- })();
3047
+ pumpAsyncIterableToReadable(readable, generator());
2949
3048
  return readable;
2950
3049
  }
2951
3050
  /**
@@ -2975,8 +3074,8 @@ function compose(...transforms) {
2975
3074
  transform: chunk => chunk
2976
3075
  });
2977
3076
  }
2978
- const isNativeTransform = (stream) => stream instanceof Transform;
2979
- if (len === 1 && isNativeTransform(transforms[0])) {
3077
+ // Preserve identity: compose(single) returns the same transform.
3078
+ if (len === 1) {
2980
3079
  return transforms[0];
2981
3080
  }
2982
3081
  // Chain the transforms: first → second → ... → last
@@ -2987,19 +3086,34 @@ function compose(...transforms) {
2987
3086
  transforms[i].pipe(transforms[i + 1]);
2988
3087
  }
2989
3088
  class ComposedTransform extends Transform {
2990
- constructor() {
2991
- super(...arguments);
3089
+ constructor(options) {
3090
+ super(options);
2992
3091
  this._dataForwarding = false;
2993
3092
  this._endForwarding = false;
3093
+ this._dataForwardCleanup = null;
3094
+ this._endForwardCleanup = null;
3095
+ this._errorForwardCleanup = [];
3096
+ for (const t of transforms) {
3097
+ const onError = (err) => {
3098
+ this.emit("error", err);
3099
+ };
3100
+ this._errorForwardCleanup.push(addEmitterListener(t, "error", onError));
3101
+ }
2994
3102
  }
2995
3103
  on(event, listener) {
2996
3104
  if (event === "data" && !this._dataForwarding) {
2997
3105
  this._dataForwarding = true;
2998
- last.on("data", (chunk) => this.emit("data", chunk));
3106
+ const onData = (chunk) => {
3107
+ this.emit("data", chunk);
3108
+ };
3109
+ this._dataForwardCleanup = addEmitterListener(last, "data", onData);
2999
3110
  }
3000
3111
  if (event === "end" && !this._endForwarding) {
3001
3112
  this._endForwarding = true;
3002
- last.on("end", () => this.emit("end"));
3113
+ const onEnd = () => {
3114
+ this.emit("end");
3115
+ };
3116
+ this._endForwardCleanup = addEmitterListener(last, "end", onEnd, { once: true });
3003
3117
  }
3004
3118
  return super.on(event, listener);
3005
3119
  }
@@ -3025,6 +3139,18 @@ function compose(...transforms) {
3025
3139
  return last.pipe(destination);
3026
3140
  }
3027
3141
  destroy(error) {
3142
+ if (this._dataForwardCleanup) {
3143
+ this._dataForwardCleanup();
3144
+ this._dataForwardCleanup = null;
3145
+ }
3146
+ if (this._endForwardCleanup) {
3147
+ this._endForwardCleanup();
3148
+ this._endForwardCleanup = null;
3149
+ }
3150
+ for (let i = this._errorForwardCleanup.length - 1; i >= 0; i--) {
3151
+ this._errorForwardCleanup[i]();
3152
+ }
3153
+ this._errorForwardCleanup.length = 0;
3028
3154
  for (const t of transforms) {
3029
3155
  t.destroy(error);
3030
3156
  }
@@ -3048,12 +3174,6 @@ function compose(...transforms) {
3048
3174
  objectMode: first?.objectMode ?? true,
3049
3175
  transform: chunk => chunk
3050
3176
  });
3051
- // Forward errors from any transform
3052
- for (const t of transforms) {
3053
- t.on("error", (err) => {
3054
- composed.emit("error", err);
3055
- });
3056
- }
3057
3177
  // Reflect underlying readability/writability like the previous duck-typed wrapper
3058
3178
  Object.defineProperty(composed, "readable", {
3059
3179
  get: () => last.readable