Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,43 @@ The API is designed to be as JavaScript-standard as possible, so `XzReadableStre

If you're using this to decompress content from a file or other source, rather than an HTTP response body, you'll need to get a `ReadableStream` (a web stream) for the file's data to replace the `compressedResponse.body` stream above. For example, in Node.js you could use `Readable.toWeb(fs.createReadStream(filename))` to stream from the disk, or `new Blob([buffer]).stream()` if you already have the data in a `Buffer`.

## Cloudflare Workers & other restricted runtimes

Some runtimes disallow dynamic wasm compilation from bytes at runtime (for example, Cloudflare Workers).

To support these environments, you can provide a pre-compiled `WebAssembly.Module` with `XzReadableStream.setWasmModule(...)` once at startup, before creating any `XzReadableStream` instances.

### Cloudflare Workers example

```js
import { XzReadableStream } from 'xz-decompress';
import xzWasmModule from 'xz-decompress/dist/native/xz-decompress.wasm';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes that internal path part of the public API contract for this module. It's also a bit of an awkward fallback generally, especially in downstream modules that want to wrap this package.

I have an interesting alternative approach: what if we create a pure JS fallback? We can mechanically compile the existing inline WASM to asm.js with wasm2js and just ship the JS equivalent. Browsers no longer optimize asm nowadays, so it's a bit slower, but it's pure JS so it'll run anywhere. That removes the WASM requirement completely. Performance hit is unlikely to matter unless your app is decompressing huge XZs in batch all day long. Makes usage and deployment way simpler, covers lots of other cases cleanly, and it'd be easy to bring back the optional WASM file approach. Means no special APIs or funky WASM deployment steps required.

I'd suggest we still use the current model by preference, but pull in a precompiled pure JS equivalent when it's unavailable.

Would that work for you?


// Call once during startup/module initialization:
XzReadableStream.setWasmModule(xzWasmModule);

export default {
async fetch(request) {
const compressedResponse = await fetch('https://example.com/somefile.xz');
return new Response(new XzReadableStream(compressedResponse.body));
}
};
```

### Generic runtime pattern

If your runtime/bundler gives you a `WebAssembly.Module` from a static `.wasm` import, pass it to `setWasmModule` in the same way.

If your runtime only gives you bytes or a URL, you may need to compile yourself:

```js
const wasmBytes = await fetch(wasmUrl).then((r) => r.arrayBuffer());
const wasmModule = await WebAssembly.compile(wasmBytes);
XzReadableStream.setWasmModule(wasmModule);
```

Note: this fallback only works where `WebAssembly.compile(...)` is allowed by the runtime.

## What about `.tar.xz` files?

Since the `.xz` format only represents one file, it's common for people to bundle up a collection of files as `.tar`, and then compress this to `.tar.xz`.
Expand Down
42 changes: 34 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"files": [
"dist/package/**",
"dist/native/xz-decompress.wasm",
"types.d.ts"
]
}
89 changes: 62 additions & 27 deletions src/xz-decompress.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ const ReadableStream = globalThis.ReadableStream
// This won't be reached in modern browsers, and bundlers will ignore due to 'browser' field in package.json:
|| require('stream/web').ReadableStream;

// Try to compile the wasm module eagerly at import time. This fails in runtimes
// like Cloudflare Workers that block WebAssembly.compile entirely; those runtimes
// must call XzReadableStream.setWasmModule() with a pre-compiled module instead.
let _wasmModulePromise = (async () => {
const base64Wasm = xzwasmBytes.replace('data:application/wasm;base64,', '');
const wasmBytes = Uint8Array.from(atob(base64Wasm), c => c.charCodeAt(0)).buffer;
return WebAssembly.compile(wasmBytes);
})().catch(() => null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compilation still needs to be lazy in all cases


const XZ_OK = 0;
const XZ_STREAM_END = 1;

Expand Down Expand Up @@ -97,35 +106,67 @@ export class XzReadableStream extends ReadableStream {
static _moduleInstance;
static _contextMutex = new ContextMutex();

/**
* Provide a pre-compiled WebAssembly.Module for runtimes that block
* dynamic compilation (e.g. Cloudflare Workers).
*/
static setWasmModule(wasmModule) {
_wasmModulePromise = Promise.resolve(wasmModule);
XzReadableStream._moduleInstance = null;
XzReadableStream._moduleInstancePromise = null;
}

static async _getModuleInstance() {
const base64Wasm = xzwasmBytes.replace('data:application/wasm;base64,', '');
const wasmBytes = Uint8Array.from(atob(base64Wasm), c => c.charCodeAt(0)).buffer;
const wasmOptions = {};
const module = await WebAssembly.instantiate(wasmBytes, wasmOptions);
XzReadableStream._moduleInstance = module.instance;
const compiledModule = await _wasmModulePromise;
if (!compiledModule) {
throw new Error(
'WebAssembly compilation is not available in this runtime. ' +
'Call XzReadableStream.setWasmModule(module) with a pre-compiled WebAssembly.Module before use.'
);
}
XzReadableStream._moduleInstance = await WebAssembly.instantiate(compiledModule);
}

constructor(compressedStream) {
let xzContext;
let unconsumedInput = null;
let finalized = false;
let initError = null;
const compressedReader = compressedStream.getReader();

super({
async start(controller) {
await XzReadableStream._contextMutex.acquire();
function finalizeOnce() {
if (finalized) return;
finalized = true;
if (xzContext) {
xzContext.dispose();
xzContext = null;
}
XzReadableStream._contextMutex.release();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very suspicious of the changes in the stream machinery here. Seems like a totally separate issue, and I suspect the agent has just got confused and run into a separate issue because there's no reason Workers should have different stream issues than normal JS.

Lets drop all of this and do a PR just for module loading. If there is a separate issue with the stream behaviour, we should be able to reproduce it in Node with a failing test, and then fix it independently in another PR. For now lets revert all of that and just fix the module part here.

}

try {
if (!XzReadableStream._moduleInstance) {
await (XzReadableStream._moduleInstancePromise || (XzReadableStream._moduleInstancePromise = XzReadableStream._getModuleInstance()));
}
xzContext = new XzContext(XzReadableStream._moduleInstance);
} catch (error) {
XzReadableStream._contextMutex.release();
throw error;
const initPromise = (async () => {
await XzReadableStream._contextMutex.acquire();
try {
if (!XzReadableStream._moduleInstance) {
await (XzReadableStream._moduleInstancePromise || (XzReadableStream._moduleInstancePromise = XzReadableStream._getModuleInstance()));
}
xzContext = new XzContext(XzReadableStream._moduleInstance);
} catch (error) {
initError = error;
XzReadableStream._contextMutex.release();
}
})();

super({
async start() {
await initPromise;
if (initError) throw initError;
},

async pull(controller) {
await initPromise;
if (initError) throw initError;

try {
if (xzContext.needsMoreInput()) {
if (unconsumedInput === null || unconsumedInput.byteLength === 0) {
Expand All @@ -144,26 +185,20 @@ export class XzReadableStream extends ReadableStream {
xzContext.resetOutputBuffer();

if (nextOutputResult.finished) {
xzContext.dispose();
XzReadableStream._contextMutex.release();
finalizeOnce();
controller.close();
}
} catch (error) {
if (xzContext) {
xzContext.dispose();
}
XzReadableStream._contextMutex.release();
finalizeOnce();
throw error;
}
},
cancel() {
async cancel() {
await initPromise;
try {
if (xzContext) {
xzContext.dispose();
}
return compressedReader.cancel();
} finally {
XzReadableStream._contextMutex.release();
finalizeOnce();
}
}
});
Expand Down
Loading
Loading