src/lib/demo/navigation-handler.ts

Total Symbols
6
Lines of Code
145
Avg Complexity
3.2
Symbol Types
1

File Relationships

graph LR navigateAndWait["navigateAndWait"] waitForTabLoad["waitForTabLoad"] listener["listener"] waitForUrlPattern["waitForUrlPattern"] navigateAndWait -->|calls| waitForTabLoad waitForTabLoad -->|dynamic_call| listener waitForUrlPattern -->|dynamic_call| listener click navigateAndWait "../symbols/915565f33bfb5777.html" click waitForTabLoad "../symbols/f95e5c46991b38e7.html" click listener "../symbols/8c93922ea0749d9f.html" click waitForUrlPattern "../symbols/ad4ebf9ec28c0540.html"

Symbols by Kind

function 6

All Symbols

Name Kind Visibility Status Lines Signature
navigateAndWait function exported- 20-32 navigateAndWait( tabId: number, url: string, timeoutMs = 30_000, ): : Promise<boolean>
waitForTabLoad function exported- 37-77 waitForTabLoad( tabId: number, timeoutMs = 30_000, ): : Promise<boolean>
injectContentScript function exported- 88-104 injectContentScript(tabId: number): : Promise<boolean>
waitForUrlPattern function exported- 109-138 waitForUrlPattern( tabId: number, urlFragment: string, timeoutMs = 15_000, ): : Promise<boolean>
listener function - 120-134 listener( updatedTabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, )
sleep function - 142-144 sleep(ms: number): : Promise<void>

Full Source

/**
 * Navigation Handler — manages page navigation during demo replay.
 *
 * Runs in the **background** service-worker context. Uses `chrome.tabs`
 * and `chrome.webNavigation` to navigate and wait for page load before
 * signalling the orchestrator to continue.
 */

import { createLogger } from "@/lib/logger";

const log = createLogger("NavigationHandler");

// ── Public API ────────────────────────────────────────────────────────────

/**
 * Navigate a tab to `url` and wait until the page is fully loaded.
 *
 * @returns true when the target URL is loaded, false on timeout.
 */
export async function navigateAndWait(
  tabId: number,
  url: string,
  timeoutMs = 30_000,
): Promise<boolean> {
  try {
    await chrome.tabs.update(tabId, { url });
    return await waitForTabLoad(tabId, timeoutMs);
  } catch (err) {
    log.warn(`Navigation to ${url} failed:`, err);
    return false;
  }
}

/**
 * Wait for a tab to finish loading (status === "complete").
 */
export function waitForTabLoad(
  tabId: number,
  timeoutMs = 30_000,
): Promise<boolean> {
  return new Promise((resolve) => {
    const timer = setTimeout(() => {
      chrome.tabs.onUpdated.removeListener(listener);
      log.warn(`Tab ${tabId} load timed out after ${timeoutMs}ms`);
      resolve(false);
    }, timeoutMs);

    function listener(
      updatedTabId: number,
      changeInfo: chrome.tabs.TabChangeInfo,
    ) {
      if (updatedTabId === tabId && changeInfo.status === "complete") {
        clearTimeout(timer);
        chrome.tabs.onUpdated.removeListener(listener);
        resolve(true);
      }
    }

    chrome.tabs.onUpdated.addListener(listener);

    // Check if already loaded
    chrome.tabs
      .get(tabId)
      .then((tab) => {
        if (tab.status === "complete") {
          clearTimeout(timer);
          chrome.tabs.onUpdated.removeListener(listener);
          resolve(true);
        }
      })
      .catch(() => {
        clearTimeout(timer);
        chrome.tabs.onUpdated.removeListener(listener);
        resolve(false);
      });
  });
}

/**
 * Inject a content script into a tab.
 * Used to re-inject after cross-origin navigations that destroy the
 * previous content-script context.
 *
 * Chrome re-injects the manifest-declared content script automatically
 * when the page loads. This function polls until the content script is
 * responsive (via PING) or the timeout is reached.
 */
export async function injectContentScript(tabId: number): Promise<boolean> {
  const maxAttempts = 6;
  const retryDelayMs = 250;

  for (let i = 0; i < maxAttempts; i++) {
    await sleep(retryDelayMs);
    try {
      const response = await chrome.tabs.sendMessage(tabId, { type: "PING" });
      if (response?.pong) return true;
    } catch {
      // content script not yet ready — retry
    }
  }

  log.warn(`Content script did not respond in tab ${tabId} after polling`);
  return false;
}

/**
 * Wait for a URL pattern to appear in the tab (e.g. after a redirect).
 */
export function waitForUrlPattern(
  tabId: number,
  urlFragment: string,
  timeoutMs = 15_000,
): Promise<boolean> {
  return new Promise((resolve) => {
    const timer = setTimeout(() => {
      chrome.tabs.onUpdated.removeListener(listener);
      resolve(false);
    }, timeoutMs);

    function listener(
      updatedTabId: number,
      changeInfo: chrome.tabs.TabChangeInfo,
      tab: chrome.tabs.Tab,
    ) {
      if (
        updatedTabId === tabId &&
        changeInfo.status === "complete" &&
        tab.url?.includes(urlFragment)
      ) {
        clearTimeout(timer);
        chrome.tabs.onUpdated.removeListener(listener);
        resolve(true);
      }
    }

    chrome.tabs.onUpdated.addListener(listener);
  });
}

// ── Utilities ─────────────────────────────────────────────────────────────

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}