Monorepo for Aesthetic.Computer aesthetic.computer
core reports 2026-01-12-websocket-module-loader.md
4.3 kB

WebSocket Module Loader Status Report #

Date: January 12, 2026

Goal #

Bypass flaky Netlify HTTP proxy on localhost that causes ERR_CONTENT_LENGTH_MISMATCH errors by loading ES modules via WebSocket instead.

What's Working ✅ #

  1. WebSocket connects successfully - 📦 WebSocket connected to wss://localhost:8889
  2. Bundles are received - 📦 Received bundle for: bios.mjs (89 modules)
  3. Topological sort works - No more false circular dependency detection
  4. Blob URLs are created - 📦 Bundle loaded: lib/disk.mjs (56 modules via WS)
  5. Main modules load via blob URLs - bios.mjs, parse.mjs, disk.mjs all load

Remaining Issues ❌ #

1. Worker Loading Fails #

📦 Worker load failed (attempt 1/4), retrying in 200ms...
📦 Worker load failed (attempt 2/4), retrying in 400ms...

The web worker (system/public/aesthetic.computer/lib/worker.mjs) is loaded via new Worker(url).

  • Workers can't use blob URLs directly from parent context
  • Worker needs its own module loading mechanism or inline bundling

2. HTTP Fallback Still Hits ERR_CONTENT_LENGTH_MISMATCH #

GET https://localhost:8888/bios.mjs net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK)

Some code paths still try HTTP:

  • boot.mjs:579 - fetchAndShowSource() fetches source for display/debugging
  • Worker loading attempts

3. Prefetch Timeout #

📦 Prefetch failed for bios.mjs: WebSocket timeout for bios.mjs

Background prefetch requests time out - likely because the module was already loaded via the first request and the server doesn't send a duplicate response.

Files Modified #

/system/public/aesthetic.computer/module-loader.mjs #

  • Fixed getSessionWsUrl() to fetch /session/prompt endpoint (like disk.mjs does)
  • Changed dependency graph to only use static imports (not dynamic imports)
  • Fixed static import regex to require from keyword (was incorrectly matching dynamic imports)
  • Dynamic imports without blob URLs now convert to absolute HTTP URLs (blob URLs can't resolve relative paths)
  • getBlobUrl() now has isDynamic parameter - missing dynamic import blob URLs don't trigger hadFallback

Key Regex Changes #

Dependency graph (line ~178):

// OLD (matched dynamic imports too):
/(?:import|export)\s+(?:[\s\S]*?\s+from\s+)?["'](\.{1,2}\/[^"'\s]+\.m?js)["']/g

// NEW (requires "from" keyword):
/(?:import|export)\s+(?:[^;]*?\s+)?from\s*["'](\.{1,2}\/[^"'\s]+\.m?js)["']/g

Rewrite static imports (line ~348):

// OLD:
/^(?!\s*\/\/).*?((?:import|export)\s+(?:[\s\S]*?\s+from\s+)?["'])(\.\.?\/[^"']+)(["'])/gm

// NEW:
/^(?!\s*\/\/).*?((?:import|export)\s+(?:[^;]*?\s+)?from\s*["'])(\.\.?\/[^"']+)(["'])/gm

Architecture Understanding #

Circular Dependency Pattern #

  • headers.mjs line 2: import { tokenize } from "./kidlisp.mjs"; (STATIC)
  • kidlisp.mjs line 3154: await import('./headers.mjs'); (DYNAMIC - runtime only)

The dynamic import is intentional to break the load-time cycle. The fix was to exclude dynamic imports from the dependency graph used for topological sorting.

Why Blob URLs Can't Resolve Relative Paths #

When a module is loaded from blob:https://localhost/abc123, any import("./foo.mjs") inside it fails because:

  • Blob URLs don't have a directory context
  • ./foo.mjs relative to blob:... is meaningless

Solution: Convert dynamic imports without blob URLs to absolute HTTP URLs like https://localhost:8888/aesthetic.computer/lib/foo.mjs

Next Steps #

  1. Fix Worker Loading

    • Option A: Bundle worker code into a single blob (no imports)
    • Option B: Create inline worker with importScripts or similar
    • Option C: Use service worker to intercept worker module requests
  2. Eliminate Remaining HTTP Fetches

    • fetchAndShowSource() in boot.mjs could use the WebSocket bundle data
    • Or catch and suppress the error since it's just for debugging
  3. Clean Up Debug Logging

    • Remove or reduce the 📦 DEBUG: console logs once stable

Server-Side Reference #

Bundle gathering in /session-server/session.mjs lines 1341-1400:

  • gatherDeps() recursively collects static AND dynamic imports
  • Uses path.normalize(path.join(...)) for resolution
  • Both static and dynamic imports are included in the bundle sent to client