Startseite / Artikel

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.

Verlag: REACTAPP.TOP6 Min. Lesezeit1360 Wörter

Quellen: Handling concurrency on the Web with Web Locks API

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
  • Einfache API – kein benutzerdefiniertes Messaging-Protokoll
  • Browserkompatibilität – aufgeführt in der MDN-Kompatibilitätsliste für aktuelle Browser
  • 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:

    1. Keine doppelten Uploads aufgrund von Konkurrenzsituationen bei der Wiederherstellung über mehrere Tabellen
    2. Besseres Benutzererlebnis – die Koordination blockierte nicht die Benutzeroberfläche
    3. Geringere Infrastrukturkosten – weniger doppelte Übertragungen und gespeicherte Objekte
    4. 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
  • Hintergrundsynchronisierung, die nur einmal ausgeführt werden muss
  • Jede Singleton-Aufgabe, die nicht in parallelen Tabellen ausgeführt werden sollte
  • 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:

    1. Blockieren Sie niemals Benutzeraktionen – bevorzugen Sie für von dem Benutzer gestartete Gesten nicht-blockierende Sperrmechanismen
    2. Sicher versagen – reduzieren Sie die Leistung, anstatt Uploads vollständig zu stören
    3. 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“.