{"version":3,"sources":["../../../../src/client/components/offline.ts"],"sourcesContent":["/**\n * Centralized offline detection, state management, and retry logic.\n *\n * This module tracks whether the app is offline and provides primitives for\n * retrying failed network requests. It is designed to be extended in the\n * future — e.g., instrumenting module chunk loading, Flight chunk resolution,\n * or eventually being promoted to a React-level feature.\n *\n * All stateful behavior (event listeners, polling, state tracking) only runs\n * in the browser. On the server and during hydration, getOffline() always\n * returns false.\n *\n * ## Known limitation: queued fetches\n *\n * When the user navigates multiple times while offline, each navigation queues\n * a separate fetch that blocks on waitForConnection(). When connectivity is\n * restored, all of them resume and retry simultaneously.\n *\n * Future mitigations:\n * - Stale cache access (PR 3): offline navigations will reuse back-forward\n *   cache entries, so most navigations won't issue new fetches at all. This is\n *   the primary shield against duplicate requests.\n * - Fetch cancellation: on router.refresh(), we could abort pending blocked\n *   fetches since refresh invalidates all dynamic caches.\n */\n\n// Backoff delays for the polling loop: 500ms → 1s → 2s → 3s (cap)\n\n// Timeout for the HEAD connectivity check. If the request doesn't resolve\n// within this window, we assume we're still offline. 200ms is more than enough\n// — network errors reject almost instantly.\nconst CONNECTIVITY_CHECK_TIMEOUT_MS = 200\n\nimport { pingPrefetchScheduler } from './segment-cache/scheduler'\nimport { fetch } from './segment-cache/fetch'\nimport { RSC_HEADER } from './app-router-headers'\nimport { dispatchOfflineChange } from './use-offline'\n\nexport type OfflineState = {\n  promise: Promise<void>\n  resolve: () => void\n  timeoutHandle: ReturnType<typeof setTimeout> | null\n  backoffStep: number\n}\n\nlet offlineState: OfflineState | null = null\n\n/**\n * Returns true if the error from a fetch() rejection indicates a network\n * failure (as opposed to an intentional abort or timeout). If it is a\n * network error, also starts the connectivity polling loop.\n *\n * - AbortError: the request was intentionally canceled via AbortSignal\n * - TimeoutError: AbortSignal.timeout() expired — could be a slow server,\n *   not necessarily offline\n */\nexport function checkOfflineError(err: unknown): boolean {\n  if (err instanceof DOMException) {\n    if (err.name === 'AbortError' || err.name === 'TimeoutError') {\n      return false\n    }\n  }\n  notifyOffline()\n  return true\n}\n\n/**\n * Returns whether the app is currently considered offline (i.e., a\n * connectivity polling loop is active). Always returns false on the\n * server and during hydration.\n */\nexport function getOffline(): OfflineState | null {\n  return offlineState\n}\n\n/**\n * Enters the offline state if not already in it, and starts the\n * connectivity polling loop.\n */\nfunction notifyOffline(): OfflineState {\n  if (offlineState !== null) {\n    return offlineState\n  }\n  let resolve: () => void\n  const promise = new Promise<void>((r) => {\n    resolve = r\n  })\n  offlineState = {\n    promise,\n    resolve: resolve!,\n    timeoutHandle: null,\n    backoffStep: 0,\n  }\n  dispatchOfflineChange(true)\n  checkConnectivity(offlineState)\n  return offlineState\n}\n\n/**\n * Call this when any network request succeeds while we're in the offline state.\n * If a polling loop is active, this short-circuits it — no need to wait for\n * the next HEAD check if we already know we're back online.\n */\nexport function notifyOnline(): void {\n  if (offlineState === null) {\n    return\n  }\n  if (offlineState.timeoutHandle !== null) {\n    clearTimeout(offlineState.timeoutHandle)\n  }\n  const resolve = offlineState.resolve\n  offlineState = null\n  resolve()\n  dispatchOfflineChange(false)\n  pingPrefetchScheduler()\n}\n\n/**\n * Does a HEAD request to confirm connectivity, then either resolves the\n * offline state or schedules the next check with backoff.\n */\nasync function checkConnectivity(state: OfflineState): Promise<void> {\n  // Cancel any previously scheduled check so we don't end up with\n  // parallel polling loops.\n  if (state.timeoutHandle !== null) {\n    clearTimeout(state.timeoutHandle)\n    state.timeoutHandle = null\n  }\n\n  const controller = new AbortController()\n  const timeoutId = setTimeout(\n    () => controller.abort(),\n    CONNECTIVITY_CHECK_TIMEOUT_MS\n  )\n  try {\n    // HEAD request to the current page with the RSC header, so we're\n    // testing connectivity to the same endpoint that navigations use.\n    await fetch(location.href, {\n      method: 'HEAD',\n      headers: { [RSC_HEADER]: '1' },\n      signal: controller.signal,\n    })\n    clearTimeout(timeoutId)\n    // If the fetch didn't throw, we're back online.\n    notifyOnline()\n  } catch (err) {\n    // If the error is from our own timeout abort, that actually means\n    // the request went out and is waiting for a response — i.e., we're\n    // back online. A truly offline request fails almost instantly (well\n    // within the 200ms timeout).\n    if (err instanceof DOMException && err.name === 'AbortError') {\n      clearTimeout(timeoutId)\n      notifyOnline()\n      return\n    }\n    // Network error — still offline. Schedule the next check with backoff.\n    const delay =\n      state.backoffStep === 0\n        ? 500\n        : state.backoffStep === 1\n          ? 1000\n          : state.backoffStep === 2\n            ? 2000\n            : 3000\n    state.backoffStep++\n    state.timeoutHandle = setTimeout(() => checkConnectivity(state), delay)\n  }\n}\n\n/**\n * Returns a promise that resolves when connectivity is restored.\n */\nexport function waitForConnection(state: OfflineState) {\n  return state.promise\n}\n\nfunction pingOfflineState() {\n  if (offlineState !== null) {\n    checkConnectivity(offlineState)\n  }\n}\n\n// Set up browser event listeners for proactive offline detection.\nif (typeof window !== 'undefined') {\n  window.addEventListener('offline', notifyOffline)\n  window.addEventListener('online', pingOfflineState)\n}\n"],"names":["CONNECTIVITY_CHECK_TIMEOUT_MS","pingPrefetchScheduler","fetch","RSC_HEADER","dispatchOfflineChange","offlineState","checkOfflineError","err","DOMException","name","notifyOffline","getOffline","resolve","promise","Promise","r","timeoutHandle","backoffStep","checkConnectivity","notifyOnline","clearTimeout","state","controller","AbortController","timeoutId","setTimeout","abort","location","href","method","headers","signal","delay","waitForConnection","pingOfflineState","window","addEventListener"],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,kEAAkE;AAElE,0EAA0E;AAC1E,+EAA+E;AAC/E,4CAA4C;AAC5C,MAAMA,gCAAgC;AAEtC,SAASC,qBAAqB,QAAQ,4BAA2B;AACjE,SAASC,KAAK,QAAQ,wBAAuB;AAC7C,SAASC,UAAU,QAAQ,uBAAsB;AACjD,SAASC,qBAAqB,QAAQ,gBAAe;AASrD,IAAIC,eAAoC;AAExC;;;;;;;;CAQC,GACD,OAAO,SAASC,kBAAkBC,GAAY;IAC5C,IAAIA,eAAeC,cAAc;QAC/B,IAAID,IAAIE,IAAI,KAAK,gBAAgBF,IAAIE,IAAI,KAAK,gBAAgB;YAC5D,OAAO;QACT;IACF;IACAC;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASC;IACd,OAAON;AACT;AAEA;;;CAGC,GACD,SAASK;IACP,IAAIL,iBAAiB,MAAM;QACzB,OAAOA;IACT;IACA,IAAIO;IACJ,MAAMC,UAAU,IAAIC,QAAc,CAACC;QACjCH,UAAUG;IACZ;IACAV,eAAe;QACbQ;QACAD,SAASA;QACTI,eAAe;QACfC,aAAa;IACf;IACAb,sBAAsB;IACtBc,kBAAkBb;IAClB,OAAOA;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASc;IACd,IAAId,iBAAiB,MAAM;QACzB;IACF;IACA,IAAIA,aAAaW,aAAa,KAAK,MAAM;QACvCI,aAAaf,aAAaW,aAAa;IACzC;IACA,MAAMJ,UAAUP,aAAaO,OAAO;IACpCP,eAAe;IACfO;IACAR,sBAAsB;IACtBH;AACF;AAEA;;;CAGC,GACD,eAAeiB,kBAAkBG,KAAmB;IAClD,gEAAgE;IAChE,0BAA0B;IAC1B,IAAIA,MAAML,aAAa,KAAK,MAAM;QAChCI,aAAaC,MAAML,aAAa;QAChCK,MAAML,aAAa,GAAG;IACxB;IAEA,MAAMM,aAAa,IAAIC;IACvB,MAAMC,YAAYC,WAChB,IAAMH,WAAWI,KAAK,IACtB1B;IAEF,IAAI;QACF,iEAAiE;QACjE,kEAAkE;QAClE,MAAME,MAAMyB,SAASC,IAAI,EAAE;YACzBC,QAAQ;YACRC,SAAS;gBAAE,CAAC3B,WAAW,EAAE;YAAI;YAC7B4B,QAAQT,WAAWS,MAAM;QAC3B;QACAX,aAAaI;QACb,gDAAgD;QAChDL;IACF,EAAE,OAAOZ,KAAK;QACZ,kEAAkE;QAClE,mEAAmE;QACnE,oEAAoE;QACpE,6BAA6B;QAC7B,IAAIA,eAAeC,gBAAgBD,IAAIE,IAAI,KAAK,cAAc;YAC5DW,aAAaI;YACbL;YACA;QACF;QACA,uEAAuE;QACvE,MAAMa,QACJX,MAAMJ,WAAW,KAAK,IAClB,MACAI,MAAMJ,WAAW,KAAK,IACpB,OACAI,MAAMJ,WAAW,KAAK,IACpB,OACA;QACVI,MAAMJ,WAAW;QACjBI,MAAML,aAAa,GAAGS,WAAW,IAAMP,kBAAkBG,QAAQW;IACnE;AACF;AAEA;;CAEC,GACD,OAAO,SAASC,kBAAkBZ,KAAmB;IACnD,OAAOA,MAAMR,OAAO;AACtB;AAEA,SAASqB;IACP,IAAI7B,iBAAiB,MAAM;QACzBa,kBAAkBb;IACpB;AACF;AAEA,kEAAkE;AAClE,IAAI,OAAO8B,WAAW,aAAa;IACjCA,OAAOC,gBAAgB,CAAC,WAAW1B;IACnCyB,OAAOC,gBAAgB,CAAC,UAAUF;AACpC","ignoreList":[0]}