What createWsRequestTracker Does

export function createWsRequestTracker<Epoch extends boolean = false>(
  options: WsRequestTrackerOptions<Epoch>,
): WsRequestTracker<Epoch> {
  // Creates a tracker that bridges WebSocket request/response pairs into Promises.
  // Handles timeouts and provides a clean API for awaiting responses.
  const { timeoutMs, enableEpochs = false } = options;
  const pending = new Map<string, PendingEntry>();
  // pending map to store in-flight WS requests, keyed by contractId
  let epoch = 0;
  // current epoch used for stale request handling (epoch mode)
 
  return {
    track<T = void>(
      contractId: string,
    ): Promise<Epoch extends true ? T | null : T> {
      // Start tracking a WS request by its contract ID
      if (enableEpochs) {
        epoch++;
      }
 
      const requestEpoch = epoch;
 
      return new Promise<any>((resolve, reject) => {
        const timeoutId = setTimeout(() => {
          const entry = pending.get(contractId);
          if (!entry) return; // already resolved/rejected
 
          pending.delete(contractId);
 
          if (enableEpochs && entry.epoch !== epoch) {
            entry.resolve(null);
            return;
          }
 
          let errorMessage =
            `WS request ${contractId} timed out after ${timeoutMs}ms — server did not respond`;
          reject(new WebError(errorMessage, "WsTimeoutError"));
        }, timeoutMs);
 
        pending.set(contractId, {
          resolve: resolve as (value: unknown) => void,
          reject,
          timeoutId,
          epoch: requestEpoch,
        });
      });
    },
 
    resolve(contractId: string, data?: unknown): void {
      // Resolve pending request for contractId
      const entry = pending.get(contractId);
      if (!entry) return;
 
      if (enableEpochs && entry.epoch !== epoch) {
        clearTimeout(entry.timeoutId);
        pending.delete(contractId);
        entry.resolve(null);
        return;
      }
 
      clearTimeout(entry.timeoutId);
      pending.delete(contractId);
      entry.resolve(data);
    },
 
    // Reject a pending request
    reject(contractId: string, error: WebError): void {
      const entry = pending.get(contractId);
      if (!entry) return;
 
      clearTimeout(entry.timeoutId);
      pending.delete(contractId);
      entry.reject(error);
    },
 
    // Bump epoch to stale-discard all in-flight resolves.
    invalidate(): void {
      if (!enableEpochs) return;
      epoch++;
    },
 
    // Reject all pending and clear all timeouts.
    dispose(): void {
      for (const [contractId, entry] of pending) {
        clearTimeout(entry.timeoutId);
 
        // Reject with disposed error
        let errMessage =
          `WS tracker disposed while request ${contractId} was still pending`;
        entry.reject(
          new WebError(errMessage, "WsTrackerDisposedError"),
        );
      }
      pending.clear();
    },
 
    // Check if request is pending
    hasPending(contractId: string): boolean {
      return pending.has(contractId);
    },
 
    // Get number of pending requests
    pendingCount(): number {
      return pending.size;
    },
  };

createWsRequestTracker is a Promise-WS bridge that converts WebSocket request/response pairs into synchronous-looking async operations. In a WebSocket protocol where you send a packet and receive a response later via a callback listener, this tracker provides a clean API to await the response as a Promise.

Core Problem It Solves

Without this tracker:

  1. You send a WS packet (fire-and-forget)
  2. Later, a listener callback receives the response
  3. How do you connect step 2 back to step 1? How do you handle timeouts? How do you ensure stale responses don’t hang your app?

The tracker solves this by:

• Creating a Promise when you call track(contractId) after sending a packet

• Storing it keyed by the contract ID (packet UUID)

• Resolving/rejecting it when the listener receives the response

• Handling timeouts with automatic rejection


Workflow (Complete Flow)

┌────────────────────────────────────────────────────────────────────────────┐
│ 1. Component calls readFile() or saveFile()                                │
│    (e.g., user clicks a file or presses Ctrl+S)                            │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ 2. EditorStore sends WS packet via readFileWsFn / saveFileWsFn             │
│    → Returns a contractId (UUID)                                           │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ 3. tracker.track(contractId) creates a Promise & stores it in pending Map  │
│    → Returns Promise<T | null> (T=void by default, or T=fileData)          │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ 4. User waits for server response (await track())                          │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ 5. Server responds via WebSocket                                           │
│    → Listener (editorWebsocketEventHandler) receives packet                │
│    → Decodes the contract                                                  │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ 6. applyFileRead / applyFileWritten calls tracker.resolve()                │
│    → Looks up contractId in pending Map                                    │
│    → Clears timeout                                                        │
│    → Calls resolve() callback with data                                    │
└────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│ 7. await track() resolves, EditorStore returns typed result                │
│    → { kind: 'success', fileData }                                         │
│    → Or { kind: 'stale' } if epochs enabled and a newer request came in   │
└────────────────────────────────────────────────────────────────────────────┘

Why We Need It for service-cad/frontend/web

  1. Type-Safe Async/Await Over WebSocket Protocol

The WebSocket protocol is event-driven (fire-and-forget + callback), but Svelte components work better with await:

 // Without tracker: messy callback style
   sendReadFile(path, (result) => {
     if (result.error) { /* handle */ }
     if (result.data) { /* handle */ }
   });

   // With tracker: clean async/await
   const response = await readFile(path);
   // Throws on error, returns data on success
  1. Timeout Handling The tracker implements automatic timeouts via setTimeout. If the server doesn’t respond within WEBSOCKET_TIMEOUT_MS, the promise rejects with a WsTimeoutError.

  2. Epoch Tracking (Last-Write-Wins) With enableEpochs: true: • User rapidly clicks between files → each click sends a request • Only the latest file’s response matters (the others are “stale”) • Stale responses resolve with null instead of hanging or updating stale UI • The caller checks if (response === null) return { kind: ‘stale’ }

  3. Cleanup & Memory Management • dispose() rejects all pending promises and clears timeouts • Prevents memory leaks when components unmount • Used in Svelte onDestroy hooks


Two Modes of Operation

Simple Mode (default, enableEpochs: false)

• Every request gets its own independent promise

• Used by loadProjectTracker and killSessionTracker

• Example: loadProjectTracker.track(‘xyz’) → always resolves when server responds

Epoch Mode (enableEpochs: true)

• Each track() increments a monotonic epoch counter

• When resolve() is called, it checks if the entry’s epoch matches current

• Stale entries (older epoch) resolve with null instead of data

• Used by readTracker, saveTracker, fileTreeTracker, compileTracker


Key Methods

MethodDescription
track(contractId)Start tracking, returns `Promise<T
resolve(contractId, data?)Resolve pending request (called by listener)
reject(contractId, error)Reject on error (always surfaces, never stale-discard)
invalidate()Bump epoch, mark all pending entries as stale
dispose()Reject all pending + clear timeouts (component cleanup)
hasPending(contractId)Check if entry exists (used by listeners)
pendingCount()Number of unprocessed requests

Files Using This Tracker

• codeEditor/wsHooks/tracker.ts: readTracker, saveTracker (epoch mode)

• fileTree/wsHooks/tracker.ts: fileTreeTracker (epoch mode)

• canvas/wsHooks/tracker.ts: compileTracker (default mode)

• gitEditor/wsHooks/tracker.ts: gitTracker (epoch mode)

• core/pageState/editorPage/tracker.ts: loadProjectTracker, killSessionTracker (default mode)

All use WEBSOCKET_TIMEOUT_MS (configured elsewhere) for timeout duration.

import { WebError } from "@core/utils/errors/webError";
 
/**
 * Internal bookkeeping for a single in-flight WS request.
 *
 * `resolve` accepts `unknown` rather than a generic `T` because the pending
 * map is type-erased — it stores entries for many different request types
 * simultaneously, so there's no single `T` that fits all entries. The
 * per-call type safety lives on the public `track<T>()` method instead.
 *
 * We use `unknown` over `any` deliberately: `unknown` forces a type
 * assertion at the one internal call site where we store the resolve
 * callback (`resolve as (value: unknown) => void`), making the type
 * erasure explicit and auditable. With `any`, TypeScript stops checking
 * entirely — if we accidentally passed the wrong value internally, the
 * compiler wouldn't catch it. The single assertion is worth the trade-off
 * for keeping the rest of the internals type-checked.
 */
export type PendingEntry = {
  resolve: (value: unknown) => void;
  reject: (error: WebError) => void;
  timeoutId: ReturnType<typeof setTimeout>;
  epoch: number;
};
 
/**
 * Generic over Epoch — controls the return type of track<T>().
 *
 * When Epoch is false (default), track<T>() returns Promise<T>.
 * When Epoch is true, track<T>() returns Promise<T | null> because
 * stale responses resolve with null instead of hanging forever.
 *
 * TypeScript infers Epoch from the enableEpochs property value:
 *   createWsRequestTracker({ timeoutMs: WEBSOCKET_TIMEOUT_MS, enableEpochs: true })
 *   // → WsRequestTracker<true>, track returns Promise<T | null>
 *
 *   createWsRequestTracker({ timeoutMs: WEBSOCKET_TIMEOUT_MS })
 *   // → WsRequestTracker<false>, track returns Promise<T>
 */
export type WsRequestTrackerOptions<Epoch extends boolean = false> = {
  timeoutMs: number;
  enableEpochs?: Epoch;
};
 
export type WsRequestTracker<Epoch extends boolean = false> = {
  /**
   * Start tracking a request. Returns a typed Promise that resolves
   * when resolve() is called with the matching contract ID.
   *
   * T defaults to void — callers that expect response data specify it:
   *   await tracker.track<string[]>(contractId);  // expects string[]
   *   await tracker.track(contractId);             // expects void
   *
   * With enableEpochs: true, returns Promise<T | null>. null indicates
   * a stale response — a newer track() call superseded this one. The
   * caller checks for null to detect staleness and return early.
   */
  track<T = void>(
    contractId: string,
  ): Promise<Epoch extends true ? T | null : T>;
 
  /**
   * Resolve a pending request with optional data.
   *
   * Accepts `unknown` because the tracker is type-erased internally —
   * it doesn't know which `T` the corresponding track<T>() call used.
   * The caller (WS listener) passes whatever the server sent, and the
   * awaiting handler receives it typed via the Promise<T> from track().
   *
   * With enableEpochs: true, stale entries (epoch doesn't match current)
   * are settled with null instead of the provided data. This ensures the
   * caller's promise always settles — its finally block runs, no leaked
   * closures.
   */
  resolve(contractId: string, data?: unknown): void;
 
  /** Reject a pending request with an error. Always surfaces (never stale-discarded). */
  reject(contractId: string, error: WebError): void;
 
  /** Bump epoch to stale-discard all in-flight resolves. No-op without enableEpochs. */
  invalidate(): void;
 
  /** Reject all pending and clear timers. Call on component destroy. */
  dispose(): void;
 
  /** Whether a tracked entry exists for this contract ID. */
  hasPending(contractId: string): boolean;
 
  /** Number of currently pending (unresolved) tracked requests. */
  pendingCount(): number;
};
 
/**
 * Creates a tracker that bridges WebSocket request/response pairs into Promises.
 *
 * The problem: WS senders fire-and-forget a packet, and the response arrives
 * later via a separate listener callback. There's no built-in way for the
 * caller to await the full round-trip, detect timeouts, or correlate a
 * response back to the request that triggered it.
 *
 * This tracker solves that by letting the caller `track(contractId)` after
 * sending a packet. This returns a Promise that resolves when the listener
 * calls `resolve(contractId)` with the matching ID, or rejects on timeout.
 *
 * Two modes of operation:
 *
 * 1. Simple tracking (enableEpochs=false, the default)
 *    Every tracked request gets its own independent promise. resolve() always
 *    delivers. This is the mode used by the git editor where each operation
 *    (commit, stage, merge) is a distinct action that doesn't supersede others.
 *
 * 2. Epoch tracking (enableEpochs=true)
 *    Each track() call increments a monotonic epoch counter. When resolve()
 *    is called, it checks whether the pending entry's epoch matches the
 *    current epoch. If stale (a newer request was tracked since), the entry
 *    is settled with null — the awaiting promise resolves to null, signaling
 *    staleness to the caller. This implements last-write-wins for rapid
 *    sequential requests (e.g. clicking between files quickly — only the
 *    most recent file read matters).
 *
 *    Stale timeouts also settle with null (not rejection) — if a stale
 *    request's timeout fires, it resolves with null rather than surfacing
 *    an error for a request the user already navigated away from.
 *
 *    reject() always surfaces regardless of epoch — errors should never be
 *    silently swallowed.
 *
 *    invalidate() bumps the epoch without tracking a new request, causing all
 *    in-flight responses to be discarded when they arrive. Use this when you
 *    want to cancel all pending without triggering error callbacks (e.g.
 *    navigating away from the current context).
 */
export function createWsRequestTracker<Epoch extends boolean = false>(
  options: WsRequestTrackerOptions<Epoch>,
): WsRequestTracker<Epoch> {
  const { timeoutMs, enableEpochs = false } = options;
  const pending = new Map<string, PendingEntry>();
  let epoch = 0;
 
  return {
    /**
     * Start tracking a WS request by its contract ID (the packet UUID).
     * Returns a Promise that resolves when the listener calls resolve()
     * with the same contract ID, or rejects on timeout.
     *
     * Call this immediately after sending the packet:
     *   const contractId = await sender(...);
     *   await tracker.track(contractId);
     *
     * This creates a `Promise<T>` with a properly typed `resolve: (value: T) => void`
     * callback but then immediately erases it to `unknown` for storage. When the
     * promise resolves, the caller gets T back. The erasure is a one-line bridge
     * between the typed public API and the untyped internal map. This is us making T
     * the same type as our storage, not the typescript rule that `unknown` types must
     * do a type assertion before use.
     */
    track<T = void>(
      contractId: string,
    ): Promise<Epoch extends true ? T | null : T> {
      if (enableEpochs) {
        epoch++;
      }
 
      // Snapshot the current epoch so we can check staleness on resolve
      const requestEpoch = epoch;
 
      // Cast: TypeScript can't evaluate the conditional return type
      // inside a generic factory. The runtime behavior is correct —
      // with epochs, stale entries resolve with null; without, they
      // resolve with T. The public type signature enforces this.
      return new Promise<any>((resolve, reject) => {
        const timeoutId = setTimeout(() => {
          const entry = pending.get(contractId);
          if (!entry) return; // already resolved/rejected
 
          pending.delete(contractId);
 
          // Stale epoch check — if a newer request was tracked since this
          // one, don't surface a timeout error for an abandoned request.
          // Resolve with null so the caller's promise settles cleanly
          // (its finally block runs, no leaked closure).
          if (enableEpochs && entry.epoch !== epoch) {
            entry.resolve(null);
            return;
          }
 
          let errorMessage =
            `WS request ${contractId} timed out after ${timeoutMs}ms — server did not respond`;
          reject(new WebError(errorMessage, "WsTimeoutError"));
        }, timeoutMs);
 
        pending.set(contractId, {
          resolve: resolve as (value: unknown) => void,
          reject,
          timeoutId,
          epoch: requestEpoch,
        });
      });
    },
 
    /**
     * Resolve a pending request. Called by the WS listener after applying
     * state from the server response.
     *
     * If epochs are enabled and the entry is stale (a newer request was
     * tracked since this one), the entry is settled with null instead of
     * the provided data. This ensures the caller's promise always settles
     * — its finally block runs and closures are released. The caller
     * checks for null to detect staleness.
     *
     * No-op if contractId is not found (already timed out, already
     * resolved, or an unknown/broadcast ID).
     */
    resolve(contractId: string, data?: unknown): void {
      const entry = pending.get(contractId);
      if (!entry) return;
 
      // Stale epoch check — a newer request was tracked since this one.
      // Settle with null so the caller's promise resolves (its finally
      // block runs, no leaked closure). The caller checks for null to
      // detect staleness: if (result === null) return { kind: 'stale' };
      if (enableEpochs && entry.epoch !== epoch) {
        clearTimeout(entry.timeoutId);
        pending.delete(contractId);
        entry.resolve(null);
        return;
      }
 
      clearTimeout(entry.timeoutId);
      pending.delete(contractId);
      entry.resolve(data);
    },
 
    /**
     * Reject a pending request. Called by the WS listener when applying
     * state fails, or internally on timeout.
     *
     * Unlike resolve, reject always surfaces regardless of epoch staleness.
     * Errors should never be silently swallowed — the handler needs to
     * know something went wrong so it can surface it to the user.
     *
     * No-op if contractId is not found.
     */
    reject(contractId: string, error: WebError): void {
      const entry = pending.get(contractId);
      if (!entry) return;
 
      clearTimeout(entry.timeoutId);
      pending.delete(contractId);
      entry.reject(error);
    },
 
    /**
     * Bump the epoch without tracking a new request.
     *
     * All currently pending entries become stale — their resolve() calls
     * will settle with null when the server eventually responds.
     * Their timeouts also settle with null (not rejection) since the
     * timeout callback checks epoch staleness before rejecting.
     *
     * Use this when you want to "cancel" all in-flight requests without
     * triggering error callbacks. For example, navigating away from a
     * context where the responses are no longer relevant.
     *
     * No-op if epochs are not enabled.
     */
    invalidate(): void {
      if (!enableEpochs) return;
      epoch++;
    },
 
    /**
     * Reject all pending requests and clear all timeouts.
     * Call this from component onDestroy to prevent memory leaks
     * and dangling timer callbacks.
     */
    dispose(): void {
      for (const [contractId, entry] of pending) {
        clearTimeout(entry.timeoutId);
 
        // Reject with disposed error
        let errMessage =
          `WS tracker disposed while request ${contractId} was still pending`;
        entry.reject(
          new WebError(errMessage, "WsTrackerDisposedError"),
        );
      }
      pending.clear();
    },
 
    /**
     * Whether a tracked entry exists for this contract ID.
     * Used by listeners to decide whether to apply state:
     * - true: this is a response to a tracked request — apply + resolve
     * - false: timed out, already resolved, or unknown (future broadcast)
     */
    hasPending(contractId: string): boolean {
      return pending.has(contractId);
    },
 
    /** Number of currently pending (unresolved) tracked requests. */
    pendingCount(): number {
      return pending.size;
    },
  };
}