Koordinieren Sie mehrere Tab-Uploads mithilfe der Web Locks API
Verhindern Sie doppelte, wiederholbare Uploads in verschiedenen Browser-Tabs, indem Sie die Tricks mit localStorage und BroadcastChannel durch navigator.locks sowie einen React-Upload-Koordinator ersetzen.
Konkurrenz bedeutet, gleichzeitig an mehreren Aufgaben zu arbeiten – oder zumindest so zu erscheinen –, damit die Software weiterhin reaktiv bleibt. Ein Koch, der währenddessen, als eine Pfanne simmert und eine andere gebacken wird, weiter schneidet, erledigt die Aufgaben konkurrenzweise anstatt die Gerichte strikt nacheinander fertigzustellen.
Früher schien die Arbeit am Frontend weit entfernt von klassischen mehrthreadigen Systemen mit Sperrmechanismen, Semaphoren und Deadlocks zu sein. Die moderne Webplattform hat das geändert. Durch Workers, WebGL, Canvas und Service Workers müssen Browser-Apps heute Koordinationsschwierigkeiten bewältigen, die stark an Rennen um gemeinsam genutzte Speicher erinnern.
Das Problem
Ein Engineering-Team eines Marktplatzes benötigte zuverlässige Uploads für sehr große Dateien – im Bereich von Dutzenden Gigabyte, bis zu etwa 20 GB. Es ist problematisch, die Nutzer dazu aufzufordern, die Seite während des gesamten Transfers geöffnet zu halten. Das Produktziel bestand darin, Uploads fortsetzen zu können: Das Schließen und Wiederöffnen einer Seite sollte den Transfer ohne manuelle Neustart fortsetzen.
Durch die Möglichkeit, den Transfer fortzusetzen, entsteht ein neuer Fehlermodus. Wenn mehrere Tabs denselben laufenden Upload wiederherstellen, kann jeder Tab den Transfer unabhängig neu starten. Dadurch erhält der Objektspeicher doppelte Datenströme, die Bandbreite wird verschwendet, die Verarbeitungskosten steigen und die Fortschrittsanzeige kann unruhig reagieren, da sich die Tabs um denselben Auftrag streiten.
Das Problem ist, dass jede Registerkarte für sich genommen eine sinnvolle Aufgabe erfüllt. Die Wiederherstellung der Sitzung findet ein unvollständiges Upload-Datensatz, der Client setzt den mehrteiligen Transfer fort und die Benutzeroberfläche gibt den Fortschritt an. Wenn zwei Registerkarten geöffnet sind, führt derselbe „sinnvolle“ Ablauf dazu, dass zwei Schreiber auf denselben entfernten Objekt-Schlüssel zugreifen.
Ohne Koordinator glauben beide wiederhergestellten Registerkarten, sie besäßen das Upload-Datensatz, und beide senden Daten. Die Lösung erfordert eine gegenseitige Ausschließung zwischen den Registerkarten; Browser stellen dieses Konzept über Web Locks bereit.
Vor den Implementierungsdetails hilft es, sich daran zu erinnern, wofür ein Schloss dient.
Zuerst: Was ist ein Schloss?
Ein Schloss serialisiert den Zugriff auf eine gemeinsame Ressource, sodass die Aufrufer einen konsistenten Zustand beobachten können. Im klassischen Bankbeispiel lesen und schreiben mehrere Geldautomaten gleichzeitig den gleichen Kontostand. Ohne Synchronisierung verlaufen Abhebungen und Einzahlungen unsicher ineinander.
Mit einem Schloss führt der erste Aufrufer, der es erhält, seine Lese- oder Schreiboperation durch; die anderen warten, bis es freigegeben wird. Derselbe Ansatz gilt auch für die Regel „Nur ein Tab darf diesen Upload fortsetzen.“
Das Bild der Koordination zwischen Tabs
Mehrere ältere Browser-Muster versuchen, die Tab-Interaktionen zu koordinieren. Sie funktionieren solange, bis es zu Wettläufen, Abstürzen oder Protokollfehlern kommt.
Option 1: localStorage + Polling
Ein gängiges selbstgebautes Mutex speichert ein Flag in localStorage:
// Simplified example - DON'T use this
if (!localStorage.getItem('upload-lock')) {
localStorage.setItem('upload-lock', Date.now());
// Start upload
}
Schnell treten Probleme auf:
- Wettlaufbedingungen: Lese- und Schreibvorgänge sind nicht atomar
- Ausgelaufene Schlösser: Ein abgestürzter Tab lässt das Flag dauerhaft gesetzt
- Overhead durch Polling: Andere Tabs müssen ständig nachprüfen
- Beschränkungen bei Events: Speicher-Events werden in anderen Tabs ausgelöst, nicht beim Schreibenden
Option 2: Broadcast Channel API
Tabs können über BroadcastChannel eine Kommunikation zwischen Dokumenten herstellen:
const channel = new BroadcastChannel('upload-coordination');
channel.postMessage({ type: 'REQUEST_UPLOAD_PERMISSION' });
Es bestehen weiterhin Herausforderungen:
- Keine Mutex-Primitivfunktion: Man muss selbst eine Zustandsmaschine entwickeln
- Race Conditions: Überschneidende Nachrichten führen weiterhin zu Konflikten
- Komplexes Protokoll: Heartbeats, Absturzerkennung sowie Konfliktregeln müssen manuell implementiert werden
- Fehleranfälligkeit: Probleme mit der Reihenfolge und dem Timing sind häufig
Die Web Locks API: Ein browser-eigener Mutex
Die eingebauten Web Locks bieten einen atomaren Browser-Mutex. Zu den nützlichen Eigenschaften gehören:
- Atomare Sperreerlangung — vermeidet Race Conditions bei localStorage
- Automatische Bereinigung — das Schließen oder Abstürzen eines Tabs führt zur Freigabe der Sperre
- Nicht-blockierende Optionen — halten die Benutzeroberfläche reaktiv
Unsere Implementierung: useUploadCoordinator
Ein benutzerdefinierter React-Hook umschließt die Strategie zur Steuerung des Upload-Besitzes.
Die Kernstrategie
Interaktive Starts sollten anders behandelt werden als stille Wiederherstellungen. Wenn jemand ausdrücklich eine Datei auswählt, sollte ein nicht-blockierender Zugriff bevorzugt werden, damit die Benutzeroberfläche nie auf einer anderen Registerkarte wartet. Ist der Lock frei, wird er genutzt, um die Datei hochzuladen; ist er bereits belegt, wird weiterhin nach den Produktregeln entschieden, anstatt die Seite einzufrieren.
Wiederhergestellte Uploads sollten strenger reguliert werden: Nur die erste Registerkarte, die den Besitz übernimmt, sollte mit dem Transfer fortfahren. Spätere Registerkarten, bei denen der Lock nicht verfügbar ist, sollten den aktuellen Besitzer in Ruhe lassen, anstatt einen zweiten Stream zu öffnen.
// 1. Fresh uploads (user-initiated): Non-blocking
// Try to get the lock, but proceed regardless
navigator.locks.request(LOCK_NAME,
{ ifAvailable: true },
async (lock) => {
if (lock) {
// Hold the lock until upload completes
await holdLockUntilComplete(abortController);
}
// Upload proceeds either way - never block the user
}
);
// 2. Restored uploads: Conditional
// Only proceed if we get the lock
navigator.locks.request(LOCK_NAME,
{ ifAvailable: true },
async (lock) => {
if (lock) {
uploader.emit("restore-confirmed"); // Proceed with restore
await holdLockUntilComplete(abortController);
}
// If no lock, skip silently - another tab is handling it
}
);
Wichtige Implementierungsdetails
Halten Sie den Schutzmechanismus bis zum Abschluss oder Abbruch der Übertragung gedrückt:
const holdLockUntilComplete = (abortController: AbortController) => {
return new Promise<void>((resolve) => {
const cleanup = () => {
// Remove all listeners and resolve (releasing the lock)
uploader.off("complete", cleanup);
uploader.off("error", cleanup);
uploader.off("cancel-all", cleanup);
resolve();
};
// Release lock on any terminal event
uploader.on("complete", cleanup);
uploader.on("error", cleanup);
uploader.on("cancel-all", cleanup);
// Also release if component unmounts
abortController.signal.addEventListener("abort", cleanup);
});
};
Erkennen Sie die Unterstützung und gehen Sie sicher auf ein Ersatzverhalten über, wenn die API fehlt:
const checkWebLocksAvailable = (): boolean => {
return typeof navigator !== 'undefined' && 'locks' in navigator;
};
// Fallback behavior
if (!checkWebLocksAvailable()) {
// Allow restores to proceed (accept risk of duplicates)
uploader.emit("restore-confirmed");
return;
}
Vorteile in der Praxis
Nach dem Versand des Koordinators stellte das Team fest:
- Keine doppelten Uploads aufgrund von Konkurrenzsituationen bei der Wiederherstellung über mehrere Tabellen
- Besseres Benutzererlebnis – die Koordination blockierte nicht die Benutzeroberfläche
- Geringere Infrastrukturkosten – weniger doppelte Übertragungen und gespeicherte Objekte
- Einfachere Codebasis – keine Abfrageschleifen oder manuell erstellten Zustandsmaschinen
Nur die Tabellen, die den Schutzmechanismus gewinnen, setzen den Upload fort; die Verlierer erkennen den gehaltenen Schutzmechanismus und beenden ihre Aktion.
Praktische Überlegungen
Wann dieses Muster verwendet werden sollte:
- Dateiuploads, die nach einem Neuladen wiederhergestellt werden
Leistungsbeeinflussung:
- Minimal – es gibt keine Abfrageschleife
- Automatische Bereinigung begrenzt die Anzahl der verbleibenden Wartender
- Nicht-blockierende Anfragen halten die Interaktion reaktiv
Teststrategie:
- Testen Sie alle unterstützten Browser, die Sie bereitstellen
- Simulieren Sie plötzliche Tab-Abstürze, während ein Schutzmechanismus aktiv ist
- Überprüfen Sie das Fallback-Verhalten, wenn Web Locks nicht verfügbar sind
Fazit
Zur Koordination zwischen Tabellen ist kein anfälliges benutzerdefiniertes Protokoll erforderlich. Verlassen Sie sich auf die Mutex-Funktion der Plattform Web Locks: Sie bereinigt Ressourcen beim Verlassen des Tabs und unterstützt nicht-blockierende Zugriffe. Weitere API-Details finden Sie in der Dokumentation zu Web Locks.
Praktische Regeln, die in der Produktion funktioniert haben:
- Blockieren Sie niemals Benutzeraktionen – bevorzugen Sie für von dem Benutzer gestartete Gesten nicht-blockierende Sperrmechanismen
- Sicher versagen – reduzieren Sie die Leistung, anstatt Uploads vollständig zu stören
- Bleiben Sie einfach – nutzen Sie Plattform-Primitiven, anstatt diese neu zu entwickeln
Diese drei Regeln sind wichtiger als irgendein spezifischer Hilfsnamen. Ein ausgefeilter Koordinator, der den Klickpfad einfriert, fühlt sich schlimmer an als ein einfacher Ersatzmechanismus, der den Benutzer gelegentlich auffordert, es erneut zu versuchen. Ebenso wird ein cleveres Messaging-Protokoll, das die Fehlerbehebung vergisst, das Problem der veralteten Sperrungen erneut verursachen, das localStorage so problematisch machte.
Das gleiche Muster gilt immer dann, wenn Tabs einen einzigen Eigentümer für gemeinsame Arbeiten wählen müssen – nicht nur bei großen Uploads. Hintergrund-Synchronisierungswarteschlangen, Offline-Mutation-Flusher sowie einmalige Migrationsskripte profitieren alle von demselben Prinzip „einer Gewinner, viele höfliche Verlierer“.