documonster 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +1 -1
  2. package/README_zh.md +1 -1
  3. package/THIRD_PARTY_NOTICES.md +34 -2
  4. package/dist/esm/modules/archive/core/byte-queue.js +2 -1
  5. package/dist/esm/modules/archive/unzip/stream.browser.js +6 -0
  6. package/dist/esm/modules/csv/parse/async.js +2 -1
  7. package/dist/esm/modules/excel/bridge/csv-bridge.js +1 -1
  8. package/dist/esm/modules/excel/core/xlsx-io.browser.js +5 -9
  9. package/dist/esm/modules/excel/core/xlsx-io.js +1 -3
  10. package/dist/esm/modules/excel/core/xlsx-stream.js +3 -1
  11. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +10 -10
  12. package/dist/esm/modules/excel/stream/workbook-reader.js +30 -6
  13. package/dist/esm/modules/stream/browser/collector.js +3 -5
  14. package/dist/esm/modules/stream/browser/compose.js +13 -228
  15. package/dist/esm/modules/stream/browser/duplex-pair.js +71 -0
  16. package/dist/esm/modules/stream/browser/factories.js +35 -217
  17. package/dist/esm/modules/stream/browser/pipeline.js +43 -478
  18. package/dist/esm/modules/stream/browser/utils.js +21 -247
  19. package/dist/esm/modules/stream/browser/web.js +25 -0
  20. package/dist/esm/modules/stream/buffered-stream.js +8 -6
  21. package/dist/esm/modules/stream/core/factories.js +166 -0
  22. package/dist/esm/modules/stream/core/pipeline-api.js +32 -0
  23. package/dist/esm/modules/stream/core/sink-adapter.js +94 -24
  24. package/dist/esm/modules/stream/core/stream-helpers.js +160 -0
  25. package/dist/esm/modules/stream/core/utils.js +0 -23
  26. package/dist/esm/modules/stream/core/web-conversions.js +31 -0
  27. package/dist/esm/modules/stream/core/web-pipeline.js +90 -0
  28. package/dist/esm/modules/stream/index.base.js +2 -2
  29. package/dist/esm/modules/stream/index.browser.js +13 -19
  30. package/dist/esm/modules/stream/index.js +6 -10
  31. package/dist/esm/modules/stream/kernel/classes.js +618 -0
  32. package/dist/esm/modules/stream/kernel/compose-impl.js +225 -0
  33. package/dist/esm/modules/stream/kernel/destroy.js +255 -0
  34. package/dist/esm/modules/stream/kernel/duplexify.js +355 -0
  35. package/dist/esm/modules/stream/kernel/end-of-stream.js +212 -0
  36. package/dist/esm/modules/stream/kernel/errors.js +215 -0
  37. package/dist/esm/modules/stream/kernel/from.js +202 -0
  38. package/dist/esm/modules/stream/kernel/legacy.js +35 -0
  39. package/dist/esm/modules/stream/kernel/operators.js +285 -0
  40. package/dist/esm/modules/stream/kernel/pipeline-impl.js +561 -0
  41. package/dist/esm/modules/stream/kernel/readable-impl.js +963 -0
  42. package/dist/esm/modules/stream/kernel/state.js +253 -0
  43. package/dist/esm/modules/stream/kernel/stream-utils.js +212 -0
  44. package/dist/esm/modules/stream/kernel/web-adapters.js +584 -0
  45. package/dist/esm/modules/stream/kernel/writable.js +651 -0
  46. package/dist/esm/modules/stream/node/collector.js +7 -1
  47. package/dist/esm/modules/stream/node/compose.js +21 -206
  48. package/dist/esm/modules/stream/node/factories.js +34 -238
  49. package/dist/esm/modules/stream/node/pipeline.js +40 -94
  50. package/dist/esm/modules/stream/node/utils.js +15 -239
  51. package/dist/esm/modules/stream/node/web.js +27 -0
  52. package/dist/esm/modules/stream/pull-stream.js +15 -9
  53. package/dist/esm/modules/word/writer/streaming-writer.js +3 -0
  54. package/dist/iife/THIRD_PARTY_NOTICES.md +34 -2
  55. package/dist/iife/documonster.archive.iife.min.js +4 -4
  56. package/dist/iife/documonster.csv.iife.min.js +19 -19
  57. package/dist/iife/documonster.draw.iife.min.js +1 -1
  58. package/dist/iife/documonster.excel.iife.min.js +50 -50
  59. package/dist/iife/documonster.formula.iife.min.js +1 -1
  60. package/dist/iife/documonster.markdown.iife.min.js +1 -1
  61. package/dist/iife/documonster.mermaid.iife.min.js +1 -1
  62. package/dist/iife/documonster.pdf.iife.min.js +1 -1
  63. package/dist/iife/documonster.stream.iife.min.js +2 -2
  64. package/dist/iife/documonster.word.iife.min.js +60 -60
  65. package/dist/iife/documonster.xml.iife.min.js +1 -1
  66. package/dist/types/modules/excel/core/xlsx-io.browser.d.ts +5 -9
  67. package/dist/types/modules/excel/core/xlsx-io.d.ts +1 -3
  68. package/dist/types/modules/excel/core/xlsx-stream.d.ts +3 -2
  69. package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +2 -1
  70. package/dist/types/modules/excel/stream/workbook-reader.d.ts +2 -1
  71. package/dist/types/modules/stream/browser/collector.d.ts +1 -1
  72. package/dist/types/modules/stream/browser/compose.d.ts +14 -15
  73. package/dist/types/modules/stream/browser/duplex-pair.d.ts +10 -0
  74. package/dist/types/modules/stream/browser/factories.d.ts +22 -79
  75. package/dist/types/modules/stream/browser/pipeline.d.ts +8 -40
  76. package/dist/types/modules/stream/browser/utils.d.ts +6 -47
  77. package/dist/types/modules/stream/browser/web.d.ts +18 -0
  78. package/dist/types/modules/stream/core/add-abort-signal.d.ts +2 -1
  79. package/dist/types/modules/stream/core/consumers.d.ts +1 -1
  80. package/dist/types/modules/stream/core/event-utils.d.ts +2 -1
  81. package/dist/types/modules/stream/core/factories.d.ts +129 -0
  82. package/dist/types/modules/stream/core/pipeline-api.d.ts +10 -0
  83. package/dist/types/modules/stream/core/sink-adapter.d.ts +18 -1
  84. package/dist/types/modules/stream/core/stream-helpers.d.ts +76 -0
  85. package/dist/types/modules/stream/core/utils.d.ts +0 -9
  86. package/dist/types/modules/stream/core/web-conversions.d.ts +68 -0
  87. package/dist/types/modules/stream/core/web-pipeline.d.ts +27 -0
  88. package/dist/types/modules/stream/index.base.d.ts +6 -4
  89. package/dist/types/modules/stream/index.browser.d.ts +13 -14
  90. package/dist/types/modules/stream/index.d.ts +7 -10
  91. package/dist/types/modules/stream/kernel/classes.d.ts +220 -0
  92. package/dist/types/modules/stream/kernel/compose-impl.d.ts +10 -0
  93. package/dist/types/modules/stream/kernel/destroy.d.ts +21 -0
  94. package/dist/types/modules/stream/kernel/duplexify.d.ts +31 -0
  95. package/dist/types/modules/stream/kernel/end-of-stream.d.ts +28 -0
  96. package/dist/types/modules/stream/kernel/errors.d.ts +42 -0
  97. package/dist/types/modules/stream/kernel/from.d.ts +17 -0
  98. package/dist/types/modules/stream/kernel/legacy.d.ts +20 -0
  99. package/dist/types/modules/stream/kernel/operators.d.ts +44 -0
  100. package/dist/types/modules/stream/kernel/pipeline-impl.d.ts +32 -0
  101. package/dist/types/modules/stream/kernel/readable-impl.d.ts +34 -0
  102. package/dist/types/modules/stream/kernel/state.d.ts +223 -0
  103. package/dist/types/modules/stream/kernel/stream-utils.d.ts +34 -0
  104. package/dist/types/modules/stream/kernel/web-adapters.d.ts +65 -0
  105. package/dist/types/modules/stream/kernel/writable.d.ts +107 -0
  106. package/dist/types/modules/stream/node/collector.d.ts +7 -2
  107. package/dist/types/modules/stream/node/compose.d.ts +31 -5
  108. package/dist/types/modules/stream/node/factories.d.ts +22 -85
  109. package/dist/types/modules/stream/node/pipeline.d.ts +4 -15
  110. package/dist/types/modules/stream/node/utils.d.ts +9 -49
  111. package/dist/types/modules/stream/node/web.d.ts +18 -0
  112. package/dist/types/modules/stream/pull-stream.d.ts +2 -2
  113. package/dist/types/modules/stream/types.d.ts +24 -6
  114. package/package.json +5 -3
  115. package/dist/esm/modules/stream/browser/_lazy.js +0 -30
  116. package/dist/esm/modules/stream/browser/chunk-buffer.js +0 -223
  117. package/dist/esm/modules/stream/browser/duplex.js +0 -979
  118. package/dist/esm/modules/stream/browser/helpers.js +0 -44
  119. package/dist/esm/modules/stream/browser/microtask-context.js +0 -60
  120. package/dist/esm/modules/stream/browser/passthrough.js +0 -16
  121. package/dist/esm/modules/stream/browser/pipe-manager.js +0 -161
  122. package/dist/esm/modules/stream/browser/readable.js +0 -2208
  123. package/dist/esm/modules/stream/browser/transform.js +0 -1241
  124. package/dist/esm/modules/stream/browser/writable.js +0 -1316
  125. package/dist/esm/modules/stream/core/end-args.js +0 -24
  126. package/dist/esm/modules/stream/node/writable.js +0 -125
  127. package/dist/esm/modules/stream/utils.base.js +0 -126
  128. package/dist/esm/modules/stream/utils.browser.js +0 -80
  129. package/dist/esm/modules/stream/utils.js +0 -81
  130. package/dist/types/modules/stream/browser/_lazy.d.ts +0 -20
  131. package/dist/types/modules/stream/browser/chunk-buffer.d.ts +0 -61
  132. package/dist/types/modules/stream/browser/duplex.d.ts +0 -299
  133. package/dist/types/modules/stream/browser/helpers.d.ts +0 -13
  134. package/dist/types/modules/stream/browser/microtask-context.d.ts +0 -44
  135. package/dist/types/modules/stream/browser/passthrough.d.ts +0 -11
  136. package/dist/types/modules/stream/browser/pipe-manager.d.ts +0 -38
  137. package/dist/types/modules/stream/browser/readable.d.ts +0 -379
  138. package/dist/types/modules/stream/browser/transform.d.ts +0 -314
  139. package/dist/types/modules/stream/browser/writable.d.ts +0 -238
  140. package/dist/types/modules/stream/core/end-args.d.ts +0 -20
  141. package/dist/types/modules/stream/node/writable.d.ts +0 -53
  142. package/dist/types/modules/stream/utils.base.d.ts +0 -63
  143. package/dist/types/modules/stream/utils.browser.d.ts +0 -23
  144. package/dist/types/modules/stream/utils.d.ts +0 -24
package/README.md CHANGED
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
211
211
 
212
212
  ```html
213
213
  <!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
214
- <script src="https://unpkg.com/documonster@0.20.2/dist/iife/documonster.excel.iife.min.js"></script>
214
+ <script src="https://unpkg.com/documonster@0.21.0/dist/iife/documonster.excel.iife.min.js"></script>
215
215
  <script>
216
216
  const { Workbook, Cell } = Documonster.Excel;
217
217
  const wb = Workbook.create();
package/README_zh.md CHANGED
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
198
198
 
199
199
  ```html
200
200
  <!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
201
- <script src="https://unpkg.com/documonster@0.20.2/dist/iife/documonster.excel.iife.min.js"></script>
201
+ <script src="https://unpkg.com/documonster@0.21.0/dist/iife/documonster.excel.iife.min.js"></script>
202
202
  <script>
203
203
  const { Workbook, Cell } = Documonster.Excel;
204
204
  const wb = Workbook.create();
@@ -5,8 +5,8 @@ documonster as a whole is licensed under the Apache License, Version 2.0
5
5
  third-party components identified, and do not affect the licensing of the
6
6
  remainder of the project.
7
7
 
8
- The PDF, Word, Formula, CSV, Markdown, XML, Archive, and Stream modules, as
9
- well as the parts of the Excel module not listed below, are original works of
8
+ The PDF, Word, Formula, CSV, Markdown, XML, and Archive modules, as well as the
9
+ parts of the Excel and Stream modules not listed below, are original works of
10
10
  the documonster authors and are licensed under the Apache License, Version 2.0.
11
11
 
12
12
  ---
@@ -39,3 +39,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39
39
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40
40
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
41
41
  SOFTWARE.
42
+
43
+ ---
44
+
45
+ ## Node.js (Stream module kernel)
46
+
47
+ The stream kernel in `src/modules/stream/kernel/` — the browser's stream classes,
48
+ and the Web adapters and pipeline both entry points use — is a port of Node.js
49
+ `lib/internal/streams/` and `lib/internal/webstreams/adapters.js`. That
50
+ derivative code remains subject to the Node.js MIT License, reproduced below.
51
+ The following copyright notice and permission notice are retained in
52
+ compliance with that license.
53
+
54
+ Copyright Joyent, Inc. and other Node contributors.
55
+
56
+ Permission is hereby granted, free of charge, to any person obtaining a
57
+ copy of this software and associated documentation files (the
58
+ "Software"), to deal in the Software without restriction, including
59
+ without limitation the rights to use, copy, modify, merge, publish,
60
+ distribute, sublicense, and/or sell copies of the Software, and to permit
61
+ persons to whom the Software is furnished to do so, subject to the
62
+ following conditions:
63
+
64
+ The above copyright notice and this permission notice shall be included
65
+ in all copies or substantial portions of the Software.
66
+
67
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
68
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
69
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
70
+ NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
71
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
72
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
73
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -21,7 +21,8 @@ export class ByteQueue {
21
21
  }
22
22
  }
23
23
  _dropHeadChunk() {
24
- this._chunkHead++;
24
+ // Release the consumed chunk now rather than at the next compaction.
25
+ this._chunks[this._chunkHead++] = undefined;
25
26
  this._headOffset = 0;
26
27
  this._compactConsumedChunks();
27
28
  }
@@ -314,6 +314,8 @@ onmessage = async (ev) => {
314
314
  class WorkerInflateRaw extends Duplex {
315
315
  constructor(workerUrl) {
316
316
  super({
317
+ // Output is pushed as the worker replies, not on demand.
318
+ read() { },
317
319
  write: (chunk, _encoding, callback) => {
318
320
  this._doWrite(chunk, callback);
319
321
  },
@@ -465,6 +467,8 @@ class FallbackInflateRaw extends Duplex {
465
467
  this.chunks = [];
466
468
  this._finished = false;
467
469
  }
470
+ // Output is pushed when decompression completes, not on demand.
471
+ _read() { }
468
472
  _decompress(callback) {
469
473
  try {
470
474
  // Combine all chunks
@@ -513,6 +517,8 @@ export function createParseClass(createInflateRawFn) {
513
517
  constructor(opts = {}) {
514
518
  super({
515
519
  objectMode: true,
520
+ // Entries are pushed by the parse loop, not on demand.
521
+ read() { },
516
522
  write: (chunk, _encoding, callback) => {
517
523
  this._handleWrite(chunk, callback);
518
524
  },
@@ -21,7 +21,7 @@ import { getUtf8ByteLength } from "../constants.js";
21
21
  import { CsvError } from "../errors.js";
22
22
  import { parseCsv } from "./sync.js";
23
23
  import { CsvParserStream } from "../stream/parser.js";
24
- import { isReadableStreamLike, readableStreamToAsyncIterable } from "../../stream/utils.base.js";
24
+ import { isReadableStreamLike, readableStreamToAsyncIterable } from "../../stream/core/stream-helpers.js";
25
25
  import { toError } from "../../../utils/errors.js";
26
26
  /**
27
27
  * Rows buffered for the generator's consumer before the parser is paused.
@@ -40,6 +40,7 @@ function normalizeAsyncInput(input) {
40
40
  return input;
41
41
  }
42
42
  if (isReadableStreamLike(input)) {
43
+ // The documented input: a Web stream of text or bytes, like any other source here.
43
44
  return readableStreamToAsyncIterable(input);
44
45
  }
45
46
  throw new TypeError("input must be an AsyncIterable or a ReadableStream");
@@ -24,7 +24,7 @@ import { addWorksheet, getWorksheet } from "#platform/modules/excel/core/workboo
24
24
  import { addRow, eachRow } from "../core/worksheet.js";
25
25
  import { ExcelDownloadError } from "../errors.js";
26
26
  import { pipeline } from "#platform/modules/stream/index";
27
- import { readableStreamToAsyncIterable } from "../../stream/utils.base.js";
27
+ import { readableStreamToAsyncIterable } from "../../stream/core/stream-helpers.js";
28
28
  import { createDateParser, createDateFormatter, createIsoDateFormatter } from "../../../utils/datetime.js";
29
29
  // =============================================================================
30
30
  // Constants
@@ -142,15 +142,11 @@ export async function writeStream(wb, stream, options) {
142
142
  *
143
143
  * ### Leaving `for await` early
144
144
  *
145
- * Breaking out of the loop destroys the stream with no error, so `errored` stays
146
- * `null` and no `'error'` listener fires. Use
147
- * `stream.iterator({ destroyOnReturn: false })` to keep the stream alive past the
148
- * loop — you then have to `destroy()` it yourself, or the serializer stays parked.
149
- *
150
- * Note this differs from the Node build, where Node's own async iterator destroys
151
- * the stream with an `AbortError` on early exit and sets `errored`. `errored` is
152
- * therefore not a portable way to distinguish "the consumer stopped" from
153
- * "serialization failed"; track that yourself if the same code runs on both.
145
+ * As in Node, breaking out of the loop destroys the stream with an `AbortError`:
146
+ * `errored` is set and an attached `'error'` listener receives it. To leave
147
+ * silently, `destroy()` the stream before `break`, or iterate
148
+ * `stream.iterator({ destroyOnReturn: false })` and `destroy()` it yourself
149
+ * afterwards — otherwise the serializer stays parked.
154
150
  */
155
151
  export function toStream(wb, options) {
156
152
  if (options?.format === "xlsb") {
@@ -218,9 +218,7 @@ export async function writeStream(wb, stream, options) {
218
218
  * }
219
219
  * ```
220
220
  *
221
- * The browser build destroys silently on early exit instead, so `errored` is not
222
- * a portable way to tell "the consumer stopped" from "serialization failed" —
223
- * track that yourself if the same code runs on both platforms.
221
+ * The browser build behaves the same way: its stream kernel is a port of Node's.
224
222
  */
225
223
  export function toStream(wb, options) {
226
224
  if (options?.format === "xlsb") {
@@ -1,5 +1,7 @@
1
1
  import { createReadable } from "#platform/modules/stream/index";
2
2
  const textEncoder = new TextEncoder();
3
+ /** 64 KB: the same threshold on Node (whose own default is 16 KB on Windows), Bun and browsers. */
4
+ const DEFAULT_HIGH_WATER_MARK = 64 * 1024;
3
5
  /**
4
6
  * Wrap a push-shaped XLSX serializer in a demand-driven readable byte stream.
5
7
  *
@@ -8,7 +10,7 @@ const textEncoder = new TextEncoder();
8
10
  * the backpressure signal the writer parks on at its next ZIP-entry boundary.
9
11
  */
10
12
  export function createXlsxByteStream(serialize, options) {
11
- const { highWaterMark, ...writeOptions } = options ?? {};
13
+ const { highWaterMark = DEFAULT_HIGH_WATER_MARK, ...writeOptions } = options ?? {};
12
14
  const listeners = new Map();
13
15
  let started = false;
14
16
  let ended = false;
@@ -222,13 +222,11 @@ export class WorkbookReaderBase extends EventEmitter {
222
222
  }
223
223
  throw new ExcelFileError(String(input), "read", "Could not recognise input");
224
224
  }
225
- _cleanupWaitingWorksheets(_waitingWorksheets) {
226
- // Default: attempt best-effort cleanup if the stored object provides it.
227
- for (const ws of _waitingWorksheets) {
228
- if (ws && typeof ws.cleanup === "function") {
229
- ws.cleanup();
230
- }
231
- }
225
+ /** Release every stored worksheet that provides a `cleanup`, and wait until each is gone. */
226
+ async _cleanupWaitingWorksheets(waitingWorksheets) {
227
+ await Promise.all(waitingWorksheets.map(async (ws) => {
228
+ await ws?.cleanup?.();
229
+ }));
232
230
  }
233
231
  // Unified implementations using passed-in classes
234
232
  _createWorksheetReader(params) {
@@ -302,9 +300,11 @@ export class WorkbookReaderBase extends EventEmitter {
302
300
  }
303
301
  yield* this._processWaitingWorksheets(waitingWorksheets);
304
302
  }
305
- catch (error) {
306
- this._cleanupWaitingWorksheets(waitingWorksheets);
307
- throw error;
303
+ finally {
304
+ // On every way out — the end, an error, or a consumer leaving early,
305
+ // which returns the generator and runs no `catch` — so a sheet stored
306
+ // but never replayed does not leave its spool behind.
307
+ await this._cleanupWaitingWorksheets(waitingWorksheets);
308
308
  }
309
309
  }
310
310
  _emitEntry(payload) {
@@ -26,12 +26,13 @@ class WorkbookReader extends WorkbookReaderBase {
26
26
  // a leftover temporary file is self-describing rather than a lie.
27
27
  const isXlsb = /\.bin$/i.test(entry.path);
28
28
  const filePath = join(tmpDir, `sheet${sheetNo}.${isXlsb ? "bin" : "xml"}`);
29
- const cleanup = () => {
30
- remove(tmpDir).catch(() => { });
31
- };
32
29
  const maxBytes = this._maxBufferedBytes;
30
+ const tempStream = createWriteStream(filePath);
31
+ // The file goes only once nothing has it open: removing a directory with
32
+ // an open file fails on Windows and leaves it behind.
33
+ let removed;
34
+ const cleanup = () => (removed ?? (removed = releaseFile(tempStream).then(() => remove(tmpDir))));
33
35
  const writePromise = new Promise((resolve, reject) => {
34
- const tempStream = createWriteStream(filePath);
35
36
  tempStream.on("error", reject);
36
37
  tempStream.on("finish", resolve);
37
38
  // Track bytes written to detect oversized waiting worksheets.
@@ -65,6 +66,10 @@ class WorkbookReader extends WorkbookReaderBase {
65
66
  });
66
67
  entry.pipe(tempStream);
67
68
  });
69
+ // Awaited when the sheet is replayed; a sheet never replayed (the reader
70
+ // left early, or failed first) must not turn its failure into an
71
+ // unhandled rejection.
72
+ writePromise.catch(() => { });
68
73
  return { sheetNo, path: filePath, cleanup, writePromise, isXlsb };
69
74
  }
70
75
  async *_processWaitingWorksheets(waitingWorksheets) {
@@ -80,10 +85,29 @@ class WorkbookReader extends WorkbookReaderBase {
80
85
  finally {
81
86
  // The file goes away now, so a reader not yet read must say so if it is read later rather than wait forever.
82
87
  part.finish();
83
- fileStream.close();
84
- ws.cleanup();
88
+ // Remove the file only once the stream has let go of it. The open is
89
+ // asynchronous and may still be pending when a consumer stops early;
90
+ // removing first fails it with ENOENT on a stream nobody listens to
91
+ // any more (every time on Bun, by luck not on Node).
92
+ await releaseFile(fileStream);
93
+ await ws.cleanup();
85
94
  }
86
95
  }
87
96
  }
88
97
  }
98
+ /**
99
+ * Destroy a file stream and wait until its descriptor is closed. Anything it
100
+ * reports from here on concerns a read or write that was abandoned, so it is
101
+ * dropped.
102
+ */
103
+ function releaseFile(stream) {
104
+ if (stream.closed) {
105
+ return Promise.resolve();
106
+ }
107
+ return new Promise(resolve => {
108
+ stream.on("error", () => { });
109
+ stream.once("close", () => resolve());
110
+ stream.destroy();
111
+ });
112
+ }
89
113
  export { WorkbookReader, WorkbookReaderOptionsSchema };
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Browser Stream - Collector
3
3
  */
4
- import { Writable } from "./writable.js";
5
4
  import { toBinaryChunk } from "../core/binary-chunk.js";
6
5
  import { StreamTypeError } from "../errors.js";
6
+ import { Writable } from "../kernel/writable.js";
7
7
  import { concatUint8Arrays, chunksToString } from "../../../utils/binary.js";
8
8
  // =============================================================================
9
9
  // Collector Stream
@@ -13,10 +13,8 @@ import { concatUint8Arrays, chunksToString } from "../../../utils/binary.js";
13
13
  */
14
14
  export class Collector extends Writable {
15
15
  constructor(options) {
16
- // The write handler collects into a closed-over array; it is exposed as
17
- // `this.chunks` after construction. Using the `write` option (rather than a
18
- // subclass `_write` method) keeps the synchronous direct-write path so
19
- // chunks are collected before the caller's next synchronous statement.
16
+ // The write handler collects into a closed-over array exposed as
17
+ // `this.chunks` after construction, mirroring the Node Collector.
20
18
  const chunks = [];
21
19
  super({
22
20
  ...options,
@@ -1,233 +1,18 @@
1
1
  /**
2
- * Browser Stream - Compose
3
- *
4
- * Compose multiple transform streams into one.
5
- * Aligned with Node.js compose semantics:
6
- * - Backpressure: pauses `last` when composed buffer is full
7
- * - Flush: waits for `last` to emit "end" before signalling completion
8
- * - Error: destroys composed stream on any child error (not just emit)
9
- *
10
- * Uses Transform constructor options (write, final, destroy) instead of
11
- * post-construction method overrides, keeping all internal Writable state
12
- * (buffering, serialization, _writableLength, drain) properly tracked.
2
+ * Browser Stream - compose()
13
3
  */
14
- import { createListenerRegistry } from "./helpers.js";
15
- import { Transform } from "./transform.js";
16
- import { getDefaultHighWaterMark } from "../core/utils.js";
17
- // =============================================================================
18
- // Compose
19
- // =============================================================================
4
+ import { kernel } from "../kernel/classes.js";
5
+ import { composeImpl } from "../kernel/compose-impl.js";
20
6
  /**
21
- * Compose multiple transform streams into one
22
- * Data flows through each transform in sequence
7
+ * Combine stages into one Duplex: writes go into the first, reads come out of
8
+ * the last. Stages are what `pipeline` takes — streams, Web streams,
9
+ * generator functions, iterables — and the rules are Node's: no stage throws
10
+ * ERR_MISSING_ARGS, one stage is `Duplex.from(stage)`.
11
+ *
12
+ * `T` is what is written in and `R` what is read out. They are the caller's
13
+ * statement, like a type argument to `JSON.parse`: the stages are not checked
14
+ * against them, as Node's own `compose` checks nothing.
23
15
  */
24
- export function compose(...transforms) {
25
- const len = transforms.length;
26
- if (len === 0) {
27
- // Boundary: an empty composition is a pure pass-through, so each input
28
- // chunk (type T) is re-emitted unchanged as the declared output type R.
29
- return new Transform({
30
- objectMode: true,
31
- transform: chunk => chunk
32
- });
33
- }
34
- // Preserve identity: compose(single) returns the same transform.
35
- if (len === 1) {
36
- return transforms[0];
37
- }
38
- // Chain the transforms: first → second → ... → last
39
- const first = transforms[0];
40
- const last = transforms[len - 1];
41
- // Pipe all transforms together
42
- for (let i = 0; i < len - 1; i++) {
43
- transforms[i].pipe(transforms[i + 1]);
44
- }
45
- // Track whether last is paused due to backpressure from composed.
46
- let lastPaused = false;
47
- // Track whether flush is handling the end sequence.
48
- let flushing = false;
49
- // Use per-side objectMode matching Node.js compose behavior.
50
- // When the property is missing, default to false (same as Node.js Transform).
51
- const readableObjMode = last.readableObjectMode ?? false;
52
- const writableObjMode = first.writableObjectMode ?? false;
53
- const registry = createListenerRegistry();
54
- const composed = new Transform({
55
- readableHighWaterMark: getDefaultHighWaterMark(readableObjMode),
56
- writableHighWaterMark: getDefaultHighWaterMark(writableObjMode),
57
- readableObjectMode: readableObjMode,
58
- writableObjectMode: writableObjMode,
59
- // Write path: forward writes into the head of the chain.
60
- // Using the `write` constructor option ensures all writes go through the
61
- // Transform's internal Writable state machine (_writableLength tracking,
62
- // write serialization via _writeQueue, cork buffering, drain signals).
63
- write(chunk, encoding, callback) {
64
- try {
65
- first.write(chunk, encoding, callback);
66
- }
67
- catch (err) {
68
- callback(err);
69
- }
70
- },
71
- // Flush path: end the head of the chain, then wait for `last` to emit
72
- // "end" (readable exhaustion) before completing the composed stream.
73
- // This is the `final` handler — called after all pending writes complete,
74
- // before "finish" is emitted. The Transform constructor wrapper will
75
- // automatically push(null) on the readable side after our callback.
76
- final(callback) {
77
- flushing = true;
78
- // If `last` already ended independently (e.g. a "take N" transform that
79
- // pushed null on its own), complete immediately — the "end" event has
80
- // already fired and a new once("end") listener would never trigger.
81
- if (last.readableEnded) {
82
- callback();
83
- return;
84
- }
85
- const onEnd = () => {
86
- cleanupFlush();
87
- callback();
88
- };
89
- const onError = (err) => {
90
- cleanupFlush();
91
- callback(err);
92
- };
93
- const cleanupFlush = () => {
94
- last.off?.("end", onEnd);
95
- last.off?.("error", onError);
96
- };
97
- last.once?.("end", onEnd);
98
- last.once?.("error", onError);
99
- first.end();
100
- },
101
- // Destroy path: clean up all listeners and destroy all child transforms.
102
- destroy(_error, callback) {
103
- try {
104
- registry.cleanup();
105
- for (const t of transforms) {
106
- t.destroy(_error ?? undefined);
107
- }
108
- }
109
- finally {
110
- callback(_error);
111
- }
112
- }
113
- });
114
- // Hook into the internal _readable's _read method so that when the
115
- // PipeManager (or any consumer) pulls data, we resume `last` if it was
116
- // paused due to backpressure. This is the browser equivalent of Node.js
117
- // compose's `read()` option which is called by the native pipe mechanism.
118
- //
119
- // Compose the new behavior with the Transform's existing _read patch
120
- // (which releases _afterTransformCallback). Although _afterTransformCallback
121
- // is not used when the `write` option is provided, preserving it is a
122
- // defensive measure against future changes.
123
- const composedReadable = composed._readable;
124
- const origRead = composedReadable._read;
125
- composedReadable._read = () => {
126
- origRead?.call(composedReadable);
127
- if (lastPaused) {
128
- lastPaused = false;
129
- last.resume?.();
130
- }
131
- };
132
- // Forward errors from all transforms — destroy composed on error (matches Node.js).
133
- for (const t of transforms) {
134
- registry.add(t, "error", (err) => composed.destroy(err));
135
- }
136
- // Drain is handled by the internal Writable's own drain → Transform's
137
- // _setupSideForwarding → composed.emit("drain"). No need to forward from
138
- // `first` — that would cause double-drain when both buffers cross below HWM.
139
- // Track whether both sides have completed so we can auto-destroy the composed
140
- // stream (emitting 'close'), matching Node.js compose behavior.
141
- let composedEndFired = false;
142
- let composedFinishFired = false;
143
- const maybeAutoDestroy = () => {
144
- if (composedEndFired && composedFinishFired && !composed.destroyed) {
145
- composed.destroy();
146
- }
147
- };
148
- // Finish is handled by the internal Writable's own lifecycle: when the
149
- // `final` handler calls callback(), the internal Writable emits "finish",
150
- // which _setupSideForwarding propagates to composed.emit("finish").
151
- // Forwarding finish from `last` would cause double-finish.
152
- // Track the composed stream's own finish for auto-destroy.
153
- composed.once("finish", () => {
154
- composedFinishFired = true;
155
- maybeAutoDestroy();
156
- });
157
- // Eagerly attach data/end forwarding from `last` to composed (matching Node.js).
158
- // Node.js compose immediately attaches last.on("data") so data flows into
159
- // composed's buffer right away, ensuring no data is missed.
160
- registry.add(last, "data", (chunk) => {
161
- if (!composed.push(chunk)) {
162
- lastPaused = true;
163
- last.pause?.();
164
- }
165
- });
166
- registry.once(last, "end", () => {
167
- // When flushing, the final handler + Transform wrapper handles stream
168
- // termination (push(null)). Otherwise (e.g. last ended independently),
169
- // we must push(null) ourselves.
170
- if (!flushing) {
171
- composed.push(null);
172
- }
173
- });
174
- // Track when the composed stream's own 'end' fires (from push(null)).
175
- composed.once("end", () => {
176
- composedEndFired = true;
177
- maybeAutoDestroy();
178
- });
179
- // Delegate cork/uncork to the head of the chain.
180
- composed.cork = () => {
181
- first.cork?.();
182
- };
183
- composed.uncork = () => {
184
- first.uncork?.();
185
- };
186
- // Safety net: ensure listener cleanup even if close fires through an
187
- // unexpected path (destroy option already calls registry.cleanup).
188
- composed.once("close", () => {
189
- registry.cleanup();
190
- });
191
- // Reflect underlying readability/writability like the previous duck-typed wrapper
192
- Object.defineProperty(composed, "readable", {
193
- get: () => last.readable
194
- });
195
- Object.defineProperty(composed, "writable", {
196
- get: () => first.writable
197
- });
198
- // Proxy writable-side state to `first` so properties like writableEnded and
199
- // writableFinished reflect the actual head-of-chain state, not the inner
200
- // Transform wrapper which is never written to directly.
201
- Object.defineProperty(composed, "writableEnded", {
202
- get: () => first.writableEnded ?? false
203
- });
204
- Object.defineProperty(composed, "writableFinished", {
205
- get: () => first.writableFinished ?? false
206
- });
207
- Object.defineProperty(composed, "writableLength", {
208
- get: () => first.writableLength ?? 0
209
- });
210
- Object.defineProperty(composed, "writableHighWaterMark", {
211
- get: () => first.writableHighWaterMark ?? getDefaultHighWaterMark(false)
212
- });
213
- Object.defineProperty(composed, "writableCorked", {
214
- get: () => first.writableCorked ?? 0
215
- });
216
- Object.defineProperty(composed, "writableNeedDrain", {
217
- get: () => first.writableNeedDrain ?? false
218
- });
219
- // Proxy readable-side state to `last`.
220
- Object.defineProperty(composed, "readableEnded", {
221
- get: () => last.readableEnded ?? false
222
- });
223
- Object.defineProperty(composed, "readableLength", {
224
- get: () => last.readableLength ?? 0
225
- });
226
- Object.defineProperty(composed, "readableHighWaterMark", {
227
- get: () => last.readableHighWaterMark ?? getDefaultHighWaterMark(false)
228
- });
229
- Object.defineProperty(composed, "readableFlowing", {
230
- get: () => last.readableFlowing ?? null
231
- });
232
- return composed;
16
+ export function compose(...stages) {
17
+ return composeImpl(kernel, ...stages);
233
18
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Browser Stream - duplexPair()
3
+ *
4
+ * Its own module: `DuplexSide extends Duplex` is a top-level class, which a
5
+ * bundler keeps — with the whole Duplex chain behind it — whenever the module
6
+ * holding it is kept.
7
+ */
8
+ import { Duplex } from "../kernel/classes.js";
9
+ import { nextTick } from "../kernel/state.js";
10
+ /**
11
+ * One end of a {@link duplexPair}: what is written here is pushed to the
12
+ * other end, and the write completes only when the other end reads it.
13
+ * Port of Node.js `lib/internal/streams/duplexpair.js`.
14
+ */
15
+ class DuplexSide extends Duplex {
16
+ constructor() {
17
+ super(...arguments);
18
+ this._otherSide = null;
19
+ this._pendingCallback = null;
20
+ }
21
+ _initOtherSide(otherSide) {
22
+ this._otherSide = otherSide;
23
+ }
24
+ _read(_size) {
25
+ // The other side's write waits for this read; release it.
26
+ const callback = this._pendingCallback;
27
+ if (callback) {
28
+ this._pendingCallback = null;
29
+ callback();
30
+ }
31
+ }
32
+ _write(chunk, _encoding, callback) {
33
+ const other = this._otherSide;
34
+ if (chunk.length === 0) {
35
+ nextTick(callback);
36
+ }
37
+ else {
38
+ other.push(chunk);
39
+ other._pendingCallback = callback;
40
+ }
41
+ }
42
+ _final(callback) {
43
+ this._otherSide.on("end", callback);
44
+ this._otherSide.push(null);
45
+ }
46
+ _destroy(err, callback) {
47
+ const other = this._otherSide;
48
+ if (other !== null && !other.destroyed) {
49
+ nextTick(() => {
50
+ if (other.destroyed) {
51
+ return;
52
+ }
53
+ if (err) {
54
+ other.destroy();
55
+ }
56
+ else {
57
+ other.push(null);
58
+ }
59
+ });
60
+ }
61
+ callback(err);
62
+ }
63
+ }
64
+ /** Two connected Duplex streams: each one reads what the other writes. */
65
+ export function duplexPair(options) {
66
+ const side0 = new DuplexSide(options);
67
+ const side1 = new DuplexSide(options);
68
+ side0._initOtherSide(side1);
69
+ side1._initOtherSide(side0);
70
+ return [side0, side1];
71
+ }