zstd-stream 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CrellinCreative
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,285 @@
1
+ <div align="center">
2
+
3
+ # zstd-stream
4
+
5
+ **High-performance Zstandard compression for Node.js and browsers with zero external dependencies**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/zstd-stream.svg)](https://www.npmjs.com/package/zstd-stream)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-brightgreen.svg)](https://www.typescriptlang.org/)
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ - 🚀 **Universal compatibility** - Works seamlessly in Node.js 18+ and modern browsers
18
+ - 📦 **Zero external assets** - All WebAssembly code bundled internally, works out of the box
19
+ - 🌊 **True streaming support** - Handle multi-GB files with minimal memory footprint
20
+ - ⚡ **Backpressure handling** - Efficient memory management prevents overflow
21
+ - 🎯 **Client-side optimization** - Reduce server load by compressing/decompressing in the browser
22
+ - 🔧 **ESM-first** - Modern ECMAScript modules with full TypeScript support
23
+ - 📊 **Progress tracking** - Monitor compression/decompression in real-time
24
+
25
+ Built on the latest Zstandard v2 compression algorithm, `zstd-stream` is one of the only packages that embeds all WASM code internally, eliminating asset management headaches and enabling true plug-and-play compression.
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install zstd-stream
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Usage Examples
38
+
39
+ ### Basic Text Compression
40
+
41
+ **⚠️ Note:** This method loads all data into memory. For large files (>100MB), use streaming instead.
42
+
43
+ ```typescript
44
+ import { compress, decompress } from "zstd-stream";
45
+
46
+ // Compress text
47
+ const text = "Hello, world!";
48
+ const input = new TextEncoder().encode(text);
49
+ const compressed = await compress(input, { level: 3 });
50
+
51
+ // Decompress
52
+ const decompressed = await decompress(compressed);
53
+ const output = new TextDecoder().decode(decompressed);
54
+
55
+ console.log(output); // "Hello, world!"
56
+ ```
57
+
58
+ ### Streaming Large Files (Recommended)
59
+
60
+ **✅ Use this for large files** - Processes data in chunks with constant memory usage.
61
+
62
+ ```typescript
63
+ import { compressStream, decompressStream } from "zstd-stream";
64
+
65
+ // Compress a stream
66
+ const textStream = new ReadableStream({
67
+ start(controller) {
68
+ controller.enqueue(new TextEncoder().encode("Large text data..."));
69
+ controller.close();
70
+ },
71
+ });
72
+
73
+ const compressedStream = await compressStream(textStream, { level: 5 });
74
+
75
+ // Decompress a stream
76
+ const decompressedStream = await decompressStream(compressedStream);
77
+
78
+ // Read the result
79
+ const reader = decompressedStream.getReader();
80
+ let result = "";
81
+
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done) break;
85
+ result += new TextDecoder().decode(value);
86
+ }
87
+
88
+ console.log(result);
89
+ ```
90
+
91
+ ### Browser: Download Compressed File with StreamSaver.js
92
+
93
+ **⚠️ Performance Note:** Handle compression via Web Workers for browser deployment to avoid degrading the application's performance!
94
+
95
+ ```typescript
96
+ import { compressStream } from "zstd-stream";
97
+ import streamSaver from "streamsaver";
98
+
99
+ // User selects a file
100
+ const file = document.querySelector('input[type="file"]').files[0];
101
+ const fileStream = file.stream();
102
+
103
+ // Compress the file
104
+ const compressed = await compressStream(fileStream, {
105
+ level: 9,
106
+ onProgress: (bytes) => {
107
+ console.log(`Compressed: ${(bytes / 1024 / 1024).toFixed(2)} MB`);
108
+ },
109
+ });
110
+
111
+ // Save to disk as .zst
112
+ const fileWriteStream = streamSaver.createWriteStream(`${file.name}.zst`);
113
+ const writer = fileWriteStream.getWriter();
114
+
115
+ const reader = compressed.getReader();
116
+ while (true) {
117
+ const { done, value } = await reader.read();
118
+ if (done) break;
119
+ await writer.write(value);
120
+ }
121
+
122
+ await writer.close();
123
+ ```
124
+
125
+ ### Stream File to Server via HTTP
126
+
127
+ ```typescript
128
+ import { compressStream } from "zstd-stream";
129
+
130
+ // Get file from user input
131
+ const file = document.querySelector('input[type="file"]').files[0];
132
+ const fileStream = file.stream();
133
+
134
+ // Compress and upload
135
+ const compressed = await compressStream(fileStream, { level: 6 });
136
+
137
+ const response = await fetch("https://api.example.com/upload", {
138
+ method: "POST",
139
+ headers: {
140
+ "Content-Type": "application/zstd",
141
+ "Content-Encoding": "zstd",
142
+ },
143
+ body: compressed,
144
+ duplex: "half", // Required for streaming request bodies
145
+ });
146
+
147
+ if (response.ok) {
148
+ console.log("Upload complete!");
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## API Reference
155
+
156
+ ### `compress(input, options?)`
157
+
158
+ Compress data in one operation. Best for small files.
159
+
160
+ **Parameters:**
161
+
162
+ - `input: Uint8Array` - Data to compress
163
+ - `options?: CompressOptions`
164
+ - `level?: number` - Compression level (1-19, default: 3)
165
+ - `onProgress?: (bytesWritten: number) => void` - Progress callback
166
+
167
+ **Returns:** `Promise<Uint8Array>`
168
+
169
+ ```typescript
170
+ const compressed = await compress(data, { level: 9 });
171
+ ```
172
+
173
+ ### `compressStream(input, options?)`
174
+
175
+ Compress a readable stream. Best for large files.
176
+
177
+ **Parameters:**
178
+
179
+ - `input: ReadableStream<Uint8Array>` - Stream to compress
180
+ - `options?: CompressOptions`
181
+ - `level?: number` - Compression level (1-19, default: 3)
182
+ - `onProgress?: (bytesWritten: number) => void` - Progress callback
183
+
184
+ **Returns:** `Promise<ReadableStream<Uint8Array>>`
185
+
186
+ ```typescript
187
+ const compressed = await compressStream(fileStream, { level: 5 });
188
+ ```
189
+
190
+ ### `decompress(input, options?)`
191
+
192
+ Decompress data in one operation.
193
+
194
+ **Parameters:**
195
+
196
+ - `input: Uint8Array` - Compressed data
197
+ - `options?: DecompressOptions`
198
+ - `onProgress?: (bytesWritten: number) => void` - Progress callback
199
+
200
+ **Returns:** `Promise<Uint8Array>`
201
+
202
+ ```typescript
203
+ const decompressed = await decompress(compressed);
204
+ ```
205
+
206
+ ### `decompressStream(input, options?)`
207
+
208
+ Decompress a readable stream with backpressure support.
209
+
210
+ **Parameters:**
211
+
212
+ - `input: ReadableStream<Uint8Array>` - Compressed stream
213
+ - `options?: DecompressOptions`
214
+ - `onProgress?: (bytesWritten: number) => void` - Progress callback
215
+
216
+ **Returns:** `Promise<ReadableStream<Uint8Array>>`
217
+
218
+ ```typescript
219
+ const decompressed = await decompressStream(compressedStream);
220
+ ```
221
+
222
+ ### `initialize()`
223
+
224
+ Pre-initialize the WASM module (optional). Call during app startup to avoid initialization delay on first use.
225
+
226
+ **Returns:** `Promise<void>`
227
+
228
+ ```typescript
229
+ await initialize();
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Compression Levels
235
+
236
+ Levels 1-19 are supported. Higher levels provide diminishing returns.
237
+
238
+ | Level | Speed | Ratio | Use Case |
239
+ | ----- | --------- | -------- | ---------------------------------- |
240
+ | 1-3 | Fast | Lower | Real-time, network streaming |
241
+ | 3-7 | Medium | Balanced | General purpose (recommended) |
242
+ | 8-15 | Slow | Better | File storage, archival |
243
+ | 16-19 | Very slow | Maximum | One-time compression, cold storage |
244
+
245
+ **Default level:** 3 (optimal balance of speed and compression)
246
+
247
+ ---
248
+
249
+ ## Browser Compatibility
250
+
251
+ - Chrome/Edge 80+
252
+ - Firefox 113+
253
+ - Safari 16.4+
254
+ - Node.js 18+
255
+
256
+ Requires WebAssembly and ES2022 support.
257
+
258
+ ---
259
+
260
+ ## TypeScript
261
+
262
+ Full type definitions included:
263
+
264
+ ```typescript
265
+ import type { CompressOptions, DecompressOptions } from "zstd-stream";
266
+
267
+ const options: CompressOptions = {
268
+ level: 9,
269
+ onProgress: (bytes) => console.log(`Progress: ${bytes}`),
270
+ };
271
+ ```
272
+
273
+ ---
274
+
275
+ ## License
276
+
277
+ MIT
278
+
279
+ ---
280
+
281
+ ## Credits
282
+
283
+ Built with [Zstandard](https://github.com/facebook/zstd) by Meta, compiled using [Emscripten SDK](https://emscripten.org/).
284
+
285
+ WebAssembly module embedded internally for zero-dependency deployment.
@@ -0,0 +1,27 @@
1
+ import type { Module } from "./types.js";
2
+ declare abstract class BaseProcessor {
3
+ protected destroyed: boolean;
4
+ protected module: Module | null;
5
+ destroy(): void;
6
+ protected abstract cleanup(): void;
7
+ }
8
+ export declare class ZstdCompressor extends BaseProcessor {
9
+ private level;
10
+ private ctx;
11
+ private buffer;
12
+ private bufferSize;
13
+ constructor(level: number);
14
+ init(): Promise<void>;
15
+ process(data: Uint8Array, isLast: boolean): Uint8Array;
16
+ protected cleanup(): void;
17
+ }
18
+ export declare class ZstdDecompressor extends BaseProcessor {
19
+ private ctx;
20
+ private buffer;
21
+ private bufferSize;
22
+ init(): Promise<void>;
23
+ process(data: Uint8Array): Uint8Array;
24
+ private concat;
25
+ protected cleanup(): void;
26
+ }
27
+ export {};
@@ -0,0 +1,155 @@
1
+ import { getModule } from "./loader.js";
2
+ class BaseProcessor {
3
+ destroyed = false;
4
+ module = null;
5
+ destroy() {
6
+ if (!this.destroyed) {
7
+ this.cleanup();
8
+ this.destroyed = true;
9
+ }
10
+ }
11
+ }
12
+ export class ZstdCompressor extends BaseProcessor {
13
+ level;
14
+ ctx = 0;
15
+ buffer = 0;
16
+ bufferSize = 0;
17
+ constructor(level) {
18
+ super();
19
+ this.level = level;
20
+ if (level < 1 || level > 19) {
21
+ throw new Error("Compression level must be between 1 and 19");
22
+ }
23
+ }
24
+ async init() {
25
+ this.module = getModule();
26
+ }
27
+ process(data, isLast) {
28
+ if (!this.module)
29
+ throw new Error("Not initialized");
30
+ if (this.destroyed)
31
+ throw new Error("Destroyed");
32
+ if (!this.ctx) {
33
+ this.ctx = this.module._createCCtx();
34
+ if (!this.ctx || this.module._initCStream(this.ctx, this.level) !== 0) {
35
+ this.cleanup();
36
+ throw new Error("Failed to create compression context");
37
+ }
38
+ }
39
+ if (!data.length && !isLast)
40
+ return new Uint8Array(0);
41
+ const srcPtr = this.module._malloc(data.length);
42
+ if (!srcPtr)
43
+ throw new Error("Failed to allocate source buffer");
44
+ this.module.HEAPU8.set(data, srcPtr);
45
+ try {
46
+ if (!this.buffer) {
47
+ this.bufferSize = this.module._cStreamOutSize();
48
+ this.buffer = this.module._malloc(this.bufferSize);
49
+ if (!this.buffer)
50
+ throw new Error("Failed to allocate output buffer");
51
+ }
52
+ const result = Number(this.module._compressStream(this.ctx, this.buffer, this.bufferSize, srcPtr, data.length, isLast ? 2 : 0));
53
+ if (result < 0) {
54
+ throw new Error(`Compression failed: ${this.module._getErrorName(-result)}`);
55
+ }
56
+ return result === 0
57
+ ? new Uint8Array(0)
58
+ : new Uint8Array(this.module.HEAPU8.subarray(this.buffer, this.buffer + result));
59
+ }
60
+ finally {
61
+ this.module._free(srcPtr);
62
+ }
63
+ }
64
+ cleanup() {
65
+ if (this.module) {
66
+ if (this.ctx)
67
+ this.module._freeCCtx(this.ctx);
68
+ if (this.buffer)
69
+ this.module._free(this.buffer);
70
+ this.ctx = 0;
71
+ this.buffer = 0;
72
+ }
73
+ }
74
+ }
75
+ export class ZstdDecompressor extends BaseProcessor {
76
+ ctx = 0;
77
+ buffer = 0;
78
+ bufferSize = 0;
79
+ async init() {
80
+ this.module = getModule();
81
+ }
82
+ process(data) {
83
+ if (!this.module)
84
+ throw new Error("Not initialized");
85
+ if (this.destroyed)
86
+ throw new Error("Destroyed");
87
+ if (!this.ctx) {
88
+ this.ctx = this.module._createDCtx();
89
+ if (!this.ctx || this.module._initDStream(this.ctx) === 0) {
90
+ this.cleanup();
91
+ throw new Error("Failed to create decompression context");
92
+ }
93
+ }
94
+ if (!data.length)
95
+ return new Uint8Array(0);
96
+ if (!this.buffer) {
97
+ this.bufferSize = this.module._dStreamOutSize();
98
+ this.buffer = this.module._malloc(this.bufferSize);
99
+ if (!this.buffer)
100
+ throw new Error("Failed to allocate output buffer");
101
+ }
102
+ const srcPtr = this.module._malloc(data.length);
103
+ if (!srcPtr)
104
+ throw new Error("Failed to allocate source buffer");
105
+ this.module.HEAPU8.set(data, srcPtr);
106
+ try {
107
+ let srcPos = 0;
108
+ const chunks = [];
109
+ while (srcPos < data.length) {
110
+ const result = this.module._decompressStream(this.ctx, this.buffer, this.bufferSize, srcPtr + srcPos, data.length - srcPos);
111
+ if (result < 0) {
112
+ const errorCode = Number(BigInt(result) & 0x7fffffffffffffffn);
113
+ throw new Error(`Decompression failed: ${this.module._getErrorName(errorCode)}`);
114
+ }
115
+ const consumed = Number(BigInt(result) >> 32n);
116
+ const outputSize = Number(BigInt(result) & 0xffffffffn);
117
+ if (consumed === 0 && outputSize === 0) {
118
+ throw new Error("Decompression stalled");
119
+ }
120
+ if (outputSize > 0) {
121
+ chunks.push(new Uint8Array(this.module.HEAPU8.subarray(this.buffer, this.buffer + outputSize)));
122
+ }
123
+ srcPos += consumed;
124
+ }
125
+ return this.concat(chunks);
126
+ }
127
+ finally {
128
+ this.module._free(srcPtr);
129
+ }
130
+ }
131
+ concat(chunks) {
132
+ if (chunks.length === 0)
133
+ return new Uint8Array(0);
134
+ if (chunks.length === 1)
135
+ return chunks[0];
136
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
137
+ const result = new Uint8Array(total);
138
+ let offset = 0;
139
+ for (const chunk of chunks) {
140
+ result.set(chunk, offset);
141
+ offset += chunk.length;
142
+ }
143
+ return result;
144
+ }
145
+ cleanup() {
146
+ if (this.module) {
147
+ if (this.ctx)
148
+ this.module._freeDCtx(this.ctx);
149
+ if (this.buffer)
150
+ this.module._free(this.buffer);
151
+ this.ctx = 0;
152
+ this.buffer = 0;
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,12 @@
1
+ export interface CompressOptions {
2
+ level?: number;
3
+ onProgress?: (bytesWritten: number) => void;
4
+ }
5
+ export interface DecompressOptions {
6
+ onProgress?: (bytesWritten: number) => void;
7
+ }
8
+ export declare function compress(input: Uint8Array, options?: CompressOptions): Promise<Uint8Array>;
9
+ export declare function compressStream(input: ReadableStream<Uint8Array>, options?: CompressOptions): Promise<ReadableStream<Uint8Array>>;
10
+ export declare function decompress(input: Uint8Array, options?: DecompressOptions): Promise<Uint8Array>;
11
+ export declare function decompressStream(input: ReadableStream<Uint8Array>, options?: DecompressOptions): Promise<ReadableStream<Uint8Array>>;
12
+ export declare function initialize(): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ import { ZstdCompressor, ZstdDecompressor } from "./compressor.js";
2
+ import { initZstd } from "./loader.js";
3
+ let initialized = false;
4
+ async function ensureInit() {
5
+ if (!initialized) {
6
+ await initZstd();
7
+ initialized = true;
8
+ }
9
+ }
10
+ // Compress Uint8Array -> Uint8Array
11
+ export async function compress(input, options) {
12
+ await ensureInit();
13
+ const { level = 3, onProgress } = options || {};
14
+ const compressor = new ZstdCompressor(level);
15
+ await compressor.init();
16
+ try {
17
+ const result = compressor.process(input, true);
18
+ onProgress?.(result.length);
19
+ return result;
20
+ }
21
+ finally {
22
+ compressor.destroy();
23
+ }
24
+ }
25
+ export async function compressStream(input, options) {
26
+ await ensureInit();
27
+ const { level = 3, onProgress } = options || {};
28
+ const reader = input.getReader();
29
+ let compressor = null;
30
+ let totalWritten = 0;
31
+ let inputDone = false;
32
+ return new ReadableStream({
33
+ async start() {
34
+ compressor = new ZstdCompressor(level);
35
+ await compressor.init();
36
+ },
37
+ async pull(controller) {
38
+ try {
39
+ if (inputDone)
40
+ return;
41
+ let loopIterations = 0;
42
+ // Keep reading until we have data to enqueue or input is exhausted
43
+ while (!inputDone) {
44
+ loopIterations++;
45
+ const { done, value } = await reader.read();
46
+ if (done) {
47
+ // Final flush
48
+ const chunk = compressor.process(new Uint8Array(0), true);
49
+ if (chunk.length > 0) {
50
+ controller.enqueue(chunk);
51
+ totalWritten += chunk.length;
52
+ onProgress?.(totalWritten);
53
+ }
54
+ controller.close();
55
+ reader.releaseLock();
56
+ compressor?.destroy();
57
+ inputDone = true;
58
+ return;
59
+ }
60
+ // Process chunk
61
+ const chunk = compressor.process(value, false);
62
+ if (chunk.length > 0) {
63
+ controller.enqueue(chunk);
64
+ totalWritten += chunk.length;
65
+ onProgress?.(totalWritten);
66
+ return; // Exit after enqueuing data
67
+ }
68
+ // If chunk is empty, continue loop to read next input chunk
69
+ }
70
+ }
71
+ catch (error) {
72
+ inputDone = true;
73
+ reader.releaseLock();
74
+ compressor?.destroy();
75
+ controller.error(error);
76
+ }
77
+ },
78
+ cancel() {
79
+ reader.releaseLock();
80
+ compressor?.destroy();
81
+ },
82
+ });
83
+ }
84
+ // Decompress Uint8Array -> Uint8Array
85
+ export async function decompress(input, options) {
86
+ await ensureInit();
87
+ const { onProgress } = options || {};
88
+ const decompressor = new ZstdDecompressor();
89
+ await decompressor.init();
90
+ try {
91
+ const result = decompressor.process(input);
92
+ onProgress?.(result.length);
93
+ return result;
94
+ }
95
+ finally {
96
+ decompressor.destroy();
97
+ }
98
+ }
99
+ // Decompress ReadableStream -> ReadableStream with backpressure
100
+ export async function decompressStream(input, options) {
101
+ await ensureInit();
102
+ const { onProgress } = options || {};
103
+ const reader = input.getReader();
104
+ let decompressor = null;
105
+ let totalWritten = 0;
106
+ let inputDone = false;
107
+ return new ReadableStream({
108
+ async start() {
109
+ decompressor = new ZstdDecompressor();
110
+ await decompressor.init();
111
+ },
112
+ async pull(controller) {
113
+ try {
114
+ if (inputDone) {
115
+ return; // Already finished
116
+ }
117
+ const { done, value } = await reader.read();
118
+ if (done) {
119
+ inputDone = true;
120
+ controller.close();
121
+ reader.releaseLock();
122
+ decompressor?.destroy();
123
+ return;
124
+ }
125
+ const chunk = decompressor.process(value);
126
+ if (chunk.length > 0) {
127
+ controller.enqueue(chunk);
128
+ totalWritten += chunk.length;
129
+ onProgress?.(totalWritten);
130
+ }
131
+ }
132
+ catch (error) {
133
+ inputDone = true;
134
+ reader.releaseLock();
135
+ decompressor?.destroy();
136
+ controller.error(error);
137
+ throw error;
138
+ }
139
+ },
140
+ cancel() {
141
+ reader.releaseLock();
142
+ decompressor?.destroy();
143
+ },
144
+ });
145
+ }
146
+ export async function initialize() {
147
+ await ensureInit();
148
+ }
package/dist/lib.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const lib: string;