{
  "version": 3,
  "sources": ["panel_rows_cache.js"],
  "sourcesContent": ["/*\n * static/js/panel_rows_cache.js \u2014 instant re-open for the map's UGC panels\n * (SNOW-879).\n *\n * The favourites, routes and observations roundels each open a sheet whose\n * rows are loaded over HTMX from that app's list endpoint. All three did it\n * the same way and all three did it on EVERY open, from cold: the panel\n * appeared, showed \"Loading your favourites\u2026\", and the rows arrived a round\n * trip later. Nothing was kept between opens, and nothing was fetched before\n * the first one \u2014 so the fifth open of a list that had not changed cost\n * exactly as much as the first.\n *\n * This module is the one place that changes, so the three panels keep the\n * loaders they already have rather than each growing a cache of its own\n * (the shape ``map_region_panel.js`` already uses for its pinned list, and\n * the reason it is the one panel that did not have this problem).\n *\n * Stale-while-revalidate\n * ----------------------\n * ``load()`` paints the cached markup into the panel synchronously and THEN\n * issues the same ``htmx.ajax`` the panel used to issue on its own. So the\n * rows are on screen on the frame the roundel is pressed, and the server\n * still gets the last word a moment later. What the user sees is never more\n * than one open out of date, and a favourite's danger chip \u2014 the one part of\n * these rows that changes without the user touching anything \u2014 is refreshed\n * on every open.\n *\n * HTMX STAYS THE TRANSPORT. Everything hanging off these requests keeps\n * working because none of it is bypassed: each panel's own\n * ``htmx:responseError`` / ``htmx:sendError`` handlers still draw its error\n * line, ``favourites_offline.js`` still reads the roster sidecar out of the\n * swapped DOM on ``htmx:afterOnLoad`` and writes it through to IndexedDB,\n * and the endpoints stay behind ``@require_htmx`` (invariant 4).\n *\n * The one thing this module does to those requests is SUPPRESS THE SWAP when\n * the response is byte-identical to what was just painted from cache \u2014 via\n * ``htmx:beforeSwap``'s mutable ``shouldSwap`` (htmx 2.0.4). That is not an\n * optimisation, it is a correctness fix: re-swapping identical rows would\n * destroy and rebuild every node in the panel a moment after the user could\n * first touch them, throwing away an inline rename they had already started\n * and resetting the scroll region under them. Comparing the raw\n * ``serverResponse`` strings rather than the parsed ``innerHTML`` is what\n * makes the comparison trustworthy \u2014 the DOM round-trip normalises\n * whitespace and attribute order, so two identical responses would not\n * always compare equal after it.\n *\n * The warm\n * --------\n * ``warm()`` fills the cache before any panel is opened, so even the FIRST\n * open of a session is instant. It is a plain ``fetch`` carrying\n * ``HX-Request`` rather than an ``htmx.ajax`` \u2014 the same move\n * ``map_region_panel.js`` makes against ``favourite_region_toggle`` \u2014 because\n * a warm has no target to swap into and must not fire the lifecycle events\n * the panels bind to. A warm that fails is simply not cached; the panel then\n * behaves exactly as it did before this module existed.\n *\n * ``onWarmed()`` is the consequence of raising no htmx event: a module that\n * writes a panel's rows through to storage on ``htmx:beforeSwap`` sees a\n * panel OPEN and never sees a warm, so before SNOW-661 the observation rows\n * reached IndexedDB only if the user had opened the sheet while online \u2014\n * which is not the user the offline read exists for. A handler registered\n * for a key is handed the body of every successful warm for it, and the\n * registration runs the other way round on purpose: this module knows\n * nothing about observations, and the module that cares about that key is\n * the one that names it (the ownership boundary SNOW-722 cost the routes\n * panel a list of favourites to learn). No handler is registered for\n * ``favourites`` or ``routes``, so a warm for those keys goes on persisting\n * nothing.\n *\n * The cache itself still persists nothing. It is per page load, in memory,\n * and therefore cannot outlive a sign-out or leak across an account switch\n * \u2014 the hazard ``map_overlay_offline_cache.js`` has to carry a\n * ``principal`` to defend against, and one this module avoids rather than\n * solves. What a handler does with a body it is handed is that handler's\n * business, including the account partitioning: ``observations_offline.js``\n * stamps the ``principal`` on the row it writes, exactly as it does for a\n * body that arrives through a swap, so nothing about that hazard moves here.\n *\n * No exports beyond ``window.pwaPanelRows``.\n */\n\n(function panelRowsCacheInit() {\n  'use strict';\n\n  /**\n   * Cached raw response bodies by panel key ('favourites', 'routes',\n   * 'observations'). The value is the server's response text verbatim.\n   *\n   * @type {Map<string, string>}\n   */\n  const cache = new Map();\n\n  /**\n   * Per rows container: which panel it belongs to, and the exact response\n   * body currently PAINTED into it (null when nothing was).\n   *\n   * ``painted`` is what the swap suppression below compares against, and it\n   * has to be this rather than the cache: a warm that lands between a panel\n   * opening and its own request coming back would otherwise fill the cache\n   * with a body identical to the response, suppress the swap, and leave the\n   * panel on \"Loading\u2026\" for good \u2014 a request answered, cached, and never\n   * shown. Suppressing only what is demonstrably already on screen cannot\n   * make that mistake.\n   *\n   * Weak because every open clones a NEW container out of the sheet's\n   * <template> and drops the last one on the floor.\n   *\n   * @type {WeakMap<Element, {key: string, painted: ?string}>}\n   */\n  const owners = new WeakMap();\n\n  /**\n   * Handlers to notify when a warm for a panel key succeeds, by key.\n   *\n   * A list rather than one handler per key so that a second registration\n   * cannot silently displace the first \u2014 a module losing its write-through\n   * to a load-order change is precisely the class of failure this hook was\n   * added to fix.\n   *\n   * @type {Map<string, Array<Function>>}\n   */\n  const warmHandlers = new Map();\n\n  /**\n   * Paint cached markup into a rows container.\n   *\n   * ``htmx.process`` is not optional: these rows arrive carrying their own\n   * Remove forms, and without it they would be inert markup for the moment\n   * before the revalidation lands \u2014 a delete button that does nothing is\n   * worse than one that is not there yet.\n   *\n   * @param {Element} target The rows container.\n   * @param {string} body The response body to paint.\n   * @returns {boolean} True when it was painted.\n   */\n  function paint(target, body) {\n    if (typeof body !== 'string') return false;\n    try {\n      target.innerHTML = body;\n      htmx.process(target);\n      return true;\n    } catch (_e) {\n      // A malformed cached body must not take the panel down with it \u2014 the\n      // htmx.ajax below is about to replace this content anyway.\n      return false;\n    }\n  }\n\n  /**\n   * Load a panel's rows: cached markup now, the server's answer next.\n   *\n   * @param {string} key The panel key ('favourites', 'routes', 'observations').\n   * @param {string} url The list endpoint, used VERBATIM \u2014 every caller's\n   *   URL is written by the server and carries query parameters that\n   *   rebuilding it here would drop.\n   * @param {Element} target The rows container to swap into.\n   * @param {{cached?: boolean}} [options] ``cached: true`` to paint from\n   *   cache first. Pass it for an OPEN; leave it off for a re-read that\n   *   follows a mutation, where the cache is known to be behind and\n   *   painting it would flash a row the user has just deleted back onto\n   *   the screen.\n   * @returns {Promise<void>|undefined} htmx.ajax's promise, or undefined\n   *   when there was nothing to do.\n   */\n  function load(key, url, target, options) {\n    if (!key || !url || !target || typeof htmx === 'undefined') return undefined;\n    const state = { key: key, painted: null };\n    owners.set(target, state);\n    if (options && options.cached) {\n      const body = cache.get(key);\n      if (paint(target, body)) state.painted = body;\n    } else {\n      cache.delete(key);\n    }\n    return htmx.ajax('GET', url, { target: target, swap: 'innerHTML' });\n  }\n\n  /**\n   * Run `fn` once the browser is otherwise idle.\n   *\n   * A warm exists to make a LATER interaction fast; competing with the map's\n   * own first paint to do it would trade the delay the user complained about\n   * for one they would notice more. `requestIdleCallback` is absent on\n   * Safari before 17, hence the timeout fallback.\n   *\n   * @param {Function} fn The work to defer.\n   * @returns {void}\n   */\n  function onIdle(fn) {\n    if (typeof requestIdleCallback === 'function') requestIdleCallback(fn);\n    else setTimeout(fn, 1200);\n  }\n\n  /**\n   * Register a handler to receive the body of every successful warm for one\n   * panel key.\n   *\n   * Called by the module that owns that panel's storage, never by this one:\n   * see this module's header. Registration must happen before the warm\n   * lands, which document order already guarantees \u2014 every registering\n   * module is a deferred script loaded after this one, and a warm does not\n   * run until the browser is idle.\n   *\n   * @param {string} key The panel key.\n   * @param {Function} handler Called with the response body, verbatim. Its\n   *   return value is ignored and a throw or a rejection is swallowed \u2014 a\n   *   warm is best effort and must not become a source of errors.\n   * @returns {void}\n   */\n  function onWarmed(key, handler) {\n    if (!key || typeof handler !== 'function') return;\n    const handlers = warmHandlers.get(key) || [];\n    handlers.push(handler);\n    warmHandlers.set(key, handlers);\n  }\n\n  /**\n   * Hand a freshly warmed body to every handler registered for its key.\n   *\n   * @param {string} key The panel key.\n   * @param {string} body The response body, verbatim.\n   * @returns {void}\n   */\n  function notifyWarmed(key, body) {\n    const handlers = warmHandlers.get(key);\n    if (!handlers) return;\n    handlers.forEach(function (handler) {\n      try {\n        const result = handler(body);\n        if (result && typeof result.catch === 'function') {\n          result.catch(function () {\n            // Best effort \u2014 a handler that cannot store the body leaves the\n            // panel exactly as it behaved before this hook existed.\n          });\n        }\n      } catch (_e) {\n        // As above: a throwing handler must not take the warm down with it.\n      }\n    });\n  }\n\n  /**\n   * Fill the cache before any panel has been opened, so even the first open\n   * of a session paints instantly. Deferred to idle; safe to call at module\n   * init.\n   *\n   * Best effort throughout: a failed or non-2xx warm is not cached, is not\n   * handed to the key's warm handlers, and is not retried, leaving the\n   * panel's own load to report the failure in the place the user can see\n   * it.\n   *\n   * @param {string} key The panel key.\n   * @param {string} url The list endpoint.\n   * @returns {void}\n   */\n  function warm(key, url) {\n    if (!key || !url || cache.has(key)) return;\n    onIdle(function () {\n      // Re-checked inside the callback: a user quick enough to open the\n      // panel before the browser went idle has already filled this entry,\n      // and warming over it would be a request for something we hold.\n      if (cache.has(key)) return;\n      fetch(url, {\n        credentials: 'same-origin',\n        // These endpoints are @require_htmx and this is not an htmx request\n        // \u2014 the same plain-fetch-with-the-header move map_region_panel.js\n        // makes against favourite_region_toggle.\n        headers: { 'HX-Request': 'true' },\n      })\n        .then(function (resp) {\n          return resp.ok ? resp.text() : null;\n        })\n        .then(function (body) {\n          if (typeof body !== 'string' || cache.has(key)) return;\n          cache.set(key, body);\n          // Only on the branch that caches. An entry already present means\n          // the panel was opened while this request was out, so its own\n          // swap has been through every listener bound to it \u2014 including\n          // the write-through this hook exists to reach \u2014 with a body no\n          // older than this one.\n          notifyWarmed(key, body);\n        })\n        .catch(function () {\n          // Offline, or the endpoint is unreachable. Nothing cached, nothing\n          // shown \u2014 the panel opens exactly as it did before SNOW-879.\n        });\n    });\n  }\n\n  /**\n   * Drop a panel's cached rows.\n   *\n   * @param {string} key The panel key.\n   * @returns {void}\n   */\n  function invalidate(key) {\n    cache.delete(key);\n  }\n\n  // Delegated from document.body because htmx raises the lifecycle events\n  // for an `htmx.ajax` call with no source element against document.body \u2014\n  // the same reason row_removed.js and each panel's error handlers are bound\n  // there rather than to the container.\n  document.body?.addEventListener('htmx:beforeSwap', function (ev) {\n    const detail = ev.detail;\n    if (!detail || !detail.target) return;\n    const state = owners.get(detail.target);\n    if (!state) return;\n    // An error response is not an answer about the rows \u2014 htmx has already\n    // decided not to swap it, and each panel draws its own error line from\n    // the matching htmx:responseError. Caching it would serve the failure\n    // back as content on the next open.\n    if (detail.isError || !detail.shouldSwap) return;\n    const body = detail.serverResponse;\n    if (typeof body !== 'string') return;\n    cache.set(state.key, body);\n    if (state.painted === body) {\n      // Byte-identical to what this container is ALREADY showing. See this\n      // module's header: suppressing the swap protects an in-progress inline\n      // rename and the panel's scroll position from a rebuild that would\n      // change nothing.\n      detail.shouldSwap = false;\n      return;\n    }\n    // htmx is about to put this on screen, so it becomes what a later\n    // revalidation of this same container compares against.\n    state.painted = body;\n  });\n\n  Object.defineProperty(window, 'pwaPanelRows', {\n    value: Object.freeze({\n      load: load,\n      warm: warm,\n      invalidate: invalidate,\n      onWarmed: onWarmed,\n    }),\n    writable: false,\n    configurable: false,\n  });\n}());\n"],
  "mappings": "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",
  "names": ["cache", "owners", "warmHandlers", "paint", "target", "body", "load", "key", "url", "options", "state", "onIdle", "fn", "onWarmed", "handler", "handlers", "notifyWarmed", "result", "warm", "resp", "invalidate", "ev", "detail"]
}
