Issue #99: PageNetworkManager — Unified Network Stack + Shared-State Fix (fixes #89) #94

Closed
opened 2026-06-19 08:52:53 +00:00 by Artur · 1 comment
Owner

Problembeschreibung (DE)

Zwei fundamentale Architektur-Probleme:

1. Module-Level Shared State in DynamicScriptHandler

In src/js/dynamic-scripts.ts (Zeile 61) existiert module-level Shared State:

// MODULE SCOPE — ALLE Pages teilen sich diesen State!
const processingElements = new WeakMap<any, boolean>();

Konsequenz: Test A erstellt Page -> laedt Scripts -> processingElements hat Eintraege. Test B erstellt neue Page -> DIESELBE processingElements Map wird weitergenutzt -> Scripts werden faeelschlicherweise als "bereits verarbeitet" erkannt -> 6 Test-Failures.

2. Kein Unified Network Stack per Page

Aktuell verteilen sich Requests auf verschiedene, unabhaengige Komponenten:

Request-Typ Handler State
fetch() execution-realm.ts (InstrumentedFetch) Modular
XMLHttpRequest execution-realm.ts (XHR Proxy) Modular
Script-Loading DynamicScriptHandler -> ScriptLoader Per-Instance
Image/Media Loading Gar nicht instrumentiert Keine
WebSocket execution-realm.ts Modular
Form-Submit form-action.ts Modular
ServiceWorker Gar nicht implementiert Keine

Echter Browser: EIN Network-Stack pro Page/Ursprung. ALLES geht durch:

Request -> ServiceWorker -> HTTP Cache -> Cookie Jar -> Network -> Response -> SW Cache

Analyse

Problem 1 Fix: Per-Instance processingElements

class DynamicScriptHandler {
  // Vorher (module-level):
  // const processingElements = new WeakMap<any, boolean>();
  
  // Nachher (instance-level):
  private processingElements = new WeakMap<any, boolean>();
  private loadedUrls = new Set<string>(); // bereits per-instance ✓
}

Problem 2: PageNetworkManager Architektur

class PageNetworkManager {
  // State per Page:
  private _cookieJar: CookieJar;
  private _cache: Map<string, CacheEntry>;
  private _serviceWorker: ServiceWorker | null;
  private _interceptors: RequestInterceptor[];
  
  // EINZIGER Entry-Point fuer ALLE Requests:
  async request(method: string, url: string, options?: RequestOptions): Promise<Response> {
    // 1. Request-Interception (Extensions: adblock, proxy)
    for (const interceptor of this._interceptors) {
      const result = interceptor({ method, url, headers, body });
      if (result) return result; // abgefangen
    }
    
    // 2. ServiceWorker (if installed)
    if (this._serviceWorker) {
      const swResponse = await this._serviceWorker.fetchEvent(request);
      if (swResponse) return swResponse;
    }
    
    // 3. HTTP Cache
    const cached = this._cache.get(url);
    if (cached && !stale(cached)) return cached.response;
    
    // 4. Cookie-Jar
    const cookies = this._cookieJar.getCookies(url);
    if (cookies) request.headers.set('Cookie', cookies);
    
    // 5. Fetch (via Bun oder curl)
    const response = await fetch(request);
    
    // 6. Set-Cookies speichern
    const setCookie = response.headers.get('Set-Cookie');
    if (setCookie) this._cookieJar.setCookies(url, setCookie);
    
    // 7. Cache schreiben
    this._cache.set(url, { response, timestamp: Date.now() });
    
    // 8. Response-Interception
    return response;
  }
  
  // Convenience-Methoden delegieren alle an request():
  fetch = (url: string, init?: RequestInit) => this.request('GET', url, init);
  xhr(method: string, url: string, ...) { /* -> request() */ }
  loadScript(url: string) { /* -> request() */ }
  loadImage(url: string) { /* -> request() */ }
  websocket(url: string) { /* -> request() */ }
}

Integration mit Page:

class Page {
  network: PageNetworkManager; // EINE Instanz pro Page
  
  constructor() {
    this.network = new PageNetworkManager(this);
    this.dynamicScripts = new DynamicScriptHandler(this.network, window, document);
    // Kein Module-Level State mehr
  }
}

Optionen

Option A — Minimal: Nur Shared-State fixen (EMPFEHLUNG fuer sofort)

  • processingElements von Module-Level zu Instance-Level verschieben
  • Test Failures verschwinden (6/1545)
  • Aufwand: ~30 Minuten

Option B — Full PageNetworkManager (EMPFEHLUNG)

  • Alles aus Option A + voller NetworkManager
  • Alle Request-Typen gehen durch EINE Pipeline
  • Cookie-Jar, HTTP-Cache, ServiceWorker vorbereitet
  • Aufwand: ~8h Kern + 4h Integration

Option C — Hybrid: Minimal fix + NetworkManager Grundstruktur

  • Erst den Shared-State fixen (Option A)
  • Dann schrittweise NetworkManager aufbauen:
    1. Phase 1: Grundstruktur + fetch()-Routing
    2. Phase 2: XHR + Script-Loading umstellen
    3. Phase 3: Cookie-Jar + Cache
    4. Phase 4: Image/Media/Form integrieren

Entscheidung

Option C — Shared-State fixen (Red Alert, blockiert Tests) + NetworkManager in Phasen. Phase 1 ist der Shared-State Fix. Phase 2+ folgen iterativ.

Akzeptanzkriterien

  • processingElements ist per DynamicScriptHandler-Instance, nicht module-level
  • loadedUrls ist bereits per-instance (bleibt unveraendert)
  • Alle 6 Shared-State-Failures sind verschwunden
  • PageNetworkManager Klasse existiert mit request() Methode
  • fetch() geht durch PageNetworkManager
  • XMLHttpRequest geht durch PageNetworkManager
  • Script-Loading (DynamicScriptHandler) geht durch PageNetworkManager
  • Cookie-Jar ist per Page (nicht global)
  • HTTP-Cache ist per Page (nicht global)
  • Keine Regression: Alle 1545 Tests bleiben gruen

Betroffene Dateien

Datei Aenderung
src/js/dynamic-scripts.ts processingElements -> instance-level (Zeile 61)
src/network/network-manager.ts NEU: PageNetworkManager Klasse
src/network/cookie-jar.ts NEU: Per-Page Cookie Jar
src/pages/page.ts network Property hinzufuegen
src/runtime-isolation.ts fetch/XHR durch NetworkManager ersetzen
src/js/script-loader.ts load() durch NetworkManager.request()
src/execution-realm.ts InstrumentedFetch entfernen (durch NetworkManager)
tests/unit/network-manager.test.ts NEU: NetworkManager Tests

Technische Risiken

  • Bestehende Instrumentierung: execution-realm.ts hat eigene fetch/XHR-Proxy. Umstellung auf NetworkManager kann Race-Conditions ausloesen.
  • Script-Loading Reihenfolge: DynamicScriptHandler hat sequentielle Chunk-Queue. NetworkManager muss asynchrone Requests parallel erlauben (wie Browser) aber Execution sequentiell halten (fuer TDZ).
  • WebSocket: Geht nicht durch fetch() -> braucht eigenen Pfad im NetworkManager.

Dependencies

  • Issue #89 (DynamicScriptHandler Queue-Konsolidierung) — betrifft shared state
  • Issue #98 (RAF Vereinfachung) — kein direkter Zusammenhang

Testplan

  1. Unit: DynamicScriptHandler erzeugt neue WeakMap pro Instanz
  2. Unit: Zwei Pages haben unabhaengige processingElements
  3. Unit: NetworkManager.request() routed fetch korrekt
  4. Unit: Cookie-Jar speichert/liest per Page
  5. Integration: Zwei parallele Pages mit gleicher URL -> unabhaengige Requests
  6. Regression: Alle 1545 Tests
## Problembeschreibung (DE) Zwei fundamentale Architektur-Probleme: ### 1. Module-Level Shared State in DynamicScriptHandler In `src/js/dynamic-scripts.ts` (Zeile 61) existiert **module-level** Shared State: ```typescript // MODULE SCOPE — ALLE Pages teilen sich diesen State! const processingElements = new WeakMap<any, boolean>(); ``` Konsequenz: Test A erstellt Page -> laedt Scripts -> `processingElements` hat Eintraege. Test B erstellt neue Page -> DIESELBE `processingElements` Map wird weitergenutzt -> Scripts werden faeelschlicherweise als "bereits verarbeitet" erkannt -> 6 Test-Failures. ### 2. Kein Unified Network Stack per Page Aktuell verteilen sich Requests auf verschiedene, unabhaengige Komponenten: | Request-Typ | Handler | State | |---|---|---| | fetch() | execution-realm.ts (InstrumentedFetch) | Modular | | XMLHttpRequest | execution-realm.ts (XHR Proxy) | Modular | | Script-Loading | DynamicScriptHandler -> ScriptLoader | Per-Instance | | Image/Media Loading | Gar nicht instrumentiert | Keine | | WebSocket | execution-realm.ts | Modular | | Form-Submit | form-action.ts | Modular | | ServiceWorker | Gar nicht implementiert | Keine | Echter Browser: **EIN** Network-Stack pro Page/Ursprung. ALLES geht durch: ``` Request -> ServiceWorker -> HTTP Cache -> Cookie Jar -> Network -> Response -> SW Cache ``` ## Analyse **Problem 1 Fix: Per-Instance processingElements** ```typescript class DynamicScriptHandler { // Vorher (module-level): // const processingElements = new WeakMap<any, boolean>(); // Nachher (instance-level): private processingElements = new WeakMap<any, boolean>(); private loadedUrls = new Set<string>(); // bereits per-instance ✓ } ``` **Problem 2: PageNetworkManager Architektur** ```typescript class PageNetworkManager { // State per Page: private _cookieJar: CookieJar; private _cache: Map<string, CacheEntry>; private _serviceWorker: ServiceWorker | null; private _interceptors: RequestInterceptor[]; // EINZIGER Entry-Point fuer ALLE Requests: async request(method: string, url: string, options?: RequestOptions): Promise<Response> { // 1. Request-Interception (Extensions: adblock, proxy) for (const interceptor of this._interceptors) { const result = interceptor({ method, url, headers, body }); if (result) return result; // abgefangen } // 2. ServiceWorker (if installed) if (this._serviceWorker) { const swResponse = await this._serviceWorker.fetchEvent(request); if (swResponse) return swResponse; } // 3. HTTP Cache const cached = this._cache.get(url); if (cached && !stale(cached)) return cached.response; // 4. Cookie-Jar const cookies = this._cookieJar.getCookies(url); if (cookies) request.headers.set('Cookie', cookies); // 5. Fetch (via Bun oder curl) const response = await fetch(request); // 6. Set-Cookies speichern const setCookie = response.headers.get('Set-Cookie'); if (setCookie) this._cookieJar.setCookies(url, setCookie); // 7. Cache schreiben this._cache.set(url, { response, timestamp: Date.now() }); // 8. Response-Interception return response; } // Convenience-Methoden delegieren alle an request(): fetch = (url: string, init?: RequestInit) => this.request('GET', url, init); xhr(method: string, url: string, ...) { /* -> request() */ } loadScript(url: string) { /* -> request() */ } loadImage(url: string) { /* -> request() */ } websocket(url: string) { /* -> request() */ } } ``` **Integration mit Page:** ```typescript class Page { network: PageNetworkManager; // EINE Instanz pro Page constructor() { this.network = new PageNetworkManager(this); this.dynamicScripts = new DynamicScriptHandler(this.network, window, document); // Kein Module-Level State mehr } } ``` ## Optionen **Option A — Minimal: Nur Shared-State fixen (EMPFEHLUNG fuer sofort)** - `processingElements` von Module-Level zu Instance-Level verschieben - Test Failures verschwinden (6/1545) - Aufwand: ~30 Minuten **Option B — Full PageNetworkManager (EMPFEHLUNG)** - Alles aus Option A + voller NetworkManager - Alle Request-Typen gehen durch EINE Pipeline - Cookie-Jar, HTTP-Cache, ServiceWorker vorbereitet - Aufwand: ~8h Kern + 4h Integration **Option C — Hybrid: Minimal fix + NetworkManager Grundstruktur** - Erst den Shared-State fixen (Option A) - Dann schrittweise NetworkManager aufbauen: 1. Phase 1: Grundstruktur + fetch()-Routing 2. Phase 2: XHR + Script-Loading umstellen 3. Phase 3: Cookie-Jar + Cache 4. Phase 4: Image/Media/Form integrieren ## Entscheidung **Option C** — Shared-State fixen (Red Alert, blockiert Tests) + NetworkManager in Phasen. Phase 1 ist der Shared-State Fix. Phase 2+ folgen iterativ. ## Akzeptanzkriterien - [ ] `processingElements` ist per DynamicScriptHandler-Instance, nicht module-level - [ ] `loadedUrls` ist bereits per-instance (bleibt unveraendert) - [ ] Alle 6 Shared-State-Failures sind verschwunden - [ ] `PageNetworkManager` Klasse existiert mit `request()` Methode - [ ] `fetch()` geht durch PageNetworkManager - [ ] `XMLHttpRequest` geht durch PageNetworkManager - [ ] Script-Loading (DynamicScriptHandler) geht durch PageNetworkManager - [ ] Cookie-Jar ist per Page (nicht global) - [ ] HTTP-Cache ist per Page (nicht global) - [ ] Keine Regression: Alle 1545 Tests bleiben gruen ## Betroffene Dateien | Datei | Aenderung | |---|---| | `src/js/dynamic-scripts.ts` | `processingElements` -> instance-level (Zeile 61) | | `src/network/network-manager.ts` | **NEU**: PageNetworkManager Klasse | | `src/network/cookie-jar.ts` | **NEU**: Per-Page Cookie Jar | | `src/pages/page.ts` | `network` Property hinzufuegen | | `src/runtime-isolation.ts` | `fetch`/`XHR` durch NetworkManager ersetzen | | `src/js/script-loader.ts` | `load()` durch NetworkManager.request() | | `src/execution-realm.ts` | `InstrumentedFetch` entfernen (durch NetworkManager) | | `tests/unit/network-manager.test.ts` | **NEU**: NetworkManager Tests | ## Technische Risiken - **Bestehende Instrumentierung**: `execution-realm.ts` hat eigene fetch/XHR-Proxy. Umstellung auf NetworkManager kann Race-Conditions ausloesen. - **Script-Loading Reihenfolge**: DynamicScriptHandler hat sequentielle Chunk-Queue. NetworkManager muss asynchrone Requests parallel erlauben (wie Browser) aber Execution sequentiell halten (fuer TDZ). - **WebSocket**: Geht nicht durch fetch() -> braucht eigenen Pfad im NetworkManager. ## Dependencies - Issue #89 (DynamicScriptHandler Queue-Konsolidierung) — betrifft shared state - Issue #98 (RAF Vereinfachung) — kein direkter Zusammenhang ## Testplan 1. **Unit**: DynamicScriptHandler erzeugt neue WeakMap pro Instanz 2. **Unit**: Zwei Pages haben unabhaengige processingElements 3. **Unit**: NetworkManager.request() routed fetch korrekt 4. **Unit**: Cookie-Jar speichert/liest per Page 5. **Integration**: Zwei parallele Pages mit gleicher URL -> unabhaengige Requests 6. **Regression**: Alle 1545 Tests
Artur closed this issue 2026-06-19 10:12:27 +00:00
Author
Owner

Gelöst in Commit f6a8c4b

Was wurde gemacht

Phase 1 — Shared-State Fix (processingElements):

  • processingElements von module-level (const) zu instance-level (private processingElements) in DynamicScriptHandler
  • Alle 7 Referenzen in src-setter, appendChild, insertBefore, MutationObserver, _queueDynamic und chunk-processing aktualisiert
  • Jede Page/Handler-Instanz hat jetzt ihre eigene WeakMap → keine Cross-Page-Interferenz mehr

Phase 2 — PageNetworkManager (new):

  • src/network/network-manager.ts: Unified Network Pipeline pro Page
  • Pipeline: Interceptors → HTTP Cache → CookieJar → Transport → Set-Cookie → Cache Write
  • CookieJar + HTTP Cache isoliert pro Page (wie Browser-Tab)
  • setTransport() zur Injektion der Transport-Funktion (via createFetch)
  • Convenience: fetch(), addInterceptor(), waitForRequest/Response, filterByUrl/Transport/Status
  • Cache mit Cache-Control max-age TTL (default 300s)
  • Cookie serialize/deserialize für Persistenz

Tests

  • 31 Unit-Tests für PageNetworkManager (Basic, Request, Cache Hit/Miss/Stale/Expiry, Cookies, Interceptors, Convenience, Isolation)
  • Alle 31 pass
  • Keine Regression in bestehenden Tests

Nächste Schritte (separates Issue)

  • Phase 3: Integration in page.ts + runtime-isolation.ts (reqMgr + cookieJar → PageNetworkManager)
  • ServiceWorker Pipeline
  • Image/Media Loading durch PageNetworkManager
**Gelöst in Commit f6a8c4b** ✅ ## Was wurde gemacht **Phase 1 — Shared-State Fix (processingElements):** - `processingElements` von module-level (`const`) zu instance-level (`private processingElements`) in `DynamicScriptHandler` - Alle 7 Referenzen in src-setter, appendChild, insertBefore, MutationObserver, `_queueDynamic` und chunk-processing aktualisiert - Jede Page/Handler-Instanz hat jetzt ihre eigene WeakMap → keine Cross-Page-Interferenz mehr **Phase 2 — PageNetworkManager (new):** - `src/network/network-manager.ts`: Unified Network Pipeline pro Page - Pipeline: Interceptors → HTTP Cache → CookieJar → Transport → Set-Cookie → Cache Write - CookieJar + HTTP Cache isoliert pro Page (wie Browser-Tab) - `setTransport()` zur Injektion der Transport-Funktion (via createFetch) - Convenience: `fetch()`, `addInterceptor()`, `waitForRequest/Response`, `filterByUrl/Transport/Status` - Cache mit Cache-Control max-age TTL (default 300s) - Cookie serialize/deserialize für Persistenz ## Tests - 31 Unit-Tests für PageNetworkManager (Basic, Request, Cache Hit/Miss/Stale/Expiry, Cookies, Interceptors, Convenience, Isolation) - Alle 31 pass - Keine Regression in bestehenden Tests ## Nächste Schritte (separates Issue) - Phase 3: Integration in page.ts + runtime-isolation.ts (reqMgr + cookieJar → PageNetworkManager) - ServiceWorker Pipeline - Image/Media Loading durch PageNetworkManager
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
glow-all/true-headless-browser#94
No description provided.