Ein abhängigkeitsfreier paralleler HTTP-Range-Downloader, der unverändert im Browser, in Chrome-MV3-Erweiterungen und unter Node.js 18+ läuft.
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}% mit ${megabytesPerSecond.toFixed(2)} MB/s`)
});Eine einzelne HTTP-Verbindung ist häufig nicht der Engpass, den das Netz darstellt. Viele Server drosseln den Durchsatz pro Verbindung: Ein Stream bleibt weit unter der verfügbaren Leitungsgeschwindigkeit, während acht parallele Streams sie ausschöpfen. Die Lösung ist bekannt — Byte-Bereiche gleichzeitig anfordern und wieder zusammensetzen — doch genau in den Details beschädigen sich Downloads unbemerkt selbst.
Diese Bibliothek existiert, weil sich diese Details lohnen, einmal sauber festgehalten zu werden:
- Ein Server kann eine Range-Anfrage mit
200und dem vollständigen Body beantworten statt mit206und dem angeforderten Ausschnitt. Wird das eingefügt, verschiebt sich jedes nachfolgende Byte — und die Datei fällt erst viel später auf, in dem Programm, das sie liest. - Ein Chunk kann zu kurz zurückkommen. Nimmt man ihn an, entsteht eine Datei, die vollständig aussieht und es nicht ist.
- Credentials sind entscheidend. Eine Anfrage, die Cookies benötigt, aber ohne sie gesendet wird, erhält
403— und eine Prüfung, die nur „Größe unbekannt“ meldet, verbirgt den eigentlichen Grund.
Jeder dieser Fälle wird ausdrücklich behandelt und ist durch einen Test abgedeckt.
- Parallele Byte-Range-Downloads über einen begrenzten Worker-Pool — die Verbindungszahl ist eine Obergrenze, kein Thread pro Chunk.
- Automatischer Rückfall. Unterstützt der Server keine Ranges, wird der Download trotzdem einspurig abgeschlossen, statt zu scheitern.
- Byteweise Überprüfung. Jeder Chunk wird gegen seine erwartete Länge geprüft, das Ergebnis gegen die angegebene Gesamtgröße.
- Wiederholungen mit Backoff, mit eigenem Budget für Server, die Ranges ignorieren, damit diese nicht die Versuche aufbrauchen, die für echte Netzfehler gedacht sind.
- Fortschritt mit geglätteter Geschwindigkeit und Restzeit.
AbortSignal-Unterstützung durchgängig, einschließlich der Backoff-Pausen.- Diagnostizierbare Fehler. Fehler tragen das Protokoll der Prüfversuche (
error.attempts) — aus „Download fehlgeschlagen“ wird eine Liste dessen, was versucht wurde und was jeweils zurückkam. - Keine Abhängigkeiten. Nur
fetch,BlobundAbortSignal.
Kein Build-Schritt, kein Paketmanager nötig — das Modul ist reines ESM.
git clone https://github.com/tafirnat/parallel-range-downloader.git
cd parallel-range-downloader
npm test # führt die Tests gegen lokale HTTP-Server ausFür das eigene Projekt genügt es, src/range-downloader.js zu kopieren oder dieses Repository als Abhängigkeit einzubinden.
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.
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);
// ... an den Download übergeben, danach immer:
URL.revokeObjectURL(objectUrl);Prüft die URL und lädt dann parallel, wenn der Server es zulässt, sonst einspurig. Das ist der Einstiegspunkt für die meisten Anwendungsfälle.
Liefert { blob, size, parallel, attempts }; parallel zeigt an, welcher Weg genommen wurde.
Ermittelt Dateigröße und Range-Unterstützung, ohne die Datei zu laden.
Liefert { size, rangeSupported, init, attempts }. Ein size von 0 bedeutet, dass der Server überhaupt nicht gelesen werden konnte — attempts nennt den Grund.
Verwendet wird ein Ein-Byte-Range-GET statt HEAD, denn viele Server beantworten HEAD anders als GET (oder lehnen es ab), und die Frage „lieferst du Ranges“ ist nur verlässlich beantwortet, wenn tatsächlich eine Range angefordert wurde. HEAD bleibt als Rückfallebene erhalten.
Die Engine selbst, wenn die Größe bereits bekannt ist. Benötigt options.size. Liefert einen Blob.
| Option | Standard | Gilt für | Bedeutung |
|---|---|---|---|
size |
— | downloadParallel |
Gesamtgröße in Bytes. Erforderlich. |
chunkSize |
10 MiB |
download, parallel | Bytes pro Range-Anfrage. |
connections |
8 |
download, parallel | Maximale gleichzeitige Anfragen. |
retries |
4 |
download, parallel | Versuche pro Chunk vor dem Abbruch. |
onProgress |
— | download, parallel | Wird nach jedem fertigen Chunk aufgerufen. |
fetchInit |
{} |
alle | Wird in jedes fetch übernommen (Header, Mode, ...). |
credentialModes |
include, dann omit | probe, download | Zu testende Credentials-Modi, in dieser Reihenfolge. |
signal |
— | alle | AbortSignal. |
{
downloadedBytes, totalBytes, percent,
bytesPerSecond, megabytesPerSecond, etaSeconds,
completedChunks, totalChunks
}Die Geschwindigkeit wird über 500-ms-Fenster geglättet; die rohen Werte pro Chunk schwanken für eine Anzeige viel zu stark.
Wird bei jedem Fehler aus diesem Modul geworfen. Trägt cause, sofern vorhanden, und attempts — das Protokoll der Prüfversuche —, wenn der Fehler bei der Prüfung entstand.
- Prüfen. Ein Ein-Byte-Range-
GETklärt Gesamtgröße,206-Unterstützung und den Credentials-Modus, auf den der Server antwortet. Dieser Modus wird für alle Chunk-Anfragen wiederverwendet: Die falsche Wahl ist der Unterschied zwischen vollem Tempo und einem stillen403. - Pool.
min(connections, chunkCount)Worker holen sich Chunk-Indizes von einem gemeinsamen Zähler. Jeder lädt seinen Bereich, prüft die Länge und legt den Puffer an seinem Index ab — die Reihenfolge hängt damit nie von der Fertigstellungsreihenfolge ab. - Verifizieren. Vor dem Zusammensetzen muss jeder Platz belegt sein, danach muss die
Blob-Größe der angegebenen Gesamtgröße entsprechen. Schlägt eine der Prüfungen fehl, wird ein Fehler geworfen, statt eine plausibel wirkende Datei zurückzugeben. - Freigeben. Beim Erzeugen des
Blobwerden die Daten kopiert, daher werden die Quellpuffer sofort verworfen — das halbiert den Spitzenspeicher für den Rest des Laufs. Auch im Fehlerfall werden sie verworfen: Ein gescheiterter 4-GB-Download darf keine 4 GB hinter einer abgelehnten Promise festhalten.
examples/chrome-mv3/ ist eine lauffähige Erweiterung, die die Architektur zeigt, mit der lange Downloads unter Manifest V3 überhaupt möglich sind.
Ein MV3-Service-Worker wird nach etwa 30 Sekunden Inaktivität beendet — weit weniger, als ein großer Download braucht, und er nimmt die laufenden Puffer mit. Ein Offscreen-Dokument ist ein normaler DOM-Kontext, der so lange lebt, wie die Erweiterung ihn offen hält. Download, Chunk-Puffer und Blob-Aufbau liegen deshalb dort; der Service-Worker beschränkt sich auf Nachrichtenvermittlung und chrome.downloads.
Die Lebensdauer der Object-URL ist die andere Hälfte des Problems. Das Beispiel gibt jede URL erst frei, wenn chrome.downloads.onChanged das Ende des Speicherns meldet: Früheres Freigeben bricht den Speichervorgang ab, gar kein Freigeben hält die gesamte Datei für die Lebensdauer des Dokuments im Speicher.
Das Beispiel bindet eine eigene Kopie der Engine ein, da eine Erweiterung nicht außerhalb ihres Wurzelverzeichnisses importieren kann. Nach Änderungen an src/ bitte npm run sync:example ausführen.
Vor dem Einsatz zu wissen:
- Die gesamte Datei wird im Speicher gehalten, bevor sie zurückgegeben wird. Für einige hundert Megabyte ist das unproblematisch, für zweistellige Gigabyte-Größen falsch. Jeden Chunk direkt auf die Festplatte zu schreiben würde diese Grenze aufheben, aber eine deutlich größere API erfordern.
- Kein Fortsetzen über Sitzungen hinweg. Wiederholungen fangen einen fehlgeschlagenen Chunk innerhalb eines Laufs ab; stirbt der Prozess, ist nichts gespeichert.
- Parallelität hilft gegen Drosselung pro Verbindung, nicht gegen eine tatsächlich ausgelastete Leitung. Füllt eine Verbindung die Leitung bereits, machen acht sie nicht breiter.
credentialsist ein Browser-Konzept. Unter Node hat es keine Wirkung, und der erste Modus gewinnt schlicht.
npm testSieben Tests laufen gegen kurzlebige lokale HTTP-Server und decken den regulären Weg sowie die relevanten Fehlerfälle ab: ein Server, der Ranges ignoriert, ein Server, der zu kurze Chunks liefert, eingestreute 503-Antworten und ein Abbruch mitten im Download. Die korrekte Zusammensetzung wird per SHA-256 gegen die Ausgangsdaten geprüft, nicht nur über die Größe.
MIT — siehe LICENSE.