GitHub
Prototyppackages/safe-array/src

Safe Array

@ralphschuler/safe-array

Verzögert Adds und Removes, damit Mutationen eine laufende Iteration nicht unkontrolliert verschieben.

iterationdeferred mutationcollection

01 · Problem

Wofür braucht man das?

Direktes Hinzufügen oder Entfernen während forEach kann Elemente überspringen, doppelt besuchen oder die Schleifenlogik schwer vorhersagbar machen.

02 · Denkmodell

Das mentale Modell

Die Iteration sieht einen Snapshot. Mutation landet in Queues und wird nur an klaren Flush-Grenzen atomar übernommen.

Im Repository

SafeArray exportiert add, remove, filter und forEach. addQueued leert die Add-Queue nie; dadurch werden alte Adds in jedem Durchlauf erneut angehängt und auch length/isEmpty falsch.

03 · Kontrollfluss

Was passiert in welcher Reihenfolge?

  1. Add und Remove in getrennten Pending-Strukturen sammeln.
  2. Vor Iteration einen Flush durchführen und beide Queues leeren.
  3. Über einen stabilen Snapshot iterieren.
  4. Währenddessen gesammelte Änderungen erst nach der Schleife übernehmen.

04 · Bauteile

Die entscheidenden Verträge

SafeArray.add / remove Stellt Werte in Mutationsqueues.
forEach Flusht, iteriert und verarbeitet Removals noch einmal.
filter Baut ein neues Array über SafeArray.forEach.
length / isEmpty Berechnet Werte aus Basisarray und Queues, gegenwärtig durch Add-Duplikate falsch.

05 · Build it yourself

Selbst implementieren

Definiere zuerst, ob Änderungen aus Callback N noch im selben oder erst im nächsten Durchlauf sichtbar werden.

  1. Konstruktorinput kopieren und Pending-Adds sowie Remove-Set anlegen.
  2. Flush übernimmt jede Queue genau einmal und leert sie sofort.
  3. Iteration arbeitet auf einer Kopie oder einem unveränderten Basispuffer.
minimal.ts · unabhängig vom Package
class DeferredList<T> {
  private values: T[] = [];
  private adds: T[] = [];
  private removes = new Set<T>();

  add(value: T) { this.adds.push(value); }
  remove(value: T) { this.removes.add(value); }
  flush() {
    this.values = this.values.filter(value => !this.removes.has(value));
    this.values.push(...this.adds);
    this.removes.clear();
    this.adds = [];
  }
}

06 · Verifizieren

Was du testen solltest

  • Ein Add erscheint nach beliebig vielen Flushes genau einmal.
  • Remove während Iteration folgt der dokumentierten Sichtbarkeitsregel.
  • Add und Remove desselben Objekts in einem Zyklus haben eine festgelegte Reihenfolge.

07 · Grenzen

Kompromisse und Stolperfallen

  • Snapshots sind klar und erzeugen O(n)-Kopien.
  • Referenzgleichheit genügt für Objekte nur bei stabiler Identität.
  • Deferred Mutation schützt Iterationen, verzögert aber sichtbare Zustandsänderungen.
Wichtig

Das Konstruktorarray sollte kopiert werden, damit externe Mutation die internen Invarianten nicht umgeht.

08 · Weiterdenken

Quellcode und Nachbarn

Originalcode auf GitHub ansehen