@xlsxflow/core 1.1.5 → 1.1.6

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.6
4
+
5
+ ### Changed
6
+
7
+ - `SheetWriter`, `SheetEditor` and `OdsWriter` keep up to four chunks queued for compression instead of one, so building the next chunk overlaps compressing the last. Writing 10M cells takes about 10% less time; the files are the same, and the writer still waits while nobody reads its output.
8
+
9
+ ### Testing
10
+
11
+ - Public CI on every push and pull request: the tests on Node 20, 22 and 24 with coverage; the README's claims in Node, Bun, Deno, Chromium, Firefox, WebKit and Cloudflare Workers; and written and edited files validated with Microsoft's Open XML SDK and opened in LibreOffice. OpenSSF Scorecard and CodeQL check the repository.
12
+
3
13
  ## 1.1.5
4
14
 
5
15
  Fixes found while testing every claim in the README on Node 20.12 and 25, Bun, Deno, Chrome and Cloudflare Workers.
package/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  [![npm version](https://img.shields.io/npm/v/@xlsxflow/core.svg?style=flat-square)](https://www.npmjs.com/package/@xlsxflow/core)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
10
+ [![CI](https://img.shields.io/github/actions/workflow/status/xlsxflow/xlsxflow/ci.yml?branch=main&label=CI&style=flat-square)](https://github.com/xlsxflow/xlsxflow/actions/workflows/ci.yml)
11
+ [![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/xlsxflow/xlsxflow?label=OpenSSF%20Scorecard&style=flat-square)](https://scorecard.dev/viewer/?uri=github.com/xlsxflow/xlsxflow)
10
12
 
11
13
  <p>
12
14
  <a href="#features">Features</a> •
@@ -28,8 +30,8 @@ Rows are read and written one at a time instead of loading the whole workbook, s
28
30
  ## Features
29
31
 
30
32
  - **No dependencies**: TypeScript on Web APIs (`ReadableStream`, `CompressionStream`, `Blob`).
31
- - **Streaming**: rows are read and written one at a time, so memory stays flat as files grow (10M cells written with about 1 MB of extra heap; see [Benchmarks](#benchmarks)).
32
- - **Runs anywhere with Web APIs**: tested on Node 20.12 and later, Bun, Deno, Chrome and Cloudflare Workers (without `nodejs_compat`).
33
+ - **Streaming**: rows are read and written one at a time, so memory stays flat as files grow (10M cells written with about 2 MB of extra heap; see [Benchmarks](#benchmarks)).
34
+ - **Runs anywhere with Web APIs**: tested on Node 20.12 and later, Bun, Deno, Chrome, Firefox, Safari (WebKit) and Cloudflare Workers (without `nodejs_compat`).
33
35
  - **Read, write and edit**: stream rows out of a file, generate one on the fly, or change cells, rows, columns and sheets of an existing file while keeping everything else in it.
34
36
  - **Styles and formulas**: fonts, fills, borders, alignment, number formats, conditional formats, validations, tables, notes, hyperlinks, autofilters, images, protection and page setup. Formulas and styles read back too.
35
37
  - **Older and open formats**: the same reader opens Excel 97-2003 `.xls` files and OpenDocument `.ods` files, and `OdsWriter` writes `.ods`.
@@ -285,16 +287,26 @@ SheetJS reads and writes far more formats, and ExcelJS has a longer track record
285
287
  October 2023). XlsxFlow focuses on `.xlsx`: streaming in flat memory, keeping everything in a file it edits,
286
288
  and running on Web APIs alone.
287
289
 
290
+ ## Testing
291
+
292
+ Every push and pull request runs, [in public CI](https://github.com/xlsxflow/xlsxflow/actions/workflows/ci.yml):
293
+
294
+ - the test suite (nearly 400 tests, including fuzzing with malformed ZIP and XML input and files saved by Excel, LibreOffice, SheetJS, ExcelJS, openpyxl and xlsx-populate) on Node 20, 22 and 24, with 96% line coverage;
295
+ - the README's claims, checked in Node, Bun, Deno, Chromium, Firefox, WebKit and Cloudflare Workers;
296
+ - written and edited workbooks validated against the Office Open XML schema with Microsoft's [Open XML SDK](https://github.com/dotnet/Open-XML-SDK), then opened and recalculated in LibreOffice.
297
+
298
+ [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/xlsxflow/xlsxflow) and CodeQL check the repository, and npm releases are published from CI with [provenance](https://docs.npmjs.com/generating-provenance-statements).
299
+
288
300
  ## Benchmarks
289
301
 
290
- Write benchmark: 10 numeric columns, at 100,000 rows (1M cells) and 1,000,000 rows (10M cells). Each library ran in its own process on Node v25.8.2 with a 4 GB heap limit, and "Heap" is the growth in heap usage. Times are from one run on a laptop with other apps open; runs on that machine varied by up to 2×, so treat differences under about 20% as a tie. Reproduce with `npx tsx scripts/benchmark-competitors.ts` (inside `packages/core`, after `pnpm build`). `BENCH_ROWS=1000000` runs 10M cells, `BENCH_LIBS=xlsxflow,exceljs` runs a subset, and a library still writing after `BENCH_TIMEOUT_MIN` minutes (default 10) is stopped. The 10M runs for SheetJS and excel4node used a 30-minute limit.
302
+ Write benchmark: 10 numeric columns, at 100,000 rows (1M cells) and 1,000,000 rows (10M cells). Each library ran in its own process on Node v25.8.2 with a 4 GB heap limit, and "Heap" is the growth in heap usage. XlsxFlow and ExcelJS times are medians of five alternating runs (XlsxFlow 1.1.6 was faster in all five at both sizes); the other libraries' times are from one run. The laptop had other apps open and single runs varied by up to 2×, so treat differences under about 20% as a tie. Reproduce with `npx tsx scripts/benchmark-competitors.ts` (inside `packages/core`, after `pnpm build`). `BENCH_ROWS=1000000` runs 10M cells, `BENCH_LIBS=xlsxflow,exceljs` runs a subset, and a library still writing after `BENCH_TIMEOUT_MIN` minutes (default 10) is stopped. The 10M runs for SheetJS and excel4node used a 30-minute limit.
291
303
 
292
304
  1M cells:
293
305
 
294
306
  | Library | Write Time | File Size | Heap |
295
307
  |---|---|---|---|
296
- | **XlsxFlow** | **2,899 ms** | **2.9 MB** | **+2 MB** |
297
- | ExcelJS 4.4 (streaming writer) | 3,397 ms | 3.0 MB | +9 MB |
308
+ | **XlsxFlow** | **1,723 ms** | **2.9 MB** | **+2 MB** |
309
+ | ExcelJS 4.4 (streaming writer) | 2,290 ms | 3.0 MB | +9 MB |
298
310
  | SheetJS 0.20.3, `compression: true` | 4,370 ms | 8.4 MB | +140 MB |
299
311
  | SheetJS 0.20.3, default options | 5,379 ms | 31.4 MB | +140 MB |
300
312
  | write-excel-file | 7,681 ms | 2.8 MB | +2 MB |
@@ -306,8 +318,8 @@ Write benchmark: 10 numeric columns, at 100,000 rows (1M cells) and 1,000,000 ro
306
318
 
307
319
  | Library | Write Time | File Size | Heap |
308
320
  |---|---|---|---|
309
- | **XlsxFlow** | **19.5 s** | **29.9 MB** | **+1 MB** |
310
- | ExcelJS 4.4 (streaming writer) | 22.0 s | 31.2 MB | +7 MB |
321
+ | **XlsxFlow** | **19.1 s** | **29.9 MB** | **+2 MB** |
322
+ | ExcelJS 4.4 (streaming writer) | 20.5 s | 31.2 MB | +7 MB |
311
323
  | write-excel-file | 68.1 s | 29.3 MB | +1 MB |
312
324
  | xlsx-populate | 75.3 s | 30.0 MB | +1,118 MB |
313
325
  | SheetJS 0.20.3 (with and without compression) | not finished after 30 min | | |
package/dist/index.cjs CHANGED
@@ -62,9 +62,11 @@ class ZipStreamWriter {
62
62
  const writer = compressor.writable.getWriter();
63
63
  const reader = compressor.readable.getReader();
64
64
  const input = inputStream.getReader();
65
- // Node's and Bun's CompressionStream accept thousands of writes without backpressure, so one chunk
66
- // is fed at a time: a write resolves once the chunk is compressed, which waits while nobody reads
65
+ // Node's and Bun's CompressionStream accept thousands of writes without backpressure, so only a few
66
+ // chunks are fed ahead: a write resolves once the chunk is compressed, which waits while nobody reads
67
67
  // the output. Rows are then only pulled as fast as the ZIP is consumed.
68
+ // A few writes in flight keep the compressor busy while the next chunk is built
69
+ const inFlight = [];
68
70
  const feed = (async () => {
69
71
  try {
70
72
  while (true) {
@@ -74,8 +76,13 @@ class ZipStreamWriter {
74
76
  await this.roomInQueue();
75
77
  uncompressedSize += value.length;
76
78
  crc = crc32Update(crc, value);
77
- await writer.write(value);
79
+ const written = writer.write(value);
80
+ written.catch(() => { }); // after a failure, writes nobody awaits any more must not go unhandled
81
+ inFlight.push(written);
82
+ if (inFlight.length >= 4)
83
+ await inFlight.shift();
78
84
  }
85
+ await Promise.all(inFlight);
79
86
  await writer.close();
80
87
  }
81
88
  catch (err) {