@zip.js/zip.js 2.8.31 → 2.8.32

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/BENCHMARKS.md CHANGED
@@ -16,7 +16,9 @@ should not trust anyone's benchmark (including this one) without reproducing it.
16
16
  > `CompressionStream` run on the threadpool while you simply issue concurrent `add()`
17
17
  > calls. It also streams arbitrarily large files at a flat, low memory ceiling. For
18
18
  > deflating thousands of tiny buffers in one shot, **fflate** remains the throughput
19
- > and footprint champion.
19
+ > and footprint champion. Against native tooling, zip.js on a modern runtime is a bit
20
+ > faster *and* tighter than 7-Zip's fast mode on single-file streaming; 7-Zip keeps a
21
+ > real edge only on thousands of tiny files.
20
22
 
21
23
  ## Environment
22
24
 
@@ -172,6 +174,80 @@ compression pipeline. **jszip buffers the entire file** and needs ~527 MB, at mo
172
174
  twice the wall-clock time. If you process files that do not fit comfortably in memory,
173
175
  avoid jszip.
174
176
 
177
+ ## Against native tooling — 7-Zip
178
+
179
+ How far is zip.js from a native archiver? [`benchmarks/bench-7z.js`](benchmarks/bench-7z.js)
180
+ compares it against the `7zz` CLI (7-Zip 25.01), disk-to-disk on both sides, under Deno
181
+ 2.9.3 and Bun 1.3.14 (both runtimes agree within noise; Deno numbers shown). 7-Zip
182
+ timings include process spawn (~ms).
183
+
184
+ One comparison is impossible to make perfectly fair: **no 7-Zip preset runs zlib's
185
+ algorithm.** `-mx=5+` is a near-optimal parser (much slower, smaller output) and
186
+ `-mx=1..4` a greedy one (faster, larger output) — they bracket zlib level 6. So the
187
+ table carries two anchors: `-mx=6` as the *ratio* anchor and `-mx=1` as the *speed*
188
+ anchor. On this machine the `-mx` ladder on the 256 MB file reads: `-mx=1/3` → 5.9 s,
189
+ `-mx=5/6` → 34 s, `-mx=7` → 85 s — the 6× jump at `-mx=5` is the switch from greedy
190
+ matching to optimal parsing, and it cannot be hidden by threads (one file = one deflate
191
+ stream = one core).
192
+
193
+ | Workload (compress) | zip.js (workers) | 7-Zip `-mx=6` (mt) | 7-Zip `-mx=1` (mt) |
194
+ |---|--:|--:|--:|
195
+ | Compressible text (20 MB) | **426 ms** / 6.2 MB | 2606 ms / 5.8 MB | 470 ms / 6.8 MB |
196
+ | Incompressible data (20 MB) | **342 ms** | 410 ms | 372 ms |
197
+ | 5,000 files × ~2 KB | 780 ms / 5.5 MB | 199 ms / 4.9 MB | **168 ms** / 5.0 MB |
198
+ | Large file, disk-to-disk (256 MB) | **5.5 s** / 79.5 MB | 33.4 s / 73.6 MB | 5.9 s / 86.7 MB |
199
+
200
+ | Workload (decompress) | zip.js (workers) | 7-Zip (mt) |
201
+ |---|--:|--:|
202
+ | Compressible text (20 MB) | **69 ms** | 88 ms |
203
+ | 5,000 files × ~2 KB | 976 ms | **471 ms** |
204
+
205
+ - **On single-file streaming, zip.js strictly dominates 7-Zip's speed tier**: a bit
206
+ faster *and* ~9 % smaller output, because zlib-6's lazy matching is a better
207
+ speed/ratio point than 7-Zip's greedy fast mode. What `-mx=6` buys for its 6× time is
208
+ ~7 % more compression — a trade zip.js cannot make, but also one most workloads don't
209
+ want.
210
+ - **7-Zip legitimately dominates many small files** (~4× on compress, ~2× on
211
+ decompress): its per-entry cost is near zero, while zip.js pays per-entry
212
+ orchestration (worker round trips, header writes, one output stream per extracted
213
+ file). Same lesson as the fflate rows above — tiny-entry workloads are zip.js's cost
214
+ center, and the codec backend is irrelevant there.
215
+
216
+ ## The runtime's zlib decides zip.js throughput
217
+
218
+ On bulk data zip.js is a thin wrapper around the platform's `CompressionStream` — it
219
+ adds ~7 % over the raw encoder on the 256 MB stream. That means throughput is decided
220
+ by **which zlib the runtime vendors**, and they differ a lot. Same 256 MB compressible
221
+ file, one thread, level-6-class output everywhere:
222
+
223
+ | Encoder | Time | Output |
224
+ |---|--:|--:|
225
+ | zlib-ng — Deno & Bun `CompressionStream` | **4.8–5.1 s** | 79.4 MB |
226
+ | Chromium zlib — Node `CompressionStream` / `node:zlib` | 8.9 s | 78.3 MB |
227
+ | classic zlib — Apple `gzip -6` | 12.5 s | 78.9 MB |
228
+ | classic zlib compiled to WASM — zip.js `useCompressionStream: false` | ~16 s | 78.9 MB |
229
+ | (reference) 7-Zip `-mx=6` | 33.5 s | 73.6 MB |
230
+
231
+ Verified in the runtimes' sources: Deno builds `flate2` with vendored
232
+ **zlib-ng** (`__vendored_zlib_ng` default feature), Bun vendors **zlib-ng 2.3.3**
233
+ (SIMD CRC/adler/match kernels, NEON on arm64) behind `node:zlib` and
234
+ `CompressionStream`, and Node vendors **Chromium's zlib fork** (SIMD checksums and
235
+ hash sliding, but a match finder much closer to classic zlib).
236
+
237
+ Consequences worth knowing:
238
+
239
+ - **The same zip.js code runs ~1.75× faster on Deno/Bun than on Node** for bulk
240
+ compression. A "zip.js is fast/slow" measurement is often really a statement about
241
+ the host's zlib — and the Node tables above are the *pessimistic* end.
242
+ - **The WASM backend is the portability floor, not a peer**: classic zlib plus ~30 %
243
+ WebAssembly overhead ≈ 3× slower than native on compress. Decompression barely
244
+ suffers (inflate is cheap — within ~10 % of native). It still lands mid-curve
245
+ against 7-Zip: 16 s / 78.9 MB sits between `-mx=1` (5.9 s / 86.7 MB) and `-mx=6`
246
+ (33.5 s / 73.6 MB).
247
+ - Output sizes across the zlib family are interchangeable (78–79 MB): it is one
248
+ algorithm at four levels of implementation tuning. zip.js inherits whichever the
249
+ host provides — including future upgrades, for free.
250
+
175
251
  ## When to pick which
176
252
 
177
253
  - **Choose zip.js** for the fastest compression of large or multiple entries
@@ -195,8 +271,13 @@ npm install # jszip, fflate, archiver (zip.js is used from the repo)
195
271
  npm run corpus # generate the deterministic datasets under .corpus/
196
272
  node bench.js # the head-to-head tables (compress / decompress / disk streaming)
197
273
  node bench-backends.js # the parallelism & codec-backend matrix
274
+ deno run -A bench-7z.js # zip.js vs the 7zz CLI (also: bun bench-7z.js)
198
275
  ```
199
276
 
277
+ `bench-7z.js` needs the 7-Zip CLI (`brew install sevenzip`) and runs under Deno or Bun;
278
+ set `ZIPJS_BACKEND=wasm` to measure the WebAssembly codec instead of the native
279
+ `CompressionStream`, and `SKIP_HUGE=1` to skip the 256 MB combo.
280
+
200
281
  Both scripts write JSON and a human-readable log to `benchmarks/results/`. Set
201
282
  `RUNS=<n>` to change the number of repetitions (default 3). The datasets are generated
202
283
  from a seeded PRNG (`benchmarks/lib/corpus.js`), so every run — and every library —
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zip-js/zip-js",
3
- "version": "2.8.31",
3
+ "version": "2.8.32",
4
4
  "exports": {
5
5
  ".": "./index.js"
6
6
  },
package/dist/zip-core.js CHANGED
@@ -2445,7 +2445,7 @@
2445
2445
  const { value, readable, writable } = message;
2446
2446
  const transferables = [];
2447
2447
  if (value) {
2448
- message.value = value;
2448
+ message.value = value.byteOffset || value.byteLength != value.buffer.byteLength ? new Uint8Array(value) : value;
2449
2449
  transferables.push(message.value.buffer);
2450
2450
  }
2451
2451
  if (transferStreams && transferStreamsSupported) {
@@ -6646,13 +6646,13 @@
6646
6646
  */
6647
6647
 
6648
6648
 
6649
- const DEFAULT_THRESHOLD = 1024 * 1024;
6650
- const DEFAULT_DIRECTORY_NAME = ".zip.js-temp";
6649
+ const DEFAULT_THRESHOLD$2 = 1024 * 1024;
6650
+ const DEFAULT_DIRECTORY_NAME$1 = ".zip.js-temp";
6651
6651
 
6652
6652
  function createOPFSTempStream(options = {}) {
6653
6653
  const {
6654
- thresholdBytes = DEFAULT_THRESHOLD,
6655
- directoryName = DEFAULT_DIRECTORY_NAME,
6654
+ thresholdBytes = DEFAULT_THRESHOLD$2,
6655
+ directoryName = DEFAULT_DIRECTORY_NAME$1,
6656
6656
  getDirectory = () => navigator.storage.getDirectory()
6657
6657
  } = options;
6658
6658
  let directoryHandlePromise;
@@ -6781,6 +6781,269 @@
6781
6781
  */
6782
6782
 
6783
6783
 
6784
+ const DEFAULT_THRESHOLD$1 = 1024 * 1024;
6785
+
6786
+ function createBlobTempStream(options = {}) {
6787
+ const {
6788
+ thresholdBytes = DEFAULT_THRESHOLD$1
6789
+ } = options;
6790
+ return function () {
6791
+ const memoryChunks = [];
6792
+ let bufferedSize = 0;
6793
+ let spilled = false;
6794
+ let blobWriter, blobPromise, blobReader;
6795
+
6796
+ async function spillToBlob() {
6797
+ const transformStream = new TransformStream();
6798
+ blobPromise = new Response(transformStream.readable).blob();
6799
+ blobWriter = transformStream.writable.getWriter();
6800
+ spilled = true;
6801
+ for (const chunk of memoryChunks) {
6802
+ await blobWriter.write(chunk);
6803
+ }
6804
+ memoryChunks.length = 0;
6805
+ }
6806
+
6807
+ const writable = new WritableStream({
6808
+ async write(chunk) {
6809
+ if (spilled) {
6810
+ await blobWriter.write(chunk);
6811
+ } else {
6812
+ memoryChunks.push(chunk);
6813
+ bufferedSize += chunk.length;
6814
+ if (bufferedSize > thresholdBytes) {
6815
+ await spillToBlob();
6816
+ }
6817
+ }
6818
+ },
6819
+ async close() {
6820
+ if (blobWriter) {
6821
+ await blobWriter.close();
6822
+ blobWriter = null;
6823
+ }
6824
+ }
6825
+ });
6826
+
6827
+ let memoryIndex = 0;
6828
+ const readable = new ReadableStream({
6829
+ async pull(controller) {
6830
+ if (spilled) {
6831
+ if (!blobReader) {
6832
+ const blob = await blobPromise;
6833
+ blobReader = blob.stream().getReader();
6834
+ }
6835
+ const { value, done } = await blobReader.read();
6836
+ if (done) {
6837
+ controller.close();
6838
+ } else {
6839
+ controller.enqueue(value);
6840
+ }
6841
+ } else if (memoryIndex < memoryChunks.length) {
6842
+ controller.enqueue(memoryChunks[memoryIndex++]);
6843
+ } else {
6844
+ controller.close();
6845
+ }
6846
+ },
6847
+ async cancel(reason) {
6848
+ if (blobReader) {
6849
+ await blobReader.cancel(reason);
6850
+ }
6851
+ }
6852
+ }, { highWaterMark: 0 });
6853
+ async function dispose() {
6854
+ if (blobWriter) {
6855
+ try {
6856
+ await blobWriter.abort();
6857
+ } catch {
6858
+ // ignored
6859
+ }
6860
+ blobWriter = null;
6861
+ }
6862
+ if (blobPromise) {
6863
+ blobPromise.catch(() => {
6864
+ // ignored
6865
+ });
6866
+ blobPromise = null;
6867
+ }
6868
+ memoryChunks.length = 0;
6869
+ }
6870
+
6871
+ return { writable, readable, dispose };
6872
+ };
6873
+ }
6874
+
6875
+ /*
6876
+ Copyright (c) 2025 Gildas Lormeau. All rights reserved.
6877
+
6878
+ Redistribution and use in source and binary forms, with or without
6879
+ modification, are permitted provided that the following conditions are met:
6880
+
6881
+ 1. Redistributions of source code must retain the above copyright notice,
6882
+ this list of conditions and the following disclaimer.
6883
+
6884
+ 2. Redistributions in binary form must reproduce the above copyright
6885
+ notice, this list of conditions and the following disclaimer in
6886
+ the documentation and/or other materials provided with the distribution.
6887
+
6888
+ 3. The names of the authors may not be used to endorse or promote products
6889
+ derived from this software without specific prior written permission.
6890
+
6891
+ THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
6892
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
6893
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
6894
+ INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
6895
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
6896
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
6897
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
6898
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
6899
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
6900
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6901
+ */
6902
+
6903
+
6904
+ const DEFAULT_THRESHOLD = 1024 * 1024;
6905
+ const DEFAULT_DIRECTORY_NAME = ".zip.js-temp";
6906
+ const READ_CHUNK_SIZE = 512 * 1024;
6907
+ const ERR_UNSUPPORTED_CONTEXT = "createSyncAccessHandle is only available in dedicated workers";
6908
+
6909
+ function createSyncAccessHandleTempStream(options = {}) {
6910
+ const {
6911
+ thresholdBytes = DEFAULT_THRESHOLD,
6912
+ directoryName = DEFAULT_DIRECTORY_NAME,
6913
+ getDirectory
6914
+ } = options;
6915
+ if (!getDirectory &&
6916
+ (typeof FileSystemFileHandle == "undefined" || !FileSystemFileHandle.prototype.createSyncAccessHandle)) {
6917
+ throw new Error(ERR_UNSUPPORTED_CONTEXT);
6918
+ }
6919
+ const getRootDirectory = getDirectory || (() => navigator.storage.getDirectory());
6920
+ let directoryHandlePromise;
6921
+ function getTempDirectory() {
6922
+ if (!directoryHandlePromise) {
6923
+ directoryHandlePromise = Promise.resolve(getRootDirectory())
6924
+ .then(root => root.getDirectoryHandle(directoryName, { create: true }));
6925
+ }
6926
+ return directoryHandlePromise;
6927
+ }
6928
+ return function () {
6929
+ const memoryChunks = [];
6930
+ let bufferedSize = 0;
6931
+ let spilled = false;
6932
+ let fileName, accessHandle;
6933
+ let writeOffset = 0;
6934
+ let readOffset = 0;
6935
+
6936
+ async function spillToFile() {
6937
+ const directoryHandle = await getTempDirectory();
6938
+ fileName = crypto.randomUUID();
6939
+ const fileHandle = await directoryHandle.getFileHandle(fileName, { create: true });
6940
+ accessHandle = await fileHandle.createSyncAccessHandle();
6941
+ spilled = true;
6942
+ for (const chunk of memoryChunks) {
6943
+ accessHandle.write(chunk, { at: writeOffset });
6944
+ writeOffset += chunk.length;
6945
+ }
6946
+ memoryChunks.length = 0;
6947
+ }
6948
+
6949
+ const writable = new WritableStream({
6950
+ async write(chunk) {
6951
+ if (spilled) {
6952
+ accessHandle.write(chunk, { at: writeOffset });
6953
+ writeOffset += chunk.length;
6954
+ } else {
6955
+ memoryChunks.push(chunk);
6956
+ bufferedSize += chunk.length;
6957
+ if (bufferedSize > thresholdBytes) {
6958
+ await spillToFile();
6959
+ }
6960
+ }
6961
+ },
6962
+ close() {
6963
+ if (accessHandle) {
6964
+ accessHandle.flush();
6965
+ }
6966
+ }
6967
+ });
6968
+
6969
+ let memoryIndex = 0;
6970
+ const readable = new ReadableStream({
6971
+ pull(controller) {
6972
+ if (spilled) {
6973
+ const remaining = writeOffset - readOffset;
6974
+ if (remaining <= 0) {
6975
+ controller.close();
6976
+ return;
6977
+ }
6978
+ const buffer = new Uint8Array(Math.min(READ_CHUNK_SIZE, remaining));
6979
+ const read = accessHandle.read(buffer, { at: readOffset });
6980
+ if (read) {
6981
+ readOffset += read;
6982
+ controller.enqueue(buffer.subarray(0, read));
6983
+ } else {
6984
+ controller.close();
6985
+ }
6986
+ } else if (memoryIndex < memoryChunks.length) {
6987
+ controller.enqueue(memoryChunks[memoryIndex++]);
6988
+ } else {
6989
+ controller.close();
6990
+ }
6991
+ }
6992
+ }, { highWaterMark: 0 });
6993
+ async function dispose() {
6994
+ if (accessHandle) {
6995
+ try {
6996
+ accessHandle.close();
6997
+ } catch {
6998
+ // ignored
6999
+ }
7000
+ accessHandle = null;
7001
+ }
7002
+ if (fileName) {
7003
+ try {
7004
+ const directoryHandle = await getTempDirectory();
7005
+ await directoryHandle.removeEntry(fileName);
7006
+ } catch {
7007
+ // ignored
7008
+ }
7009
+ fileName = null;
7010
+ }
7011
+ memoryChunks.length = 0;
7012
+ }
7013
+
7014
+ return { writable, readable, dispose };
7015
+ };
7016
+ }
7017
+
7018
+ /*
7019
+ Copyright (c) 2025 Gildas Lormeau. All rights reserved.
7020
+
7021
+ Redistribution and use in source and binary forms, with or without
7022
+ modification, are permitted provided that the following conditions are met:
7023
+
7024
+ 1. Redistributions of source code must retain the above copyright notice,
7025
+ this list of conditions and the following disclaimer.
7026
+
7027
+ 2. Redistributions in binary form must reproduce the above copyright
7028
+ notice, this list of conditions and the following disclaimer in
7029
+ the documentation and/or other materials provided with the distribution.
7030
+
7031
+ 3. The names of the authors may not be used to endorse or promote products
7032
+ derived from this software without specific prior written permission.
7033
+
7034
+ THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
7035
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
7036
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
7037
+ INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
7038
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
7039
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
7040
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
7041
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
7042
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
7043
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
7044
+ */
7045
+
7046
+
6784
7047
  try {
6785
7048
  configure({ baseURI: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('zip-core.js', document.baseURI).href)) });
6786
7049
  } catch {
@@ -6837,7 +7100,9 @@
6837
7100
  exports.ZipWriter = ZipWriter;
6838
7101
  exports.ZipWriterStream = ZipWriterStream;
6839
7102
  exports.configure = configure;
7103
+ exports.createBlobTempStream = createBlobTempStream;
6840
7104
  exports.createOPFSTempStream = createOPFSTempStream;
7105
+ exports.createSyncAccessHandleTempStream = createSyncAccessHandleTempStream;
6841
7106
  exports.getMimeType = getMimeType;
6842
7107
  exports.terminateWorkers = terminateWorkers;
6843
7108