@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 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:
@@ -1,2 +1,5 @@
1
- export function init(imports?: WebAssembly.Imports): Promise<void>;
1
+ export interface InitOptions {
2
+ backend?: 'auto' | 'simd' | 'base';
3
+ }
4
+ export function init(imports?: WebAssembly.Imports, opts?: InitOptions): Promise<void>;
2
5
  export * from "./custom.js";
@@ -1,19 +1,26 @@
1
1
  import { setInstance, registerInit } from "./core.js";
2
- import { instantiateWithFallback } from "./util.js";
2
+ import { instantiateWithBackend } from "./util.js";
3
3
 
4
- import { wasmBytes as simdBytes } from "./wasm-inline/lz4.simd.wasm.js";
5
- import { wasmBytes as baseBytes } from "./wasm-inline/lz4.base.wasm.js";
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 getWasmBytes() {
8
- return { simdBytes, baseBytes };
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
- export function init(imports = {}) {
14
- return (_ready ??= (async () => {
15
- const { simdBytes, baseBytes } = await getWasmBytes();
16
- const { instance } = await instantiateWithFallback(simdBytes, baseBytes, imports);
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
@@ -1,2 +1,5 @@
1
- export function init(imports?: WebAssembly.Imports): Promise<void>;
1
+ export interface InitOptions {
2
+ backend?: 'auto' | 'simd' | 'base';
3
+ }
4
+ export function init(imports?: WebAssembly.Imports, opts?: InitOptions): Promise<void>;
2
5
  export * from "./custom.js";
package/dist/browser.js CHANGED
@@ -1,21 +1,28 @@
1
1
  import { setInstance, registerInit } from "./core.js";
2
- import { instantiateWithFallback } from "./util.js";
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 getWasmBytes() {
8
- const [simdRes, baseRes] = await Promise.all([fetch(simdUrl), fetch(baseUrl)]);
9
- const [simdBytes, baseBytes] = await Promise.all([simdRes.arrayBuffer(), baseRes.arrayBuffer()]);
10
- return { simdBytes, baseBytes };
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
- export function init(imports = {}) {
16
- return (_ready ??= (async () => {
17
- const { simdBytes, baseBytes } = await getWasmBytes();
18
- const { instance } = await instantiateWithFallback(simdBytes, baseBytes, imports);
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 compress_lz4(input: WasmInput): Promise<Uint8Array>;
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
- throw new TypeError("Expected a TypedArray or ArrayBuffer");
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 };