enhancement
major
minor
major
minor
major
#29970
React-Bridge: Ein Sprung, der seine Zielseite erst erzeugt, malt sie ohne die gesetzte Auswahl
Betrifft jede React-View-Anwendung, die ein Objekt über die Navigation (DisplayTargetService / show-object) auf einer Seite zeigt, die der Sprung erst öffnet. Gefunden in Pietro (Openium/Pietro #1031) am 24.10.2026, dort nicht behebbar.
Problem
- Beobachtet: Ein Sprung ("In der Akte zeigen", "Lauf öffnen") öffnet die Zielseite, schreibt die Kanäle (Reiter, Preset, Auswahl) auf dem Server richtig, aber der Client malt die Seite mit dem alten Zustand: der vorige Reiter mit der vorigen Auswahl, oder die Liste ohne Auswahl. Die URL stimmt. Ein Neuladen des Tabs zeigt das richtige Ziel.
- Erwartet: Die erzeugte Seite malt den Zustand, den der Sprung gesetzt hat.
- Reproduzieren (deterministisch): App laufen lassen, Tab offen halten, App neu starten, im selben Tab neu anmelden (Log: "SSE connection REPLACED for window …"), dann einen Sprung auf eine noch nicht geöffnete Seite auslösen. In Pietro: Läufe → Lauf → Zeile "In der Akte zeigen". 5 von 5 in Tabs, die den Neustart überlebt haben; 0 von 9 in frischen Tabs. Ohne Neustart tritt es selten auf, wenn der Sprung weniger als etwa 0,5 s nach einer anderen Aktion kommt (2 von 45 mit 2,5 s Pause, 3 von 11 mit 0,4 s Pause).
- Umgebung: TopLogic 8.0.0-alpha10, tl-react-bridge, Chromium 1440x900.
Ursache
Der Sprung erzeugt in einem Request zuerst das Control der Zielseite und setzt danach die Kanäle. Auf den Ereignisstrom gehen dadurch zwei Ereignisse in dieser Reihenfolge:
E1: PatchEvent(Elternteil, { content: {controlId: c7, state: {selection: null, ...}} })
E2: PatchEvent(c7, { selection: X })
E1 trägt das neue Kind als Deskriptor mit dem Zustand im Moment seiner Erzeugung, E2 die Änderung durch den Sprung. Der Server arbeitet richtig.
Im Browser wendet der Store des Elternteils E1 an; React rendert danach, aber nicht sofort. Für c7 gibt es noch keinen Empfänger: createChildContext legt Store und Abonnement eines Kind-Controls erst an, wenn TLChild es rendert. sse-client.ts verwirft E2 deshalb kommentarlos, und das Kind mountet mit dem Zustand aus E1.
Nach einem Neustart ist der Fall deterministisch: der Ereignisstrom wird abgewiesen, die Bridge schaltet auf Polling um und merkt sich das für den Tab (sessionStorage tl-react-sse-polling). Eine Poll-Antwort bringt E1 und E2 zusammen und verteilt sie in einer Schleife, React kann dazwischen nicht rendern. Im Strom kommen die Ereignisse einzeln, und React rendert meist dazwischen - außer der Browser ist gerade beschäftigt.
Lösung
Der Store eines Kind-Controls entsteht, wenn sein Deskriptor ankommt, nicht erst, wenn React es rendert. Wendet ein Store der Bridge einen Zustand oder Patch an, legt er für jeden darin enthaltenen Kind-Deskriptor (controlId, module, state) sofort den Store des Kindes an (bzw. aktualisiert ihn) und abonniert ihn am Ereignisstrom. Ein nachfolgender Patch für das Kind trifft damit seinen Store, in der Reihenfolge des Stroms; TLChild findet den Store beim Rendern vor. Ein so angelegter Store, der nie gerendert wird, gehört dem Control, dessen Ereignis ihn mitbrachte, und wird mit diesem abgeräumt.
Die React-Bridge (com.top_logic.layout.react) bekommt dafür Vitest-Tests, die wie in tl-chat in der test-Phase laufen.
Erledigt wenn
- Die Reproduktion oben zeigt in einem Tab nach Neustart das richtige Ziel mit Auswahl, ohne Neuladen.
- Ein Test der React-Bridge, der auf dem heutigen Stand fehlschlägt: ein Patch für ein Kind trifft vor dessen Render ein und ist nach dem Mount sichtbar.
Verweise
- Pietro #1031 (Anzeigeziele), Berichte der Worker wf-jump und wf-final-check (Openium, lokal).