@addmaple/lz4 0.1.1 → 0.2.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 +87 -0
- package/dist/browser-inline.d.ts +4 -1
- package/dist/browser-inline.js +16 -9
- package/dist/browser.d.ts +4 -1
- package/dist/browser.js +16 -9
- package/dist/core.d.ts +10 -1
- package/dist/core.js +174 -2
- package/dist/custom.js +404 -3
- package/dist/node-inline.d.ts +4 -1
- package/dist/node-inline.js +16 -9
- package/dist/node.d.ts +4 -1
- package/dist/node.js +14 -11
- package/dist/util.js +30 -0
- package/dist/wasm/lz4.base.wasm +0 -0
- package/dist/wasm/lz4.simd.wasm +0 -0
- package/dist/wasm-inline/lz4.base.wasm.js +1 -1
- package/dist/wasm-inline/lz4.simd.wasm.js +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -4,6 +4,24 @@ Fast LZ4 compression in the browser and Node.js using Rust + WASM.
|
|
|
4
4
|
|
|
5
5
|
**2.5x-3.5x faster** than `lz4js`.
|
|
6
6
|
|
|
7
|
+
## Implementation (Rust)
|
|
8
|
+
|
|
9
|
+
This package is backed by these Rust crates in the `wasm-fast-compress` repo:
|
|
10
|
+
|
|
11
|
+
- `codec-lz4` (this repo): high-level codec wrapper
|
|
12
|
+
- `lz4_flex` (Git fork, branch `wasm-simd`): core LZ4 implementation with WASM SIMD128 hot paths
|
|
13
|
+
|
|
14
|
+
## SIMD acceleration (how it works)
|
|
15
|
+
|
|
16
|
+
- We build **two WASM binaries**:
|
|
17
|
+
- `lz4.base.wasm`: compiled without `+simd128`
|
|
18
|
+
- `lz4.simd.wasm`: compiled with `-C target-feature=+simd128`
|
|
19
|
+
- At runtime, the JS loader detects SIMD support and loads the best binary automatically.
|
|
20
|
+
|
|
21
|
+
On wasm32, the SIMD build benefits from:
|
|
22
|
+
- `lz4_flex` explicit `wasm32 + simd128` intrinsics for match finding / copying hot paths
|
|
23
|
+
- additional LLVM autovectorization where applicable
|
|
24
|
+
|
|
7
25
|
## Installation
|
|
8
26
|
|
|
9
27
|
```bash
|
|
@@ -15,12 +33,81 @@ npm install @addmaple/lz4
|
|
|
15
33
|
```javascript
|
|
16
34
|
import { init, compress } from '@addmaple/lz4';
|
|
17
35
|
|
|
36
|
+
// Optional: call init() to avoid first-call latency.
|
|
37
|
+
// If you skip init(), the first compress/decompress call will lazy-initialize.
|
|
18
38
|
await init();
|
|
19
39
|
|
|
20
40
|
const input = new TextEncoder().encode('hello world');
|
|
21
41
|
const compressed = await compress(input);
|
|
22
42
|
```
|
|
23
43
|
|
|
44
|
+
### Lazy init (init() optional)
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { compress } from '@addmaple/lz4';
|
|
48
|
+
|
|
49
|
+
const input = new TextEncoder().encode('hello world');
|
|
50
|
+
const compressed = await compress(input); // triggers lazy init on first call
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Streaming compression + decompression
|
|
54
|
+
|
|
55
|
+
For chunked input (e.g. streaming over the network), use the handle-based streaming helpers:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
import { init, StreamingCompressor, StreamingDecompressor } from '@addmaple/lz4';
|
|
59
|
+
|
|
60
|
+
// Optional: init() is not required; streaming helpers also lazy-init.
|
|
61
|
+
await init();
|
|
62
|
+
|
|
63
|
+
// Compress
|
|
64
|
+
const enc = new StreamingCompressor();
|
|
65
|
+
const c1 = await enc.compressChunk(chunk1, false);
|
|
66
|
+
const c2 = await enc.compressChunk(chunk2, false);
|
|
67
|
+
const c3 = await enc.compressChunk(chunk3, true); // finish
|
|
68
|
+
|
|
69
|
+
// Decompress (finish must be true to produce output for frame format)
|
|
70
|
+
const dec = new StreamingDecompressor();
|
|
71
|
+
await dec.decompressChunk(c1, false);
|
|
72
|
+
await dec.decompressChunk(c2, false);
|
|
73
|
+
const plain = await dec.decompressChunk(c3, true);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Streaming to `fetch()` (ergonomic)
|
|
77
|
+
|
|
78
|
+
If you want to upload a `File`/`Blob` with LZ4 compression, you can pipe it through the built-in stream helper:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
import { createCompressionStream } from '@addmaple/lz4';
|
|
82
|
+
|
|
83
|
+
const body = file.stream().pipeThrough(createCompressionStream());
|
|
84
|
+
|
|
85
|
+
await fetch('/upload', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
// Not a standard encoding token like gzip; your server must explicitly support this.
|
|
89
|
+
'Content-Encoding': 'lz4',
|
|
90
|
+
},
|
|
91
|
+
body,
|
|
92
|
+
// Needed for streaming request bodies in some runtimes (notably Node fetch).
|
|
93
|
+
duplex: 'half',
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Streaming decompression from `fetch()` (ergonomic)
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
import { createDecompressionStream } from '@addmaple/lz4';
|
|
101
|
+
|
|
102
|
+
const res = await fetch('/download');
|
|
103
|
+
if (!res.body) throw new Error('No response body');
|
|
104
|
+
|
|
105
|
+
const decompressed = res.body.pipeThrough(createDecompressionStream());
|
|
106
|
+
|
|
107
|
+
// Example: read it all (or pipe somewhere else)
|
|
108
|
+
const buf = await new Response(decompressed).arrayBuffer();
|
|
109
|
+
```
|
|
110
|
+
|
|
24
111
|
### Inline (Zero-latency)
|
|
25
112
|
|
|
26
113
|
WASM bytes embedded directly in JS — no separate file fetching:
|
package/dist/browser-inline.d.ts
CHANGED
package/dist/browser-inline.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { setInstance, registerInit } from "./core.js";
|
|
2
|
-
import {
|
|
2
|
+
import { instantiateWithBackend } from "./util.js";
|
|
3
3
|
|
|
4
|
-
import { wasmBytes as
|
|
5
|
-
import { wasmBytes as
|
|
4
|
+
import { wasmBytes as _simdBytes } from "./wasm-inline/lz4.simd.wasm.js";
|
|
5
|
+
import { wasmBytes as _baseBytes } from "./wasm-inline/lz4.base.wasm.js";
|
|
6
6
|
|
|
7
|
-
async function
|
|
8
|
-
return
|
|
7
|
+
async function getSimdBytes() {
|
|
8
|
+
return _simdBytes;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getBaseBytes() {
|
|
12
|
+
return _baseBytes;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
let _ready = null;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
let _backend = null;
|
|
18
|
+
export function init(imports = {}, opts = {}) {
|
|
19
|
+
const backend = opts.backend || 'auto';
|
|
20
|
+
if (_ready && _backend === backend) return _ready;
|
|
21
|
+
_backend = backend;
|
|
22
|
+
return (_ready = (async () => {
|
|
23
|
+
const { instance } = await instantiateWithBackend({ getSimdBytes, getBaseBytes, imports, backend });
|
|
17
24
|
setInstance(instance);
|
|
18
25
|
})());
|
|
19
26
|
}
|
package/dist/browser.d.ts
CHANGED
package/dist/browser.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { setInstance, registerInit } from "./core.js";
|
|
2
|
-
import {
|
|
2
|
+
import { instantiateWithBackend } from "./util.js";
|
|
3
3
|
|
|
4
4
|
const simdUrl = new URL("./wasm/lz4.simd.wasm", import.meta.url);
|
|
5
5
|
const baseUrl = new URL("./wasm/lz4.base.wasm", import.meta.url);
|
|
6
6
|
|
|
7
|
-
async function
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
async function getSimdBytes() {
|
|
8
|
+
const res = await fetch(simdUrl);
|
|
9
|
+
return res.arrayBuffer();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getBaseBytes() {
|
|
13
|
+
const res = await fetch(baseUrl);
|
|
14
|
+
return res.arrayBuffer();
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
let _ready = null;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
let _backend = null;
|
|
20
|
+
export function init(imports = {}, opts = {}) {
|
|
21
|
+
const backend = opts.backend || 'auto';
|
|
22
|
+
if (_ready && _backend === backend) return _ready;
|
|
23
|
+
_backend = backend;
|
|
24
|
+
return (_ready = (async () => {
|
|
25
|
+
const { instance } = await instantiateWithBackend({ getSimdBytes, getBaseBytes, imports, backend });
|
|
19
26
|
setInstance(instance);
|
|
20
27
|
})());
|
|
21
28
|
}
|
package/dist/core.d.ts
CHANGED
|
@@ -6,4 +6,13 @@ export function memoryU8(): Uint8Array;
|
|
|
6
6
|
export function alloc(len: number): number;
|
|
7
7
|
export function free(ptr: number, len: number): void;
|
|
8
8
|
|
|
9
|
-
export function
|
|
9
|
+
export function compress_lz4_block(input: WasmInput): Promise<Uint8Array>;
|
|
10
|
+
export function decompress_lz4_block(input: WasmInput): Promise<Uint8Array>;
|
|
11
|
+
export function compress_lz4(input: WasmInput): Promise<Uint8Array>;
|
|
12
|
+
export function create_compressor(input: WasmInput): Promise<number>;
|
|
13
|
+
export function compress_chunk(input: WasmInput): Promise<Uint8Array>;
|
|
14
|
+
export function destroy_compressor(input: WasmInput): Promise<Uint8Array>;
|
|
15
|
+
export function decompress_lz4(input: WasmInput): Promise<Uint8Array>;
|
|
16
|
+
export function create_decompressor(input: WasmInput): Promise<number>;
|
|
17
|
+
export function decompress_chunk(input: WasmInput): Promise<Uint8Array>;
|
|
18
|
+
export function destroy_decompressor(input: WasmInput): Promise<Uint8Array>;
|
package/dist/core.js
CHANGED
|
@@ -18,7 +18,7 @@ export function wasmExports() {
|
|
|
18
18
|
let _ready = null;
|
|
19
19
|
export function registerInit(fn) { _initFn = fn; }
|
|
20
20
|
|
|
21
|
-
async function ensureReady() {
|
|
21
|
+
export async function ensureReady() {
|
|
22
22
|
if (_ready) return _ready;
|
|
23
23
|
if (!_initFn) throw new Error("init not registered");
|
|
24
24
|
_ready = _initFn();
|
|
@@ -42,7 +42,42 @@ function toBytes(input) {
|
|
|
42
42
|
if (input instanceof Uint8Array) return input;
|
|
43
43
|
if (ArrayBuffer.isView(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
44
44
|
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
45
|
-
|
|
45
|
+
if (typeof input === 'string') return new TextEncoder().encode(input);
|
|
46
|
+
throw new TypeError("Expected a TypedArray, ArrayBuffer, or string");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scalarSize(type) {
|
|
50
|
+
switch (type) {
|
|
51
|
+
case "f64": return 8;
|
|
52
|
+
case "f32":
|
|
53
|
+
case "i32":
|
|
54
|
+
case "u32": return 4;
|
|
55
|
+
case "i16":
|
|
56
|
+
case "u16": return 2;
|
|
57
|
+
case "i8":
|
|
58
|
+
case "u8": return 1;
|
|
59
|
+
case "u32_array":
|
|
60
|
+
case "i32_array":
|
|
61
|
+
case "f32_array": return 1024 * 1024;
|
|
62
|
+
default: return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function decodeReturn(view, type) {
|
|
67
|
+
switch (type) {
|
|
68
|
+
case "f32": return view.getFloat32(0, true);
|
|
69
|
+
case "f64": return view.getFloat64(0, true);
|
|
70
|
+
case "i32": return view.getInt32(0, true);
|
|
71
|
+
case "u32": return view.getUint32(0, true);
|
|
72
|
+
case "i16": return view.getInt16(0, true);
|
|
73
|
+
case "u16": return view.getUint16(0, true);
|
|
74
|
+
case "i8": return view.getInt8(0);
|
|
75
|
+
case "u8": return view.getUint8(0);
|
|
76
|
+
case "u32_array": return new Uint32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
|
|
77
|
+
case "i32_array": return new Int32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
|
|
78
|
+
case "f32_array": return new Float32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
|
|
79
|
+
default: return null;
|
|
80
|
+
}
|
|
46
81
|
}
|
|
47
82
|
|
|
48
83
|
function callWasm(abi, input, outLen, reuse) {
|
|
@@ -79,6 +114,36 @@ function callWasm(abi, input, outLen, reuse) {
|
|
|
79
114
|
return { inPtr, outPtr, len, outLen, written };
|
|
80
115
|
}
|
|
81
116
|
|
|
117
|
+
async function compress_lz4_block(input) {
|
|
118
|
+
await ensureReady();
|
|
119
|
+
const view = toBytes(input);
|
|
120
|
+
const len = view.byteLength;
|
|
121
|
+
const outLen = len + 1024;
|
|
122
|
+
const { outPtr, written, inPtr } = callWasm("compress_lz4_block", view, outLen, null);
|
|
123
|
+
|
|
124
|
+
const result = memoryU8().slice(outPtr, outPtr + written);
|
|
125
|
+
|
|
126
|
+
free(inPtr, len);
|
|
127
|
+
free(outPtr, outLen);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
export { compress_lz4_block };
|
|
131
|
+
|
|
132
|
+
async function decompress_lz4_block(input) {
|
|
133
|
+
await ensureReady();
|
|
134
|
+
const view = toBytes(input);
|
|
135
|
+
const len = view.byteLength;
|
|
136
|
+
const outLen = outLen;
|
|
137
|
+
const { outPtr, written, inPtr } = callWasm("decompress_lz4_block", view, outLen, null);
|
|
138
|
+
|
|
139
|
+
const result = memoryU8().slice(outPtr, outPtr + written);
|
|
140
|
+
|
|
141
|
+
free(inPtr, len);
|
|
142
|
+
free(outPtr, outLen);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
export { decompress_lz4_block };
|
|
146
|
+
|
|
82
147
|
async function compress_lz4(input) {
|
|
83
148
|
await ensureReady();
|
|
84
149
|
const view = toBytes(input);
|
|
@@ -93,3 +158,110 @@ async function compress_lz4(input) {
|
|
|
93
158
|
return result;
|
|
94
159
|
}
|
|
95
160
|
export { compress_lz4 };
|
|
161
|
+
|
|
162
|
+
async function create_compressor(input) {
|
|
163
|
+
await ensureReady();
|
|
164
|
+
const view = toBytes(input);
|
|
165
|
+
const len = view.byteLength;
|
|
166
|
+
const outLen = (scalarSize('u32') || 4);
|
|
167
|
+
const { outPtr, written, inPtr } = callWasm("create_compressor", view, outLen, null);
|
|
168
|
+
|
|
169
|
+
const retView = new DataView(memoryU8().buffer, outPtr, written);
|
|
170
|
+
const result = decodeReturn(retView, "u32");
|
|
171
|
+
|
|
172
|
+
free(inPtr, len);
|
|
173
|
+
free(outPtr, outLen);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
export { create_compressor };
|
|
177
|
+
|
|
178
|
+
async function compress_chunk(input) {
|
|
179
|
+
await ensureReady();
|
|
180
|
+
const view = toBytes(input);
|
|
181
|
+
const len = view.byteLength;
|
|
182
|
+
const outLen = len + 1024;
|
|
183
|
+
const { outPtr, written, inPtr } = callWasm("compress_chunk", view, outLen, null);
|
|
184
|
+
|
|
185
|
+
const result = memoryU8().slice(outPtr, outPtr + written);
|
|
186
|
+
|
|
187
|
+
free(inPtr, len);
|
|
188
|
+
free(outPtr, outLen);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
export { compress_chunk };
|
|
192
|
+
|
|
193
|
+
async function destroy_compressor(input) {
|
|
194
|
+
await ensureReady();
|
|
195
|
+
const view = toBytes(input);
|
|
196
|
+
const len = view.byteLength;
|
|
197
|
+
const outLen = Math.max(len, 4);
|
|
198
|
+
const { outPtr, written, inPtr } = callWasm("destroy_compressor", view, outLen, null);
|
|
199
|
+
|
|
200
|
+
const result = memoryU8().slice(outPtr, outPtr + written);
|
|
201
|
+
|
|
202
|
+
free(inPtr, len);
|
|
203
|
+
free(outPtr, outLen);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
export { destroy_compressor };
|
|
207
|
+
|
|
208
|
+
async function decompress_lz4(input) {
|
|
209
|
+
await ensureReady();
|
|
210
|
+
const view = toBytes(input);
|
|
211
|
+
const len = view.byteLength;
|
|
212
|
+
const outLen = len * 10;
|
|
213
|
+
const { outPtr, written, inPtr } = callWasm("decompress_lz4", view, outLen, null);
|
|
214
|
+
|
|
215
|
+
const result = memoryU8().slice(outPtr, outPtr + written);
|
|
216
|
+
|
|
217
|
+
free(inPtr, len);
|
|
218
|
+
free(outPtr, outLen);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
export { decompress_lz4 };
|
|
222
|
+
|
|
223
|
+
async function create_decompressor(input) {
|
|
224
|
+
await ensureReady();
|
|
225
|
+
const view = toBytes(input);
|
|
226
|
+
const len = view.byteLength;
|
|
227
|
+
const outLen = (scalarSize('u32') || 4);
|
|
228
|
+
const { outPtr, written, inPtr } = callWasm("create_decompressor", view, outLen, null);
|
|
229
|
+
|
|
230
|
+
const retView = new DataView(memoryU8().buffer, outPtr, written);
|
|
231
|
+
const result = decodeReturn(retView, "u32");
|
|
232
|
+
|
|
233
|
+
free(inPtr, len);
|
|
234
|
+
free(outPtr, outLen);
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
export { create_decompressor };
|
|
238
|
+
|
|
239
|
+
async function decompress_chunk(input) {
|
|
240
|
+
await ensureReady();
|
|
241
|
+
const view = toBytes(input);
|
|
242
|
+
const len = view.byteLength;
|
|
243
|
+
const outLen = len * 4;
|
|
244
|
+
const { outPtr, written, inPtr } = callWasm("decompress_chunk", view, outLen, null);
|
|
245
|
+
|
|
246
|
+
const result = memoryU8().slice(outPtr, outPtr + written);
|
|
247
|
+
|
|
248
|
+
free(inPtr, len);
|
|
249
|
+
free(outPtr, outLen);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
export { decompress_chunk };
|
|
253
|
+
|
|
254
|
+
async function destroy_decompressor(input) {
|
|
255
|
+
await ensureReady();
|
|
256
|
+
const view = toBytes(input);
|
|
257
|
+
const len = view.byteLength;
|
|
258
|
+
const outLen = Math.max(len, 4);
|
|
259
|
+
const { outPtr, written, inPtr } = callWasm("destroy_decompressor", view, outLen, null);
|
|
260
|
+
|
|
261
|
+
const result = memoryU8().slice(outPtr, outPtr + written);
|
|
262
|
+
|
|
263
|
+
free(inPtr, len);
|
|
264
|
+
free(outPtr, outLen);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
export { destroy_decompressor };
|