libfw-client
v0.4.5
Published
High-performance streaming file & folder transfer SDK for the browser (libfw WASM engine + File System Access API + IndexedDB resume).
Maintainers
Readme
libfw-client SDK
The browser SDK for libfw: a zero-config wrapper around the WASM engine, the File System Access API and IndexedDB.
The same protocol engine is also available as a native Rust client for non-browser programs —
libfw_client::native::NativeClient(asynctokio+reqwest), with a runnable CLI inexamples/rust-client.
Usage
import { LibfwClient } from 'libfw-client';
const client = new LibfwClient({
baseUrl: '/', // server origin (same-origin when empty)
concurrency: 4, // max parallel file transfers (independent HTTP streams)
uploadWindow: 8, // in-flight chunks per single file upload (raise to
// reduce upload stutter on high-latency links)
downloadWindow: 4, // parallel byte-range GETs per single file download
// (raise to reduce download stutter on high-latency links)
compress: true, // zrip per-block compression
compressLevel: 'auto',// zrip level policy: 'auto' benchmarks uploads
// (needs autoTune) / 'fast' / 'balanced' / 'max' / N
autoTune: true, // adaptive tuning: probes /capabilities and ramps
tuneTtlMs: 3600000, // how long a settled result stays cached (browser,
// localStorage); 0 = never cache, re-ramp each time
// concurrency/windows/chunk size from real stats
onEvent: (e) => {
if (e.type === 'progress') updateProgressBar(e.done, e.total);
else if (e.type === 'tuning') renderTuning(e.phase, e.params, e.stats);
// other types: fileStart, fileCompleted, ...
},
});
// Download a whole folder. Uses showDirectoryPicker when the File System
// Access API is available; otherwise the folder is zipped and saved via a
// traditional browser download — no feature detection needed by the caller.
await client.downloadFolder('your_token_here');
// Upload a FileList
const input = document.querySelector('input[type=file]');
await client.upload('your_token_here', input.files);
// Or upload a whole folder
await client.upload('your_token_here');
// Controls
client.pause();
client.resume();
client.cancel();How it works
- HTTP transport, not WebSocket — the engine drives all control commands (directory listing, metadata) and data over plain HTTP. This is what keeps transfers robust on lossy/unstable links: each transfer uses independent parallel HTTP streams, so a lost packet stalls only that one stream (which retries just its own bytes) instead of blocking a whole multiplexed WebSocket connection.
- Downloads —
downloadFolder(token, dirPath?)/downloadFile(token, filePath)list the tree (for folders) and fetch each large file asdownloadWindowconcurrentRangeGETs (tus-style parallel transfer, one independent connection per range). Each chunk is retried independently (only the lost part is re-fetched); the engine reorders the chunks in memory and pushesUint8Arrays to the SDK strictly in order.Range/If-Range/416give natural resume against the server ETag (the server is the source of truth). With the File System Access API the SDK streams chunks to disk viafileHandle.createWritable(); without it (or withdownloadMode: 'browser') it buffers the chunks and saves the result through a traditional browser download — a single file as-is, a folder packed into a.zip. - Uploads —
upload(token, files?)slices each file into chunks, reads them viareadFile, compresses each into one zstd frame and POSTs the missing chunks concurrently (out of order) withx-libfw-offsetinto a shared per-session temp on the server (positional writes). A finalx-libfw-finalcommit validates the size and atomically renames the temp into place. Only the chunks the server still misses are re-sent (x-libfw-session-statusprobe seeds resume), so interrupted uploads resume BitTorrent-style (only the broken/lost parts are re-transmitted). A dropped connection (page refresh, crashed tab) keeps that partial on the server, so a reloaded page resumes exactly where it stopped. - Resume state (
etag,offset,size) is persisted per path in IndexedDB and re-validated on every retry.createWritable()only publishes a file onclose(), so a download checkpoints its prefix to disk every time the engine reports a durable offset (~4 MiB) — a hard page refresh mid-download therefore resumes from the last checkpoint instead of restarting from byte 0. - Pause/resume/cancel drive the WASM state machine
(
idle → downloading/uploading → paused → resumed → completed/failed).
Build
# 1. Compile the WASM engine + generate the web glue (requires wasm-pack)
npm run build:wasm
# 2. (optional) bundle a UMD build
npm run build:umdThe resulting package contains:
pkg/ wasm-pack output (wasm + wasm-bindgen web glue)
index.js ESM SDK
zip.js dependency-free ZIP writer (browser-download fallback)
index.d.ts TypeScript types
dist/libfw-client.umd.js UMD bundle (after build:umd)API
new LibfwClient(options?)downloadWindow: number(default4) — in-flight byte-range window per single file download (how many concurrentRangeGETs); raise it on high-latency links.1disables parallelism (sequential downloads).chunkSize: number(default2097152, 2 MiB) — shared size used for both upload chunks and parallel download ranges; the engine reorders in-flight chunks in memory (worst case ≈downloadWindow * chunkSizebytes) so the SDK still receives data in order.compress: boolean(defaulttrue) — master switch for zrip compression;falsesends every body as identity.compressLevel: number | 'auto' | 'fast' | 'balanced' | 'max'(default'balanced') — zrip level policy whencompressis on.'fast'is the advertised minimum (least CPU, worst ratio),'balanced'the advertised default,'max'the advertised maximum (best ratio); a number is clamped into the advertised range.'auto'additionally micro-benchmarks the advertised range against a real sample of the first uploaded file (whileautoTuneis enabled) and picks the best bytes-saved-vs-CPU-time trade-off for the measured link speed; downloads request the resolved level from the server.downloadMode: 'auto' | 'fs' | 'browser'(default'auto') —'fs'streams downloads through the File System Access API;'browser'buffers and triggers a traditional browser download (folders become.zip);'auto'uses'fs'when the API exists and falls back to'browser'. Download resume requires'fs'(or an injecteddirectoryHandle): an interrupted fs-mode download is continued from the bytes already on disk, while the memory-backed'browser'fallback always restarts from byte 0 — there is no partial file to continue from.maxFallbackBytes: number(default536870912, 512 MiB) — memory cap for the in-memory'browser'fallback. File sizes are pre-checked against it before buffering; a download that would exceed it rejects with atoo-largeLibfwErrorinstead of risking an OOM.0disables.autoTune: boolean(defaultfalse) — enable the adaptive tuning engine. The engine probes the server's/capabilitieslimits and TCP-style ramps the per-file window and cross-file concurrency from the advertised minimums using real transfer stats; the chunk size follows the measured throughput (~100 ms of it, clamped into the advertised range) and the zrip level is a client policy fromcompressLevel, never ramped. When disabled, the configured static values are used as-is. Tuning state is in memory for the lifetime of the client (a settle is reused by later transfers and dropped on failure) and is cached inlocalStorageper origin + direction, so a page refresh does not re-ramp — seetuneTtlMs.tuneTtlMs: number(default3600000, 1 hour) — how long a cached tuning result stays usable. The cache is keyed by origin and direction (an upload settle says nothing about a download), is tagged with the/capabilitiesit was measured against, and expirestuneTtlMsafter the ramp settled — not after the last reuse — so a link measured long ago is re-measured even if it is used constantly. An entry is also discarded early when the capabilities change or a transfer fails.0disables the cache entirely (every transfer re-ramps). Ignored unlessautoTuneis enabled. Storage failures (private mode, quota, disabled storage) are swallowed: caching is an optimisation and never fails a transfer.
downloadFolder(token, dirPath?) → Promise<number>downloadFile(token, filePath) → Promise<number>upload(token, files?) → Promise<number>clearResumeStore(direction?) → Promise<number>pause(),resume(),cancel()state(),progress(),doneBytes(),totalBytes()tuneStatus() → { phase, params, stats, capsHash } | null— live adaptive-tuning status.phaseisuninitialized | ramping | settled | degraded;paramsis{ concurrency, uploadWindow, downloadWindow, chunkSize, compressLevel }(the zrip level is the resolved policy, i.e. what downloads request — uploads may use an'auto'-benchmarked level for the session);statsis{ rttMs, mbps }(EWMA request RTT, last-window throughput).nulluntil the WASM engine is initialised.- Events: with
autoTuneenabled,onEventadditionally receives{ type: 'tuning', phase, params, stats }on every phase transition / window evaluation. - Errors: every rejection is a
LibfwErrorwith a stablecode.
See index.d.ts for the full type surface.
