Files
paradox-save-parser-frontend/src/lib/saveArchive/zstdStream.ts
T
2026-09-23 17:26:40 +02:00

59 lines
2.2 KiB
TypeScript

// Zstandard compression and decompression as stream transforms.
//
// Saves are re-compressed in the browser because the parser backend runs on a
// cheap two core server with little disk space. zstd was picked over lzma2 and
// brotli because its decompression speed does not depend on the level the data
// was compressed with, so the level can be raised without costing the server
// anything, and because its wasm build is by far the smallest of the three.
//
// @tootallnate/zstd-wasm does the work: it is upstream zstd 1.5.7 built with
// wasi-sdk and driven through ZSTD_compressStream2, so it compresses a stream
// as it arrives rather than a buffer at a time. A save of any size comes out
// as one zstd frame, and memory stays at the library's own input and output
// buffers.
import {
ZstdCompressStream,
ZstdDecompressStream,
} from '@tootallnate/zstd-wasm'
// The wasm file ships inside the package. Turbopack rewrites this into the URL
// it emits the asset at, so the 534 kB binary is fetched once at runtime and
// cached by the browser instead of being inlined into the worker bundle.
const wasmUrl = new URL(
'../../../node_modules/@tootallnate/zstd-wasm/dist/zstd.wasm',
import.meta.url
)
// Compiling takes a moment, so it happens once and both directions share it.
let wasmModule: Promise<WebAssembly.Module> | null = null
function getWasm(): Promise<WebAssembly.Module> {
// compileStreaming would need the server to send Content-Type:
// application/wasm, which is not worth depending on.
wasmModule ??= fetch(wasmUrl)
.then((res) => res.arrayBuffer())
.then((bytes) => WebAssembly.compile(bytes))
return wasmModule
}
/**
* Compresses a stream into a single zstd frame. The level goes from 1 to 22.
*/
export function createZstdCompressStream(
compressionLevel: number
): TransformStream<Uint8Array, Uint8Array> {
return new ZstdCompressStream(getWasm(), compressionLevel)
}
/**
* Decompresses a zstd stream. Frames that follow one another are read straight
* through, so an archive packed as several frames still opens.
*/
export function createZstdDecompressStream(): TransformStream<
Uint8Array,
Uint8Array
> {
return new ZstdDecompressStream(getWasm())
}