Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

parallel-range-downloader

English · Deutsch · Türkçe

A zero-dependency parallel HTTP Range downloader that runs unchanged in browsers, Chrome MV3 extensions and Node.js 18+.

import { download } from './src/range-downloader.js';

const { blob, parallel } = await download('https://example.com/large.zip', {
  connections: 8,
  onProgress: ({ percent, megabytesPerSecond }) =>
    console.log(`${percent}% at ${megabytesPerSecond.toFixed(2)} MB/s`)
});

Why

A single HTTP connection is frequently not the bottleneck the network is. Many origins shape throughput per connection, so one stream sits far below the available line speed while eight parallel streams saturate it. The fix is well known — request byte ranges concurrently and reassemble them — but the details are where downloads quietly corrupt themselves.

This library exists because those details are worth writing down once:

  • A server can answer a Range request with 200 and the whole body instead of 206 and the slice. Splicing that into the output shifts every later byte, and the file only breaks much later, inside whatever reads it.
  • A chunk can come back short. Accepting it produces a file that looks complete and is not.
  • Credentials matter. A request that needs cookies but is sent without them gets a 403, and a probe that reports "size unknown" hides the real reason.

Each of those is handled explicitly, and each has a test.

Features

  • Parallel byte-range downloads through a bounded worker pool — the connection count is a ceiling, not a thread-per-chunk explosion.
  • Automatic fallback. If the server does not honour Range, the download still completes in a single stream instead of failing.
  • Byte-exact verification. Every chunk is checked against its expected length; the assembled result is checked against the advertised total.
  • Retries with backoff, with a separate budget for servers that ignore Range so they cannot exhaust the retries meant for real network errors.
  • Progress with smoothed speed and ETA.
  • AbortSignal support throughout, including the backoff sleeps.
  • Diagnosable failures. Errors carry the probe transcript (error.attempts), so "download failed" becomes a list of what was tried and what each attempt answered.
  • Zero dependencies. Only fetch, Blob and AbortSignal.

Install

No build step and no package manager required — the module is plain ESM.

git clone https://github.com/tafirnat/parallel-range-downloader.git
cd parallel-range-downloader
npm test          # runs the suite against local HTTP servers

To use it in your own project, copy src/range-downloader.js in, or add this repository as a dependency.

Quick start

Node.js

node examples/node-cli/download.js https://example.com/large.zip out.zip --connections 8
[########################################] 100%  512.0 MB/512.0 MB  11.83 MB/s  ETA 00:00
Saved out.zip (512.0 MB) in 43.2s via 8 parallel connections.

Browser / extension

import { download } from './range-downloader.js';

const controller = new AbortController();
document.querySelector('#cancel').onclick = () => controller.abort();

const { blob } = await download(url, {
  connections: 8,
  chunkSize: 10 * 1024 * 1024,
  signal: controller.signal,
  onProgress: ({ percent }) => (progressBar.style.width = `${percent}%`)
});

const objectUrl = URL.createObjectURL(blob);
// ... hand it to a download, then always:
URL.revokeObjectURL(objectUrl);

API

download(url, options?)

Probes the URL, then downloads in parallel when the server allows it and in a single stream when it does not. This is the entry point most callers want.

Returns { blob, size, parallel, attempts }, where parallel tells you which path was taken.

probeRange(url, options?)

Asks how large the file is and whether ranges are honoured, without downloading it.

Returns { size, rangeSupported, init, attempts }. A size of 0 means the server could not be read at all — attempts says why.

A one-byte Range GET is used rather than HEAD, because many origins answer HEAD differently from GET (or reject it), and "will you serve ranges" is only trustworthy when a range was actually requested. HEAD is kept as a fallback.

downloadParallel(url, options)

The engine itself, for when you already know the size. Requires options.size. Returns a Blob.

Options

Option Default Applies to Meaning
size downloadParallel Total bytes. Required.
chunkSize 10 MiB download, parallel Bytes per range request.
connections 8 download, parallel Maximum concurrent requests.
retries 4 download, parallel Retries per chunk before failing.
onProgress download, parallel Called after each completed chunk.
fetchInit {} all Merged into every fetch (headers, mode, ...).
credentialModes include, then omit probe, download Credentials modes to try, in order.
signal all AbortSignal.

Progress

{
  downloadedBytes, totalBytes, percent,
  bytesPerSecond, megabytesPerSecond, etaSeconds,
  completedChunks, totalChunks
}

Speed is smoothed over 500 ms windows; raw per-chunk deltas jump far too much to display.

DownloadError

Thrown for every failure originating in this module. Carries cause where one exists and attempts — the probe transcript — where the failure was a probe failure.

How it works

  1. Probe. A one-byte Range GET establishes the total size, whether 206 is honoured, and which credentials mode the server answers. That mode is then reused for every chunk request: guessing wrong is the difference between a full-speed download and a silent 403.
  2. Pool. min(connections, chunkCount) workers pull chunk indices off a shared counter. Each fetches its range, validates the length and stores the buffer at its index, so ordering never depends on completion order.
  3. Verify. Before assembly, every slot must be filled; after assembly, the Blob size must equal the advertised total. Either check failing raises rather than returning a plausible-looking file.
  4. Release. Blob construction copies the data, so the source buffers are dropped immediately — this halves peak memory for the rest of the run. On the failure path they are dropped too: a failed 4 GB download must not leave 4 GB pinned behind a rejected promise.

Chrome MV3: the offscreen document

examples/chrome-mv3/ is a working extension showing the architecture that makes long downloads possible under Manifest V3.

An MV3 service worker is terminated after roughly 30 seconds of inactivity — far shorter than a large download, and it takes the in-flight buffers with it. An offscreen document is a normal DOM context that stays alive as long as the extension keeps it open, so the download, the chunk buffers and the Blob assembly all live there. The service worker is reduced to routing messages and calling chrome.downloads.

The object-URL lifetime is the other half of the problem. The example revokes each URL only once chrome.downloads.onChanged reports the save has finished: revoking earlier cancels the save, and never revoking keeps the entire file in memory for the life of the document.

The example imports its own copy of the engine, because an extension cannot import from outside its root. Run npm run sync:example after changing src/.

Limitations

Worth knowing before you adopt it:

  • The whole file is buffered in memory before it is handed back. That is fine for a few hundred megabytes and wrong for tens of gigabytes. Streaming each chunk to disk would remove the ceiling at the cost of a much larger API.
  • No resume across sessions. Retries recover a failed chunk within a run; nothing is persisted if the process dies.
  • Parallelism helps against per-connection shaping, not against a genuinely saturated link. If one connection already fills the pipe, eight will not make it wider.
  • credentials is a browser concept. In Node it has no effect, and the first credentials mode simply wins.

Tests

npm test

Seven tests run against throwaway local HTTP servers, covering the well-behaved path and the failure modes that matter: an origin that ignores Range, an origin that returns short chunks, injected 503s, and mid-flight abort. Correct reassembly is asserted by SHA-256 against the source payload, not by size alone.

License

MIT — see LICENSE.

About

Zero-dependency parallel HTTP Range downloader for browsers, Chrome MV3 extensions and Node.js

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages