enhancement
major
minor
defect
critical
major
minor
major
#30200
Agent-Zugriff und Aufzeichnung: Kommandos arbeiten auf stabilen Objekt-IDs, fachliche Schlüssel nur für Aufzeichnung und Wiedergabe
Kontext
Der Agent-Zugriff (tl-layout-view-agent, #30048) und die Aufzeichnung/Wiedergabe von Benutzerinteraktionen der React-UI beruhen auf derselben headless Projektion der Controls (ScriptingTreeProjector, ScriptingSession). Objekte (Tabellenzeilen, Baumknoten, Optionen einer Auswahl, Layout-Elemente, Kanban-Karten, gezogene Objekte) wurden darin über einen fachlichen Schlüssel identifiziert: das JSON eines ModelName. Kommandos wie selectByKey, selectByKeys, activateByKey, expandByKey, selectItemByKey, selectCardByKey und dropObjects nahmen diese Schlüssel entgegen. Der Browser verwendete dagegen eigene, sitzungsbezogene IDs und andere Kommandos.
Beobachtet
In einer Anwendung (E-Home, 8.0.0-alpha12.817, hinter Apache 2.4 als Reverse Proxy) lieferte GET /agent-api/observe für ein Fenster mit einer Tabelle (~20 Zeilen), einem Baum und einem Formular mit 13 Auswahlfeldern 84 MB JSON für 175 Controls. Fast alles davon war das argsSchema der Kommandos mit Schlüsselargument, pro Control erneut eingebettet:
| Control / Kommando | Anzahl | Größe je argsSchema |
| dropdownSelect / selectByKey | 13 | 2.922.465 Bytes |
| tableView / selectByKey | 2 | 2.922.432 Bytes |
| tableView / activateByKey | 2 | 2.922.448 Bytes |
| tableView / dropObjects | 2 | 2.924.585 Bytes |
| treeView / dropObjects | 1 | 2.924.585 Bytes |
Das Schema war das JSON-Schema eines ModelName mit allen Untertypen, praktisch das gesamte Konfigurations-Typsystem. Ein zweiter Aufruf von /observe im selben Fenster wurde vom Proxy nach ~3 s mit 502 beantwortet ("Error reading from remote server"): Die Antwort wurde vollständig als String im Speicher aufgebaut, bevor sie geschrieben wurde, und ein Error (z. B. OutOfMemoryError) dabei brach die Verbindung ohne Status ab. (Die kompakte Sicht heißt mode=actions; der Parameter actions=true wurde ignoriert.)
Ursache war, dass der Agent die Objektbezeichnung der Wiedergabeschicht übernahm: Ein Agent arbeitet in einer laufenden Sitzung und braucht nur eine kurze ID, die Wiedergabe in einer neuen Sitzung braucht den sitzungsunabhängigen ModelName. Zudem waren die IDs des Browsers keine Objektidentitäten: Die Tabelle adressierte Zeilen über ihre Position (rowIndex), der Baum über identityHashCode (nicht eindeutig), die Auswahl vergab bei jedem Laden der Optionen neue IDs.
Lösung
- Stabile Objekt-ID je Control. ObjectIdIndex vergibt je angezeigtem Objekt eine ID, die stabil bleibt, solange das Objekt angezeigt wird, und in beide Richtungen auflöst. Tabelle, Baum, Auswahl, Layout und Kanban verwenden ihn. Die Tabelle adressiert Zeilen im Client-Protokoll über die Zeilen-ID statt über die Position; ein Klick nach einem Umsortieren trifft damit immer die angeklickte Zeile.
- Kommandos arbeiten nur auf IDs. Agent, Browser und Wiedergabe verwenden dasselbe Kommando-Vokabular. Argumente, die eine Objekt-ID tragen, sind mit @DisplayedObjectId markiert (bei Drag & Drop mit Verweis auf das Quell-Control). Die Schlüssel-Kommandos selectByKey, selectByKeys, activateByKey, expandByKey, collapseByKey, contextMenuByKey, selectItemByKey, selectCardByKey und dropObjects entfallen; valueChanged der Auswahl ist ein typisiertes Kommando (SelectValueArguments).
- Identitätsdienst je Control (ObjectIdentities): keyOf(id) liefert den fachlichen Schlüssel eines angezeigten Objekts, idOfKey(key) die ID. Aufgelöst wird nur unter den Objekten, die das Control gerade anzeigt (Baum: sichtbare Knoten; Tabelle: Zeilen der aktuellen Sicht, auch außerhalb des sichtbaren Ausschnitts, nicht in zugeklappten Gruppen; Auswahl: die Optionen ihres Modells; Layout/Kanban: angezeigte Elemente, Karten und Spalten). Es wird nichts aufgeklappt oder gescrollt; Aufklappen ist ein eigener, aufgezeichneter Schritt. Ein Objekt, das an mehreren Stellen angezeigt wird, erhält je Stelle eine eigene ID; sein Schlüssel löst auf die erste angezeigte Stelle auf.
- Aufzeichnung und Wiedergabe übersetzen generisch. Beim Aufzeichnen werden markierte IDs durch den Schlüssel ersetzt und ein Quell-Control durch seine Adresse; bei der Wiedergabe werden sie am jeweiligen Control in die aktuellen IDs aufgelöst, dann läuft das normale Kommando. Ein nicht angezeigtes Objekt lässt den Schritt mit der Meldung "Das Objekt wird nicht angezeigt" scheitern. Mehrfachauswahl per Strg/Umschalt wird Klick für Klick aufgezeichnet, ein Drop an der Zeile/dem Knoten und der Zone, an der er gemacht wurde.
- Zwei Projektionsmodi (ProjectionMode): Die Beobachtung des Agenten enthält IDs und Beschriftungen, Zusicherungen werden im Schlüsselmodus erfasst und geprüft und gelten damit sitzungsübergreifend. Das Kanban-Board projiziert alle angezeigten Karten je Spalte.
- Agent-API. Jedes Argumentschema steht einmal je Beobachtung in einer Tabelle schemas, die Aktionen verweisen per $ref darauf; das gilt auch für Kommandos mit @ReactParam-Parametern. Antworten werden als kompaktes JSON direkt in die Antwort geschrieben (pretty=true rückt ein). Die Beobachtung wird vollständig aufgebaut, bevor das erste Byte geschrieben wird; jeder Fehler dabei, auch ein Error, wird protokolliert und mit 500 und JSON-Fehlermeldung beantwortet. Ein unbekannter Wert für mode wird mit 400 abgelehnt. Gemessen in tl-demo-react (Objektliste, Ticket in Bearbeitung, 497 Knoten): 311 KB (vorher 3,9 MB auf demselben Stand ohne Schlüssel-Kommandos; der Bericht oben: 84 MB für 175 Controls).
- Zugriffstoken mit _ in der ID. Die ID eines Zugriffstokens besteht aus Base64url-Zeichen, die auch das Trennzeichen _` enthalten. Ein Token, dessen ID ein `_ enthielt (etwa jedes sechste), wurde mit 401 "No session." abgewiesen, weil die ID bis zum ersten _ gelesen wurde. Die ID wird jetzt über ihre feste Länge gelesen; bereits ausgestellte Tokens funktionieren damit.
- Tabulator in editierbaren Tabellen. Tab aus dem letzten Feld der bearbeiteten Zeile wählt die nächste Zeile und setzt den Fokus in ihr erstes Feld, Umschalt+Tab entsprechend in das letzte Bedienelement der vorigen Zeile, auch am Rand des geladenen Ausschnitts. Schreibgeschützte Checkboxen (aria-readonly) und unsichtbare Eingaben zählen dabei nicht als bearbeitbar.
Gespeicherte Aufzeichnungen mit den entfallenen Kommandos gibt es nicht (Aufzeichnungen liegen nur im Speicher).
Migration
- Eigene React-Controls (Unterklassen von ReactControl), die scriptingScalarState() überschreiben, überschreiben stattdessen scriptingScalarState(ProjectionMode mode); die Variante ohne Parameter ist final. Implementierungen von ScriptingNode.scriptingState() werden zu scriptingState(ProjectionMode mode). Ein Objekt wird mit ScriptingModelKey.putIdentity(projection, mode, objectIdentities(), ...) in die Projektion eingetragen statt über seinen Schlüssel.
- Ein eigenes Control, dessen Kommandos Objekte adressieren, hält deren IDs in einem ObjectIdIndex, liefert ihn aus objectIdentities() und markiert die ID-Argumente seiner typisierten Argument-Schnittstellen mit @DisplayedObjectId (Listen zusätzlich mit @JsonBinding(SimpleJsonListBinding.class)). Ein recordCommand-Override, der nur IDs in Schlüssel übersetzt, entfällt.
- Entfallene Typen: ModelKeyArguments (die Konstante KEY liegt jetzt in ScriptingModelKey), SelectByKeysArguments, SelectByNameArguments, DropObjectsArguments sowie die *ByKeyArguments von Tabelle und Baum.
- Agenten und Werkzeuge an `/agent-api': Die Schlüssel-Kommandos gibt es nicht mehr. Objekte werden mit den IDs aus der Beobachtung adressiert (key/`nodeId`/`rowId`/`value`, siehe das jeweilige Argumentschema); die Tabelle nimmt rowId statt rowIndex, drop erwartet keys als JSON-Array. Argumentschemas stehen unter schemas und werden per $ref referenziert, ein params-Feld gibt es nicht mehr. Die kompakte Sicht heißt mode=actions, pretty=true liefert eingerückte Ausgabe.