zstd-stream 1.0.0 → 1.1.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/README.md +273 -285
- package/dist/compressor.d.ts +59 -6
- package/dist/compressor.js +219 -105
- package/dist/index.d.ts +7 -0
- package/dist/index.js +90 -68
- package/dist/loader.d.ts +1 -1
- package/dist/loader.js +3 -6
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,285 +1,273 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# zstd-stream
|
|
4
|
-
|
|
5
|
-
**
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/zstd-stream)
|
|
8
|
-
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
[](https://www.typescriptlang.org/)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
await
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const compressed = await
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
### `
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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.
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# zstd-stream
|
|
4
|
+
|
|
5
|
+
**Streaming Zstandard compression for Node.js and browsers — zero external dependencies, bounded memory.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/zstd-stream)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://crellincreative.github.io/zstd-stream/)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
`zstd-stream` compresses and decompresses [Zstandard](https://github.com/facebook/zstd)
|
|
17
|
+
data through the Web Streams API. It streams files larger than available RAM in
|
|
18
|
+
**constant memory**, propagates **backpressure** end to end, and ships the
|
|
19
|
+
WebAssembly build **embedded** — no `.wasm` asset to host or configure.
|
|
20
|
+
|
|
21
|
+
- 🌊 **Streaming first** — process multi-GB data in fixed ~128 KB buffers
|
|
22
|
+
- ⚡ **Backpressure built in** — a slow writer automatically throttles the reader, so memory never runs away
|
|
23
|
+
- 📦 **Zero external assets** — WASM is bundled in; `npm install` and import
|
|
24
|
+
- 🌍 **Universal** — the same code runs in Node.js 18+ and modern browsers
|
|
25
|
+
- 🔧 **ESM + TypeScript** — full type definitions included
|
|
26
|
+
- 📊 **Progress callbacks** — observe bytes processed in real time
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install zstd-stream
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
Everything operates on `ReadableStream<Uint8Array>` — the same stream type
|
|
41
|
+
returned by `fetch`, `Blob.stream()`, `File.stream()`, and Node's
|
|
42
|
+
`Readable.toWeb()`.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { compressStream, decompressStream } from "zstd-stream";
|
|
46
|
+
|
|
47
|
+
// Compress any byte stream...
|
|
48
|
+
const compressed = await compressStream(source, { level: 3 });
|
|
49
|
+
|
|
50
|
+
// ...and decompress it back.
|
|
51
|
+
const restored = await decompressStream(compressed);
|
|
52
|
+
|
|
53
|
+
// Collect a stream into bytes (or use .text() for strings):
|
|
54
|
+
const bytes = new Uint8Array(await new Response(restored).arrayBuffer());
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Prefer streaming wherever the data is large or arrives incrementally. For small,
|
|
58
|
+
fully in-memory buffers there are one-shot [`compress`](#compressinput-options) /
|
|
59
|
+
[`decompress`](#decompressinput-options) helpers.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Recipes
|
|
64
|
+
|
|
65
|
+
### Compress a file and upload it
|
|
66
|
+
|
|
67
|
+
Pipe the compressed stream straight into a `fetch` request body — nothing is
|
|
68
|
+
buffered in full.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { compressStream } from "zstd-stream";
|
|
72
|
+
|
|
73
|
+
const file = document.querySelector<HTMLInputElement>("input[type=file]")!.files![0];
|
|
74
|
+
const compressed = await compressStream(file.stream(), { level: 6 });
|
|
75
|
+
|
|
76
|
+
await fetch("/upload", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Encoding": "zstd" },
|
|
79
|
+
body: compressed,
|
|
80
|
+
duplex: "half", // required when streaming a request body
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Download and decompress
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { decompressStream } from "zstd-stream";
|
|
88
|
+
|
|
89
|
+
const res = await fetch("/data.zst");
|
|
90
|
+
const decompressed = await decompressStream(res.body!);
|
|
91
|
+
|
|
92
|
+
const text = await new Response(decompressed).text();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Save a compressed file to disk (browser)
|
|
96
|
+
|
|
97
|
+
`pipeTo` drives the whole transfer and applies backpressure for you.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { compressStream } from "zstd-stream";
|
|
101
|
+
import streamSaver from "streamsaver";
|
|
102
|
+
|
|
103
|
+
const compressed = await compressStream(file.stream(), {
|
|
104
|
+
level: 9,
|
|
105
|
+
onProgress: (bytes) => console.log(`${(bytes / 1e6).toFixed(1)} MB written`),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await compressed.pipeTo(streamSaver.createWriteStream(`${file.name}.zst`));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **Tip:** compression is CPU-intensive. In the browser, run it inside a Web
|
|
112
|
+
> Worker to keep the UI responsive.
|
|
113
|
+
|
|
114
|
+
### Compress a file on disk (Node.js)
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
118
|
+
import { Readable, Writable } from "node:stream";
|
|
119
|
+
import { compressStream } from "zstd-stream";
|
|
120
|
+
|
|
121
|
+
const source = Readable.toWeb(createReadStream("big.log")) as ReadableStream<Uint8Array>;
|
|
122
|
+
const compressed = await compressStream(source, { level: 6 });
|
|
123
|
+
|
|
124
|
+
await compressed.pipeTo(Writable.toWeb(createWriteStream("big.log.zst")));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Small, in-memory data
|
|
128
|
+
|
|
129
|
+
When you already hold the whole payload, skip the streams:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { compress, decompress } from "zstd-stream";
|
|
133
|
+
|
|
134
|
+
const data = new TextEncoder().encode("Hello, world!");
|
|
135
|
+
const compressed = await compress(data, { level: 3 });
|
|
136
|
+
const restored = await decompress(compressed);
|
|
137
|
+
|
|
138
|
+
console.log(new TextDecoder().decode(restored)); // "Hello, world!"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Memory & backpressure
|
|
144
|
+
|
|
145
|
+
The streaming API is built to handle data far larger than available RAM:
|
|
146
|
+
|
|
147
|
+
- **Bounded memory** — data flows through fixed ~128 KB working buffers and is
|
|
148
|
+
emitted one slice at a time. A compressed chunk that expands to gigabytes is
|
|
149
|
+
never decoded into a single allocation.
|
|
150
|
+
- **Backpressure** — output is produced lazily, one slice per read, so a slow
|
|
151
|
+
consumer (disk, network) throttles reads from the source. A fast producer
|
|
152
|
+
cannot outrun a slow consumer and pile up in memory.
|
|
153
|
+
- **Tunable buffering** — `highWaterMark` (bytes) sets how much output may queue
|
|
154
|
+
before reads pause. Larger values trade memory for throughput.
|
|
155
|
+
|
|
156
|
+
The one-shot `compress` / `decompress` helpers, by contrast, hold the entire
|
|
157
|
+
input and output in memory — use them only for small data.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## API reference
|
|
162
|
+
|
|
163
|
+
All functions are async and lazily initialize the WASM module on first use.
|
|
164
|
+
|
|
165
|
+
### `compressStream(input, options?)`
|
|
166
|
+
|
|
167
|
+
Compress a stream. The primary API.
|
|
168
|
+
|
|
169
|
+
- `input: ReadableStream<Uint8Array>`
|
|
170
|
+
- `options?: CompressOptions`
|
|
171
|
+
- `level?: number` — compression level 1–19 (default: `3`)
|
|
172
|
+
- `onProgress?: (bytesWritten: number) => void` — cumulative compressed bytes
|
|
173
|
+
- `highWaterMark?: number` — output bytes buffered before backpressure pauses the source (default: 1 MiB)
|
|
174
|
+
|
|
175
|
+
**Returns:** `Promise<ReadableStream<Uint8Array>>`
|
|
176
|
+
|
|
177
|
+
### `decompressStream(input, options?)`
|
|
178
|
+
|
|
179
|
+
Decompress a stream.
|
|
180
|
+
|
|
181
|
+
- `input: ReadableStream<Uint8Array>`
|
|
182
|
+
- `options?: DecompressOptions`
|
|
183
|
+
- `onProgress?: (bytesWritten: number) => void` — cumulative decompressed bytes
|
|
184
|
+
- `highWaterMark?: number` — output bytes buffered before backpressure pauses the source (default: 1 MiB)
|
|
185
|
+
|
|
186
|
+
**Returns:** `Promise<ReadableStream<Uint8Array>>`
|
|
187
|
+
|
|
188
|
+
### `compress(input, options?)`
|
|
189
|
+
|
|
190
|
+
One-shot compression of an in-memory buffer. Holds all data in memory.
|
|
191
|
+
|
|
192
|
+
- `input: Uint8Array`
|
|
193
|
+
- `options?: CompressOptions` — `level`, `onProgress` (as above)
|
|
194
|
+
|
|
195
|
+
**Returns:** `Promise<Uint8Array>`
|
|
196
|
+
|
|
197
|
+
### `decompress(input, options?)`
|
|
198
|
+
|
|
199
|
+
One-shot decompression of an in-memory buffer. Holds all data in memory.
|
|
200
|
+
|
|
201
|
+
- `input: Uint8Array`
|
|
202
|
+
- `options?: DecompressOptions` — `onProgress` (as above)
|
|
203
|
+
|
|
204
|
+
**Returns:** `Promise<Uint8Array>`
|
|
205
|
+
|
|
206
|
+
### `initialize()`
|
|
207
|
+
|
|
208
|
+
Optional. Pre-loads the WASM module so the first compress/decompress call has no
|
|
209
|
+
startup cost. Safe to call multiple times.
|
|
210
|
+
|
|
211
|
+
**Returns:** `Promise<void>`
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { initialize } from "zstd-stream";
|
|
215
|
+
|
|
216
|
+
await initialize(); // e.g. during app startup
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Compression levels
|
|
222
|
+
|
|
223
|
+
Levels **1–19** are supported (default: `3`). Higher levels compress more but
|
|
224
|
+
cost more time and memory; "ultra" levels 20–22 are intentionally excluded
|
|
225
|
+
because, in streaming mode, they force every decompressor to allocate a 128 MB
|
|
226
|
+
window — even for tiny inputs.
|
|
227
|
+
|
|
228
|
+
Out-of-range values are clamped to the nearest valid level (with a
|
|
229
|
+
`console.warn`) rather than throwing — e.g. `level: 22` runs as `19`.
|
|
230
|
+
|
|
231
|
+
| Level | Speed | Ratio | Typical use |
|
|
232
|
+
| ----- | --------- | -------- | ----------------------------- |
|
|
233
|
+
| 1–3 | Fast | Lower | Real-time, network streaming |
|
|
234
|
+
| 4–7 | Balanced | Good | General purpose (recommended) |
|
|
235
|
+
| 8–15 | Slow | Better | File storage, archival |
|
|
236
|
+
| 16–19 | Very slow | Maximum | One-time / cold storage |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## TypeScript
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import type { CompressOptions, DecompressOptions } from "zstd-stream";
|
|
244
|
+
|
|
245
|
+
const options: CompressOptions = {
|
|
246
|
+
level: 9,
|
|
247
|
+
highWaterMark: 4 * 1024 * 1024,
|
|
248
|
+
onProgress: (bytes) => console.log(`Progress: ${bytes}`),
|
|
249
|
+
};
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Compatibility
|
|
255
|
+
|
|
256
|
+
| Environment | Minimum |
|
|
257
|
+
| ----------- | ------- |
|
|
258
|
+
| Node.js | 18 |
|
|
259
|
+
| Chrome/Edge | 80 |
|
|
260
|
+
| Firefox | 113 |
|
|
261
|
+
| Safari | 16.4 |
|
|
262
|
+
|
|
263
|
+
Requires WebAssembly and ES2022 support.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|
|
270
|
+
|
|
271
|
+
Built with [Zstandard](https://github.com/facebook/zstd) by Meta, compiled with
|
|
272
|
+
the [Emscripten SDK](https://emscripten.org/) and embedded for zero-dependency
|
|
273
|
+
deployment.
|
package/dist/compressor.d.ts
CHANGED
|
@@ -5,23 +5,76 @@ declare abstract class BaseProcessor {
|
|
|
5
5
|
destroy(): void;
|
|
6
6
|
protected abstract cleanup(): void;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Incremental Zstandard compressor.
|
|
10
|
+
*
|
|
11
|
+
* The engine works on two fixed WASM buffers: an input buffer sized to
|
|
12
|
+
* `_cStreamInSize()` and an output buffer sized to `_cStreamOutSize()` (which
|
|
13
|
+
* zstd guarantees is large enough to flush one full block). Feeding no more
|
|
14
|
+
* than `inputBufferSize` bytes per call keeps every call's output within a
|
|
15
|
+
* single output buffer, so memory never scales with the total stream length.
|
|
16
|
+
*/
|
|
8
17
|
export declare class ZstdCompressor extends BaseProcessor {
|
|
9
|
-
private level;
|
|
10
18
|
private ctx;
|
|
11
|
-
private
|
|
12
|
-
private
|
|
19
|
+
private inBuffer;
|
|
20
|
+
private outBuffer;
|
|
21
|
+
private outBufferSize;
|
|
22
|
+
inputBufferSize: number;
|
|
23
|
+
private readonly level;
|
|
13
24
|
constructor(level: number);
|
|
14
25
|
init(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Compress one input slice (`length` must be <= `inputBufferSize`). The
|
|
28
|
+
* slice is fully consumed; returns the bytes produced this call, which may
|
|
29
|
+
* be empty when the engine is still buffering toward a block boundary.
|
|
30
|
+
*/
|
|
31
|
+
compressStep(data: Uint8Array, offset: number, length: number): Uint8Array;
|
|
32
|
+
/**
|
|
33
|
+
* Emit one output buffer of the closing frame. `done` is true once the frame
|
|
34
|
+
* has been fully flushed (i.e. the engine returned less than a full buffer).
|
|
35
|
+
*/
|
|
36
|
+
compressFinish(): {
|
|
37
|
+
output: Uint8Array;
|
|
38
|
+
done: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** Single-shot compression of an in-memory buffer. */
|
|
15
41
|
process(data: Uint8Array, isLast: boolean): Uint8Array;
|
|
42
|
+
/** Run one `_compressStream` call and copy out any produced bytes. */
|
|
43
|
+
private run;
|
|
44
|
+
private requireModule;
|
|
16
45
|
protected cleanup(): void;
|
|
17
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Incremental Zstandard decompressor.
|
|
49
|
+
*
|
|
50
|
+
* Input is loaded into a fixed WASM buffer one `inputBufferSize`-sized slice at
|
|
51
|
+
* a time; `decompressStep()` then drains it one output buffer at a time. A
|
|
52
|
+
* single compressed slice can expand to far more than memory, so the caller is
|
|
53
|
+
* expected to enqueue each produced slice before requesting the next one.
|
|
54
|
+
*/
|
|
18
55
|
export declare class ZstdDecompressor extends BaseProcessor {
|
|
19
56
|
private ctx;
|
|
20
|
-
private
|
|
21
|
-
private
|
|
57
|
+
private inBuffer;
|
|
58
|
+
private outBuffer;
|
|
59
|
+
private outBufferSize;
|
|
60
|
+
inputBufferSize: number;
|
|
61
|
+
private inLen;
|
|
62
|
+
private inPos;
|
|
22
63
|
init(): Promise<void>;
|
|
64
|
+
/** Load one input slice (`length` must be <= `inputBufferSize`) for decoding. */
|
|
65
|
+
setInput(data: Uint8Array, offset: number, length: number): void;
|
|
66
|
+
/** True while the resident input slice still has bytes to decode. */
|
|
67
|
+
hasInput(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Decode one output buffer's worth of data from the resident input slice.
|
|
70
|
+
* Returns the produced bytes (possibly empty). Advances the internal input
|
|
71
|
+
* position; throws if the engine can neither consume nor produce (corrupt
|
|
72
|
+
* input).
|
|
73
|
+
*/
|
|
74
|
+
decompressStep(): Uint8Array;
|
|
75
|
+
/** Single-shot decompression of an in-memory buffer. */
|
|
23
76
|
process(data: Uint8Array): Uint8Array;
|
|
24
|
-
private
|
|
77
|
+
private requireModule;
|
|
25
78
|
protected cleanup(): void;
|
|
26
79
|
}
|
|
27
80
|
export {};
|
package/dist/compressor.js
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
import { getModule } from "./loader.js";
|
|
2
|
+
const EMPTY = new Uint8Array(0);
|
|
3
|
+
// ZSTD operation modes for the streaming wrapper.
|
|
4
|
+
const ZSTD_E_CONTINUE = 0;
|
|
5
|
+
const ZSTD_E_END = 2;
|
|
6
|
+
// Supported compression levels. 20-22 ("ultra") are excluded: in streaming mode
|
|
7
|
+
// they pin the window to 128 MB, forcing every decompressor to allocate 128 MB
|
|
8
|
+
// even for tiny files, and cost hundreds of MB to compress.
|
|
9
|
+
const MIN_LEVEL = 1;
|
|
10
|
+
const MAX_LEVEL = 19;
|
|
11
|
+
const DEFAULT_LEVEL = 3;
|
|
12
|
+
/**
|
|
13
|
+
* Coerce a requested level into the supported 1-19 range, warning (rather than
|
|
14
|
+
* throwing) so a misconfigured caller still gets a working stream.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeLevel(level) {
|
|
17
|
+
if (!Number.isFinite(level)) {
|
|
18
|
+
console.warn(`zstd-stream: invalid compression level ${level}; using default ${DEFAULT_LEVEL}.`);
|
|
19
|
+
return DEFAULT_LEVEL;
|
|
20
|
+
}
|
|
21
|
+
const valid = Math.min(MAX_LEVEL, Math.max(MIN_LEVEL, Math.round(level)));
|
|
22
|
+
if (valid !== level) {
|
|
23
|
+
console.warn(`zstd-stream: compression level ${level} is outside ${MIN_LEVEL}-${MAX_LEVEL}; using ${valid}.`);
|
|
24
|
+
}
|
|
25
|
+
return valid;
|
|
26
|
+
}
|
|
27
|
+
/** Join an array of chunks into a single contiguous Uint8Array. */
|
|
28
|
+
function concat(chunks) {
|
|
29
|
+
if (chunks.length === 0)
|
|
30
|
+
return EMPTY;
|
|
31
|
+
if (chunks.length === 1)
|
|
32
|
+
return chunks[0];
|
|
33
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
34
|
+
const result = new Uint8Array(total);
|
|
35
|
+
let offset = 0;
|
|
36
|
+
for (const chunk of chunks) {
|
|
37
|
+
result.set(chunk, offset);
|
|
38
|
+
offset += chunk.length;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
2
42
|
class BaseProcessor {
|
|
3
43
|
destroyed = false;
|
|
4
44
|
module = null;
|
|
@@ -9,147 +49,221 @@ class BaseProcessor {
|
|
|
9
49
|
}
|
|
10
50
|
}
|
|
11
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Incremental Zstandard compressor.
|
|
54
|
+
*
|
|
55
|
+
* The engine works on two fixed WASM buffers: an input buffer sized to
|
|
56
|
+
* `_cStreamInSize()` and an output buffer sized to `_cStreamOutSize()` (which
|
|
57
|
+
* zstd guarantees is large enough to flush one full block). Feeding no more
|
|
58
|
+
* than `inputBufferSize` bytes per call keeps every call's output within a
|
|
59
|
+
* single output buffer, so memory never scales with the total stream length.
|
|
60
|
+
*/
|
|
12
61
|
export class ZstdCompressor extends BaseProcessor {
|
|
13
|
-
level;
|
|
14
62
|
ctx = 0;
|
|
15
|
-
|
|
16
|
-
|
|
63
|
+
inBuffer = 0;
|
|
64
|
+
outBuffer = 0;
|
|
65
|
+
outBufferSize = 0;
|
|
66
|
+
inputBufferSize = 0;
|
|
67
|
+
level;
|
|
17
68
|
constructor(level) {
|
|
18
69
|
super();
|
|
19
|
-
this.level = level;
|
|
20
|
-
if (level < 1 || level > 19) {
|
|
21
|
-
throw new Error("Compression level must be between 1 and 19");
|
|
22
|
-
}
|
|
70
|
+
this.level = normalizeLevel(level);
|
|
23
71
|
}
|
|
24
72
|
async init() {
|
|
25
|
-
this.module = getModule();
|
|
73
|
+
const m = (this.module = getModule());
|
|
74
|
+
this.ctx = m._createCCtx();
|
|
75
|
+
if (!this.ctx || m._initCStream(this.ctx, this.level) !== 0) {
|
|
76
|
+
this.cleanup();
|
|
77
|
+
throw new Error("Failed to create compression context");
|
|
78
|
+
}
|
|
79
|
+
this.outBufferSize = m._cStreamOutSize();
|
|
80
|
+
this.inputBufferSize = m._cStreamInSize();
|
|
81
|
+
this.outBuffer = m._malloc(this.outBufferSize);
|
|
82
|
+
this.inBuffer = m._malloc(this.inputBufferSize);
|
|
83
|
+
if (!this.outBuffer || !this.inBuffer) {
|
|
84
|
+
this.cleanup();
|
|
85
|
+
throw new Error("Failed to allocate compression buffers");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Compress one input slice (`length` must be <= `inputBufferSize`). The
|
|
90
|
+
* slice is fully consumed; returns the bytes produced this call, which may
|
|
91
|
+
* be empty when the engine is still buffering toward a block boundary.
|
|
92
|
+
*/
|
|
93
|
+
compressStep(data, offset, length) {
|
|
94
|
+
const m = this.requireModule();
|
|
95
|
+
m.HEAPU8.set(data.subarray(offset, offset + length), this.inBuffer);
|
|
96
|
+
return this.run(length, ZSTD_E_CONTINUE);
|
|
26
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Emit one output buffer of the closing frame. `done` is true once the frame
|
|
100
|
+
* has been fully flushed (i.e. the engine returned less than a full buffer).
|
|
101
|
+
*/
|
|
102
|
+
compressFinish() {
|
|
103
|
+
this.requireModule();
|
|
104
|
+
const output = this.run(0, ZSTD_E_END);
|
|
105
|
+
return { output, done: output.length < this.outBufferSize };
|
|
106
|
+
}
|
|
107
|
+
/** Single-shot compression of an in-memory buffer. */
|
|
27
108
|
process(data, isLast) {
|
|
109
|
+
this.requireModule();
|
|
110
|
+
if (!data.length && !isLast)
|
|
111
|
+
return EMPTY;
|
|
112
|
+
const chunks = [];
|
|
113
|
+
let pos = 0;
|
|
114
|
+
while (pos < data.length) {
|
|
115
|
+
const n = Math.min(this.inputBufferSize, data.length - pos);
|
|
116
|
+
const out = this.compressStep(data, pos, n);
|
|
117
|
+
if (out.length)
|
|
118
|
+
chunks.push(out);
|
|
119
|
+
pos += n;
|
|
120
|
+
}
|
|
121
|
+
if (isLast) {
|
|
122
|
+
let result;
|
|
123
|
+
do {
|
|
124
|
+
result = this.compressFinish();
|
|
125
|
+
if (result.output.length)
|
|
126
|
+
chunks.push(result.output);
|
|
127
|
+
} while (!result.done);
|
|
128
|
+
}
|
|
129
|
+
return concat(chunks);
|
|
130
|
+
}
|
|
131
|
+
/** Run one `_compressStream` call and copy out any produced bytes. */
|
|
132
|
+
run(length, mode) {
|
|
133
|
+
const m = this.module;
|
|
134
|
+
const result = Number(m._compressStream(this.ctx, this.outBuffer, this.outBufferSize, this.inBuffer, length, mode));
|
|
135
|
+
if (result < 0) {
|
|
136
|
+
throw new Error(`Compression failed: ${m._getErrorName(-result)}`);
|
|
137
|
+
}
|
|
138
|
+
return result === 0
|
|
139
|
+
? EMPTY
|
|
140
|
+
: new Uint8Array(m.HEAPU8.subarray(this.outBuffer, this.outBuffer + result));
|
|
141
|
+
}
|
|
142
|
+
requireModule() {
|
|
28
143
|
if (!this.module)
|
|
29
144
|
throw new Error("Not initialized");
|
|
30
145
|
if (this.destroyed)
|
|
31
146
|
throw new Error("Destroyed");
|
|
32
|
-
|
|
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
|
-
}
|
|
147
|
+
return this.module;
|
|
63
148
|
}
|
|
64
149
|
cleanup() {
|
|
65
150
|
if (this.module) {
|
|
66
151
|
if (this.ctx)
|
|
67
152
|
this.module._freeCCtx(this.ctx);
|
|
68
|
-
if (this.
|
|
69
|
-
this.module._free(this.
|
|
153
|
+
if (this.outBuffer)
|
|
154
|
+
this.module._free(this.outBuffer);
|
|
155
|
+
if (this.inBuffer)
|
|
156
|
+
this.module._free(this.inBuffer);
|
|
70
157
|
this.ctx = 0;
|
|
71
|
-
this.
|
|
158
|
+
this.outBuffer = 0;
|
|
159
|
+
this.inBuffer = 0;
|
|
72
160
|
}
|
|
73
161
|
}
|
|
74
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Incremental Zstandard decompressor.
|
|
165
|
+
*
|
|
166
|
+
* Input is loaded into a fixed WASM buffer one `inputBufferSize`-sized slice at
|
|
167
|
+
* a time; `decompressStep()` then drains it one output buffer at a time. A
|
|
168
|
+
* single compressed slice can expand to far more than memory, so the caller is
|
|
169
|
+
* expected to enqueue each produced slice before requesting the next one.
|
|
170
|
+
*/
|
|
75
171
|
export class ZstdDecompressor extends BaseProcessor {
|
|
76
172
|
ctx = 0;
|
|
77
|
-
|
|
78
|
-
|
|
173
|
+
inBuffer = 0;
|
|
174
|
+
outBuffer = 0;
|
|
175
|
+
outBufferSize = 0;
|
|
176
|
+
inputBufferSize = 0;
|
|
177
|
+
// Position within the slice currently resident in the WASM input buffer.
|
|
178
|
+
inLen = 0;
|
|
179
|
+
inPos = 0;
|
|
79
180
|
async init() {
|
|
80
|
-
this.module = getModule();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
throw new Error("
|
|
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
|
-
}
|
|
181
|
+
const m = (this.module = getModule());
|
|
182
|
+
this.ctx = m._createDCtx();
|
|
183
|
+
if (!this.ctx || m._initDStream(this.ctx) === 0) {
|
|
184
|
+
this.cleanup();
|
|
185
|
+
throw new Error("Failed to create decompression context");
|
|
93
186
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
187
|
+
this.outBufferSize = m._dStreamOutSize();
|
|
188
|
+
this.inputBufferSize = m._dStreamInSize();
|
|
189
|
+
this.outBuffer = m._malloc(this.outBufferSize);
|
|
190
|
+
this.inBuffer = m._malloc(this.inputBufferSize);
|
|
191
|
+
if (!this.outBuffer || !this.inBuffer) {
|
|
192
|
+
this.cleanup();
|
|
193
|
+
throw new Error("Failed to allocate decompression buffers");
|
|
101
194
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
195
|
+
}
|
|
196
|
+
/** Load one input slice (`length` must be <= `inputBufferSize`) for decoding. */
|
|
197
|
+
setInput(data, offset, length) {
|
|
198
|
+
const m = this.requireModule();
|
|
199
|
+
m.HEAPU8.set(data.subarray(offset, offset + length), this.inBuffer);
|
|
200
|
+
this.inLen = length;
|
|
201
|
+
this.inPos = 0;
|
|
202
|
+
}
|
|
203
|
+
/** True while the resident input slice still has bytes to decode. */
|
|
204
|
+
hasInput() {
|
|
205
|
+
return this.inPos < this.inLen;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Decode one output buffer's worth of data from the resident input slice.
|
|
209
|
+
* Returns the produced bytes (possibly empty). Advances the internal input
|
|
210
|
+
* position; throws if the engine can neither consume nor produce (corrupt
|
|
211
|
+
* input).
|
|
212
|
+
*/
|
|
213
|
+
decompressStep() {
|
|
214
|
+
const m = this.requireModule();
|
|
215
|
+
const result = m._decompressStream(this.ctx, this.outBuffer, this.outBufferSize, this.inBuffer + this.inPos, this.inLen - this.inPos);
|
|
216
|
+
if (result < 0) {
|
|
217
|
+
const errorCode = Number(BigInt(result) & 0x7fffffffffffffffn);
|
|
218
|
+
throw new Error(`Decompression failed: ${m._getErrorName(errorCode)}`);
|
|
126
219
|
}
|
|
127
|
-
|
|
128
|
-
|
|
220
|
+
const consumed = Number(BigInt(result) >> 32n);
|
|
221
|
+
const outputSize = Number(BigInt(result) & 0xffffffffn);
|
|
222
|
+
if (consumed === 0 && outputSize === 0) {
|
|
223
|
+
throw new Error("Decompression stalled");
|
|
129
224
|
}
|
|
225
|
+
this.inPos += consumed;
|
|
226
|
+
return outputSize === 0
|
|
227
|
+
? EMPTY
|
|
228
|
+
: new Uint8Array(m.HEAPU8.subarray(this.outBuffer, this.outBuffer + outputSize));
|
|
130
229
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
return
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
230
|
+
/** Single-shot decompression of an in-memory buffer. */
|
|
231
|
+
process(data) {
|
|
232
|
+
this.requireModule();
|
|
233
|
+
if (!data.length)
|
|
234
|
+
return EMPTY;
|
|
235
|
+
const chunks = [];
|
|
236
|
+
let pos = 0;
|
|
237
|
+
while (pos < data.length) {
|
|
238
|
+
const n = Math.min(this.inputBufferSize, data.length - pos);
|
|
239
|
+
this.setInput(data, pos, n);
|
|
240
|
+
while (this.hasInput()) {
|
|
241
|
+
const out = this.decompressStep();
|
|
242
|
+
if (out.length)
|
|
243
|
+
chunks.push(out);
|
|
244
|
+
}
|
|
245
|
+
pos += n;
|
|
142
246
|
}
|
|
143
|
-
return
|
|
247
|
+
return concat(chunks);
|
|
248
|
+
}
|
|
249
|
+
requireModule() {
|
|
250
|
+
if (!this.module)
|
|
251
|
+
throw new Error("Not initialized");
|
|
252
|
+
if (this.destroyed)
|
|
253
|
+
throw new Error("Destroyed");
|
|
254
|
+
return this.module;
|
|
144
255
|
}
|
|
145
256
|
cleanup() {
|
|
146
257
|
if (this.module) {
|
|
147
258
|
if (this.ctx)
|
|
148
259
|
this.module._freeDCtx(this.ctx);
|
|
149
|
-
if (this.
|
|
150
|
-
this.module._free(this.
|
|
260
|
+
if (this.outBuffer)
|
|
261
|
+
this.module._free(this.outBuffer);
|
|
262
|
+
if (this.inBuffer)
|
|
263
|
+
this.module._free(this.inBuffer);
|
|
151
264
|
this.ctx = 0;
|
|
152
|
-
this.
|
|
265
|
+
this.outBuffer = 0;
|
|
266
|
+
this.inBuffer = 0;
|
|
153
267
|
}
|
|
154
268
|
}
|
|
155
269
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export interface CompressOptions {
|
|
2
2
|
level?: number;
|
|
3
3
|
onProgress?: (bytesWritten: number) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Max bytes of output buffered before backpressure pauses reading the
|
|
6
|
+
* source. Larger values trade memory for throughput. Default: 1 MiB.
|
|
7
|
+
*/
|
|
8
|
+
highWaterMark?: number;
|
|
4
9
|
}
|
|
5
10
|
export interface DecompressOptions {
|
|
6
11
|
onProgress?: (bytesWritten: number) => void;
|
|
12
|
+
/** See {@link CompressOptions.highWaterMark}. Default: 1 MiB. */
|
|
13
|
+
highWaterMark?: number;
|
|
7
14
|
}
|
|
8
15
|
export declare function compress(input: Uint8Array, options?: CompressOptions): Promise<Uint8Array>;
|
|
9
16
|
export declare function compressStream(input: ReadableStream<Uint8Array>, options?: CompressOptions): Promise<ReadableStream<Uint8Array>>;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ZstdCompressor, ZstdDecompressor } from "./compressor.js";
|
|
2
2
|
import { initZstd } from "./loader.js";
|
|
3
|
+
const DEFAULT_HIGH_WATER_MARK = 1024 * 1024;
|
|
3
4
|
let initialized = false;
|
|
4
5
|
async function ensureInit() {
|
|
5
6
|
if (!initialized) {
|
|
@@ -24,62 +25,71 @@ export async function compress(input, options) {
|
|
|
24
25
|
}
|
|
25
26
|
export async function compressStream(input, options) {
|
|
26
27
|
await ensureInit();
|
|
27
|
-
const { level = 3, onProgress } = options || {};
|
|
28
|
+
const { level = 3, onProgress, highWaterMark = DEFAULT_HIGH_WATER_MARK } = options || {};
|
|
28
29
|
const reader = input.getReader();
|
|
29
|
-
|
|
30
|
+
const compressor = new ZstdCompressor(level);
|
|
31
|
+
await compressor.init();
|
|
32
|
+
let chunk = null; // current source chunk
|
|
33
|
+
let chunkPos = 0; // bytes of `chunk` already fed
|
|
34
|
+
let finishing = false; // source drained, flushing the frame
|
|
30
35
|
let totalWritten = 0;
|
|
31
|
-
|
|
36
|
+
const release = () => {
|
|
37
|
+
reader.releaseLock();
|
|
38
|
+
compressor.destroy();
|
|
39
|
+
};
|
|
32
40
|
return new ReadableStream({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await compressor.init();
|
|
36
|
-
},
|
|
41
|
+
// Produce exactly one output slice per pull so the returned stream's
|
|
42
|
+
// queue (and thus how far we read ahead of the consumer) stays bounded.
|
|
37
43
|
async pull(controller) {
|
|
38
44
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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;
|
|
45
|
+
while (true) {
|
|
46
|
+
if (chunk && chunkPos < chunk.length) {
|
|
47
|
+
const n = Math.min(compressor.inputBufferSize, chunk.length - chunkPos);
|
|
48
|
+
const out = compressor.compressStep(chunk, chunkPos, n);
|
|
49
|
+
chunkPos += n;
|
|
50
|
+
if (out.length) {
|
|
51
|
+
controller.enqueue(out);
|
|
52
|
+
totalWritten += out.length;
|
|
52
53
|
onProgress?.(totalWritten);
|
|
54
|
+
return;
|
|
53
55
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
continue; // buffered internally; keep feeding
|
|
57
|
+
}
|
|
58
|
+
if (!finishing) {
|
|
59
|
+
const { done, value } = await reader.read();
|
|
60
|
+
if (done) {
|
|
61
|
+
finishing = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (value.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
chunk = value;
|
|
67
|
+
chunkPos = 0;
|
|
68
|
+
continue;
|
|
59
69
|
}
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
controller.enqueue(
|
|
64
|
-
totalWritten +=
|
|
70
|
+
// Flushing the final frame, one output buffer at a time.
|
|
71
|
+
const { output, done } = compressor.compressFinish();
|
|
72
|
+
if (output.length) {
|
|
73
|
+
controller.enqueue(output);
|
|
74
|
+
totalWritten += output.length;
|
|
65
75
|
onProgress?.(totalWritten);
|
|
66
|
-
return; // Exit after enqueuing data
|
|
67
76
|
}
|
|
68
|
-
|
|
77
|
+
if (done) {
|
|
78
|
+
controller.close();
|
|
79
|
+
release();
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
catch (error) {
|
|
72
|
-
|
|
73
|
-
reader.releaseLock();
|
|
74
|
-
compressor?.destroy();
|
|
85
|
+
release();
|
|
75
86
|
controller.error(error);
|
|
76
87
|
}
|
|
77
88
|
},
|
|
78
89
|
cancel() {
|
|
79
|
-
|
|
80
|
-
compressor?.destroy();
|
|
90
|
+
release();
|
|
81
91
|
},
|
|
82
|
-
});
|
|
92
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark }));
|
|
83
93
|
}
|
|
84
94
|
// Decompress Uint8Array -> Uint8Array
|
|
85
95
|
export async function decompress(input, options) {
|
|
@@ -99,49 +109,61 @@ export async function decompress(input, options) {
|
|
|
99
109
|
// Decompress ReadableStream -> ReadableStream with backpressure
|
|
100
110
|
export async function decompressStream(input, options) {
|
|
101
111
|
await ensureInit();
|
|
102
|
-
const { onProgress } = options || {};
|
|
112
|
+
const { onProgress, highWaterMark = DEFAULT_HIGH_WATER_MARK } = options || {};
|
|
103
113
|
const reader = input.getReader();
|
|
104
|
-
|
|
114
|
+
const decompressor = new ZstdDecompressor();
|
|
115
|
+
await decompressor.init();
|
|
116
|
+
let chunk = null; // current source chunk
|
|
117
|
+
let chunkPos = 0; // bytes of `chunk` already loaded into WASM
|
|
105
118
|
let totalWritten = 0;
|
|
106
|
-
|
|
119
|
+
const release = () => {
|
|
120
|
+
reader.releaseLock();
|
|
121
|
+
decompressor.destroy();
|
|
122
|
+
};
|
|
107
123
|
return new ReadableStream({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
},
|
|
124
|
+
// One output slice per pull. A small compressed slice can expand to many
|
|
125
|
+
// gigabytes, so we never decode more than one output buffer ahead of the
|
|
126
|
+
// consumer.
|
|
112
127
|
async pull(controller) {
|
|
113
128
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
while (true) {
|
|
130
|
+
if (decompressor.hasInput()) {
|
|
131
|
+
const out = decompressor.decompressStep();
|
|
132
|
+
if (out.length) {
|
|
133
|
+
controller.enqueue(out);
|
|
134
|
+
totalWritten += out.length;
|
|
135
|
+
onProgress?.(totalWritten);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
continue; // consumed input without output yet
|
|
139
|
+
}
|
|
140
|
+
if (chunk && chunkPos < chunk.length) {
|
|
141
|
+
const n = Math.min(decompressor.inputBufferSize, chunk.length - chunkPos);
|
|
142
|
+
decompressor.setInput(chunk, chunkPos, n);
|
|
143
|
+
chunkPos += n;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const { done, value } = await reader.read();
|
|
147
|
+
if (done) {
|
|
148
|
+
controller.close();
|
|
149
|
+
release();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (value.length === 0)
|
|
153
|
+
continue;
|
|
154
|
+
chunk = value;
|
|
155
|
+
chunkPos = 0;
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
158
|
catch (error) {
|
|
133
|
-
|
|
134
|
-
reader.releaseLock();
|
|
135
|
-
decompressor?.destroy();
|
|
159
|
+
release();
|
|
136
160
|
controller.error(error);
|
|
137
|
-
throw error;
|
|
138
161
|
}
|
|
139
162
|
},
|
|
140
163
|
cancel() {
|
|
141
|
-
|
|
142
|
-
decompressor?.destroy();
|
|
164
|
+
release();
|
|
143
165
|
},
|
|
144
|
-
});
|
|
166
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark }));
|
|
145
167
|
}
|
|
146
168
|
export async function initialize() {
|
|
147
169
|
await ensureInit();
|
package/dist/loader.d.ts
CHANGED
package/dist/loader.js
CHANGED
|
@@ -28,10 +28,8 @@ async function evaluateCode(code) {
|
|
|
28
28
|
const mod = await import(/* @vite-ignore */ url);
|
|
29
29
|
return mod.default || mod.ZstdWasm || mod;
|
|
30
30
|
}
|
|
31
|
-
catch
|
|
32
|
-
|
|
33
|
-
console.debug("Data URL import failed:", e?.message);
|
|
34
|
-
}
|
|
31
|
+
catch {
|
|
32
|
+
// Older Node (pre-22) can't import a data: URL — fall back to vm.
|
|
35
33
|
const vm = await import("node:vm");
|
|
36
34
|
// Create a context with required globals
|
|
37
35
|
const context = vm.createContext({
|
|
@@ -51,8 +49,7 @@ async function evaluateCode(code) {
|
|
|
51
49
|
try {
|
|
52
50
|
return await import(/* @vite-ignore */ specifier);
|
|
53
51
|
}
|
|
54
|
-
catch
|
|
55
|
-
console.warn(`⚠️ Dynamic import failed: ${specifier}`, e.message);
|
|
52
|
+
catch {
|
|
56
53
|
throw new Error(`Dynamic import not supported: ${specifier}`);
|
|
57
54
|
}
|
|
58
55
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface Module {
|
|
|
9
9
|
_decompressStream(ctx: number, dst: number, dstSize: number, src: number, srcSize: number): number;
|
|
10
10
|
_dStreamOutSize(): number;
|
|
11
11
|
_cStreamOutSize(): number;
|
|
12
|
+
_cStreamInSize(): number;
|
|
13
|
+
_dStreamInSize(): number;
|
|
12
14
|
_getErrorName(error: number): string;
|
|
13
15
|
_malloc(size: number): number;
|
|
14
16
|
_free(ptr: number): void;
|