{
  "version": 3,
  "sources": ["offline_ready_core.js"],
  "sourcesContent": ["// @ts-check\n/*\n * static/js/offline_ready_core.js \u2014 \"Get ready for offline\", the one press\n * (SNOW-1141).\n *\n * The network menu used to offer one Sync button per downloaded area\n * (SNOW-951). Nothing sent the edits waiting in the mutation queue, nothing\n * refreshed every area at once, and a person in a car park with a bar of\n * signal had to press each row in turn and read each answer. This module is\n * the one press that replaced them: its order, and the summary it ends on.\n *\n * No DOM, no strings of its own, no storage. Every effect is an injected\n * dependency, so the rules are unit-tested directly\n * (tests/js/test_offline_ready_core.js) and the two hosts supply the real\n * ones:\n *\n *   - the network menu on the map page (``pwa_offline.js``), where each\n *     area goes through ``window.pwaBasemapDownloads.syncArea``;\n *   - ``/offline/?sync=all`` (``offline_audit.js``), where each area goes\n *     through that page's own content fetch and the report re-runs over\n *     the result.\n *\n * ## The order\n *\n *   1. The mutation queue drains FIRST. A queued edit is the user's own\n *      work and the only thing on this device the server does not already\n *      have; an area's bulletins can be fetched again tomorrow, a pin\n *      dropped offline cannot be. The drain is bounded (``drainTimeoutMs``)\n *      because a dead radio hangs rather than rejects\n *      (docs/decisions/bounded-offline-read-paths.md), and a hung POST must\n *      not stop the areas from being tried.\n *   2. Then every area, ONE AT A TIME, in the order given. Each run already\n *      fetches its own list in turn, and two at once over a connection\n *      about to go would leave two half-done areas rather than one whole\n *      one.\n *\n * An area that throws is not an exception to the run: it is an area that\n * is not ready, and the next one is still tried.\n *\n * ## Outcomes\n *\n *   ready    The area's tiles are established and its content landed.\n *   partial  Something fell short \u2014 a repair that failed, content that did\n *            not land, or tiles this page could not judge at all.\n *   absent   This device no longer holds the area's tiles. A sync cannot\n *            mend that and never claims to; the remedy is a re-download.\n *\n * Exposes ``window.pwaOfflineReadyCore``:\n *\n *   run(deps)                    \u2192 Promise<RunResult>\n *   areaOutcome(syncResult)      \u2192 Outcome\n *   waitingCount(rows)           \u2192 number\n *   describe(result, strings)    \u2192 {summary: string, queue: string}\n *   progressText(step, strings)  \u2192 string\n *   DEFAULT_DRAIN_TIMEOUT_MS\n */\n(function () {\n  'use strict';\n\n  // Long enough for one batch of queued edits over a slow connection; short\n  // enough that a hung request does not cost the areas behind it the signal\n  // they were waiting for.\n  const DEFAULT_DRAIN_TIMEOUT_MS = 20000;\n\n  /** @typedef {'ready'|'partial'|'absent'} Outcome */\n  /** @typedef {{tiles?: string, content?: boolean}} SyncResult */\n  /**\n   * @typedef {{phase: 'queue'} | {phase: 'area', n: number, total: number, id: string}\n   *   | {phase: 'done', id: string, outcome: Outcome}} Step\n   */\n  /**\n   * @typedef {Object} RunDeps\n   * @property {function(): unknown} drain Sends the queued edits.\n   * @property {function(): Promise<number>|number} waiting How many edits\n   *   are still waiting once the drain has had its turn.\n   * @property {string[]} areas Area ids, in the order to run them.\n   * @property {function(string): Promise<Outcome>|Outcome} syncOne Runs one area.\n   * @property {function(Step): void} [onStep] Told of every step as it starts\n   *   and every area as it settles.\n   * @property {number} [drainTimeoutMs]\n   * @property {function(number): Promise<void>} [wait] Injected timer.\n   */\n  /**\n   * @typedef {Object} RunResult\n   * @property {number} total\n   * @property {number} ready\n   * @property {number} waiting\n   * @property {Array<{id: string, outcome: Outcome}>} outcomes\n   */\n\n  /**\n   * One ``syncArea`` answer as an outcome.\n   *\n   * Only ``'ok'`` (mended what was missing) and ``'none'`` (nothing was\n   * missing) are claims that the tiles are established \u2014 see\n   * docs/decisions/a-pre-departure-sync-is-unconditional.md. ``'unknown'``\n   * is not, so it is ``partial`` rather than ``ready``.\n   *\n   * @param {SyncResult|null|undefined} result\n   * @returns {Outcome}\n   */\n  function areaOutcome(result) {\n    if (!result) return 'partial';\n    if (result.tiles === 'absent') return 'absent';\n    const tilesOk = result.tiles === 'ok' || result.tiles === 'none';\n    return tilesOk && !!result.content ? 'ready' : 'partial';\n  }\n\n  /**\n   * How many ``queue:mutations`` rows are still waiting to send.\n   *\n   * A row marked ``failed`` is not waiting: it will never be retried, and\n   * the nav's sync badge already reports it as an error\n   * (docs/mutation-queue.md). Counting it here would promise a send that is\n   * not coming.\n   *\n   * @param {unknown} rows\n   * @returns {number}\n   */\n  function waitingCount(rows) {\n    if (!Array.isArray(rows)) return 0;\n    return rows.filter((row) => !!row && row.status !== 'failed').length;\n  }\n\n  /**\n   * @param {number} ms\n   * @returns {Promise<void>}\n   */\n  function defaultWait(ms) {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  /**\n   * Drain, then every area in turn.\n   *\n   * @param {RunDeps} deps\n   * @returns {Promise<RunResult>}\n   */\n  async function run(deps) {\n    const onStep = typeof deps.onStep === 'function' ? deps.onStep : () => {};\n    const wait = typeof deps.wait === 'function' ? deps.wait : defaultWait;\n    const timeoutMs = Number.isFinite(deps.drainTimeoutMs)\n      ? Number(deps.drainTimeoutMs)\n      : DEFAULT_DRAIN_TIMEOUT_MS;\n    const areas = Array.isArray(deps.areas) ? deps.areas.slice() : [];\n\n    onStep({ phase: 'queue' });\n    try {\n      await Promise.race([Promise.resolve(deps.drain()), wait(timeoutMs)]);\n    } catch (_err) {\n      // A drain that throws has still left the rows where they were; the\n      // count below says so.\n    }\n    let waiting = 0;\n    try {\n      const count = await deps.waiting();\n      waiting = Number.isFinite(count) && count > 0 ? count : 0;\n    } catch (_err) {\n      waiting = 0;\n    }\n\n    /** @type {Array<{id: string, outcome: Outcome}>} */\n    const outcomes = [];\n    let ready = 0;\n    for (let index = 0; index < areas.length; index += 1) {\n      const id = areas[index];\n      onStep({ phase: 'area', n: index + 1, total: areas.length, id: id });\n      /** @type {Outcome} */\n      let outcome = 'partial';\n      try {\n        const answer = await deps.syncOne(id);\n        if (answer === 'ready' || answer === 'absent') outcome = answer;\n      } catch (_err) {\n        outcome = 'partial';\n      }\n      if (outcome === 'ready') ready += 1;\n      outcomes.push({ id: id, outcome: outcome });\n      onStep({ phase: 'done', id: id, outcome: outcome });\n    }\n    return { total: areas.length, ready: ready, waiting: waiting, outcomes: outcomes };\n  }\n\n  /**\n   * ``%(name)s`` substitution, by name \u2014 the same rule as\n   * ``pwaStrings.interpolate``, restated so this core needs no other\n   * module loaded.\n   *\n   * @param {string|undefined} template\n   * @param {Record<string, string|number>} values\n   * @returns {string}\n   */\n  function fill(template, values) {\n    return String(template || '').replace(/%\\((\\w+)\\)s/g, (match, key) =>\n      Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match,\n    );\n  }\n\n  /**\n   * The button's words while a run is going.\n   *\n   * @param {Step|null} step\n   * @param {Record<string, string>} strings Carries ``ready-running`` and\n   *   ``ready-progress``.\n   * @returns {string}\n   */\n  function progressText(step, strings) {\n    if (step && step.phase === 'area') {\n      return fill(strings['ready-progress'], { n: step.n, total: step.total });\n    }\n    return fill(strings['ready-running'], {});\n  }\n\n  /**\n   * The press's outcome, in the caller's words.\n   *\n   * One summary line \u2014 every area ready, or how many of them are \u2014 and a\n   * second line only when the drain left edits behind.\n   *\n   * @param {RunResult} result\n   * @param {Record<string, string>} strings Carries ``ready-all``,\n   *   ``ready-all-one``, ``ready-some``, ``queue-waiting`` and\n   *   ``queue-waiting-one``.\n   * @returns {{summary: string, queue: string}}\n   */\n  function describe(result, strings) {\n    let summary = '';\n    if (result.total > 0) {\n      if (result.ready === result.total) {\n        summary =\n          result.total === 1\n            ? fill(strings['ready-all-one'], {})\n            : fill(strings['ready-all'], { total: result.total });\n      } else {\n        summary = fill(strings['ready-some'], { ready: result.ready, total: result.total });\n      }\n    }\n    let queue = '';\n    if (result.waiting === 1) queue = fill(strings['queue-waiting-one'], {});\n    else if (result.waiting > 1) queue = fill(strings['queue-waiting'], { n: result.waiting });\n    return { summary: summary, queue: queue };\n  }\n\n  window.pwaOfflineReadyCore = Object.freeze({\n    DEFAULT_DRAIN_TIMEOUT_MS: DEFAULT_DRAIN_TIMEOUT_MS,\n    run: run,\n    areaOutcome: areaOutcome,\n    waitingCount: waitingCount,\n    describe: describe,\n    progressText: progressText,\n  });\n})();\n"],
  "mappings": "CAwDC,UAAY,CACX,aA4CA,SAASA,EAAYC,EAAQ,CAC3B,OAAKA,EACDA,EAAO,QAAU,SAAiB,UACtBA,EAAO,QAAU,MAAQA,EAAO,QAAU,SACtCA,EAAO,QAAU,QAAU,UAH3B,SAItB,CAaA,SAASC,EAAaC,EAAM,CAC1B,OAAK,MAAM,QAAQA,CAAI,EAChBA,EAAK,OAAQC,GAAQ,CAAC,CAACA,GAAOA,EAAI,SAAW,QAAQ,EAAE,OAD7B,CAEnC,CAMA,SAASC,EAAYC,EAAI,CACvB,OAAO,IAAI,QAASC,GAAY,WAAWA,EAASD,CAAE,CAAC,CACzD,CAQA,eAAeE,EAAIC,EAAM,CACvB,MAAMC,EAAS,OAAOD,EAAK,QAAW,WAAaA,EAAK,OAAS,IAAM,CAAC,EAClEE,EAAO,OAAOF,EAAK,MAAS,WAAaA,EAAK,KAAOJ,EACrDO,EAAY,OAAO,SAASH,EAAK,cAAc,EACjD,OAAOA,EAAK,cAAc,EAC1B,IACEI,EAAQ,MAAM,QAAQJ,EAAK,KAAK,EAAIA,EAAK,MAAM,MAAM,EAAI,CAAC,EAEhEC,EAAO,CAAE,MAAO,OAAQ,CAAC,EACzB,GAAI,CACF,MAAM,QAAQ,KAAK,CAAC,QAAQ,QAAQD,EAAK,MAAM,CAAC,EAAGE,EAAKC,CAAS,CAAC,CAAC,CACrE,MAAe,CAGf,CACA,IAAIE,EAAU,EACd,GAAI,CACF,MAAMC,EAAQ,MAAMN,EAAK,QAAQ,EACjCK,EAAU,OAAO,SAASC,CAAK,GAAKA,EAAQ,EAAIA,EAAQ,CAC1D,MAAe,CACbD,EAAU,CACZ,CAGA,MAAME,EAAW,CAAC,EAClB,IAAIC,EAAQ,EACZ,QAASC,EAAQ,EAAGA,EAAQL,EAAM,OAAQK,GAAS,EAAG,CACpD,MAAMC,EAAKN,EAAMK,CAAK,EACtBR,EAAO,CAAE,MAAO,OAAQ,EAAGQ,EAAQ,EAAG,MAAOL,EAAM,OAAQ,GAAIM,CAAG,CAAC,EAEnE,IAAIC,EAAU,UACd,GAAI,CACF,MAAMC,EAAS,MAAMZ,EAAK,QAAQU,CAAE,GAChCE,IAAW,SAAWA,IAAW,YAAUD,EAAUC,EAC3D,MAAe,CACbD,EAAU,SACZ,CACIA,IAAY,UAASH,GAAS,GAClCD,EAAS,KAAK,CAAE,GAAIG,EAAI,QAASC,CAAQ,CAAC,EAC1CV,EAAO,CAAE,MAAO,OAAQ,GAAIS,EAAI,QAASC,CAAQ,CAAC,CACpD,CACA,MAAO,CAAE,MAAOP,EAAM,OAAQ,MAAOI,EAAO,QAASH,EAAS,SAAUE,CAAS,CACnF,CAWA,SAASM,EAAKC,EAAUC,EAAQ,CAC9B,OAAO,OAAOD,GAAY,EAAE,EAAE,QAAQ,eAAgB,CAACE,EAAOC,IAC5D,OAAO,UAAU,eAAe,KAAKF,EAAQE,CAAG,EAAI,OAAOF,EAAOE,CAAG,CAAC,EAAID,CAC5E,CACF,CAUA,SAASE,EAAaC,EAAMC,EAAS,CACnC,OAAID,GAAQA,EAAK,QAAU,OAClBN,EAAKO,EAAQ,gBAAgB,EAAG,CAAE,EAAGD,EAAK,EAAG,MAAOA,EAAK,KAAM,CAAC,EAElEN,EAAKO,EAAQ,eAAe,EAAG,CAAC,CAAC,CAC1C,CAcA,SAASC,EAAS7B,EAAQ4B,EAAS,CACjC,IAAIE,EAAU,GACV9B,EAAO,MAAQ,IACbA,EAAO,QAAUA,EAAO,MAC1B8B,EACE9B,EAAO,QAAU,EACbqB,EAAKO,EAAQ,eAAe,EAAG,CAAC,CAAC,EACjCP,EAAKO,EAAQ,WAAW,EAAG,CAAE,MAAO5B,EAAO,KAAM,CAAC,EAExD8B,EAAUT,EAAKO,EAAQ,YAAY,EAAG,CAAE,MAAO5B,EAAO,MAAO,MAAOA,EAAO,KAAM,CAAC,GAGtF,IAAI+B,EAAQ,GACZ,OAAI/B,EAAO,UAAY,EAAG+B,EAAQV,EAAKO,EAAQ,mBAAmB,EAAG,CAAC,CAAC,EAC9D5B,EAAO,QAAU,IAAG+B,EAAQV,EAAKO,EAAQ,eAAe,EAAG,CAAE,EAAG5B,EAAO,OAAQ,CAAC,GAClF,CAAE,QAAS8B,EAAS,MAAOC,CAAM,CAC1C,CAEA,OAAO,oBAAsB,OAAO,OAAO,CACzC,yBAA0B,IAC1B,IAAKxB,EACL,YAAaR,EACb,aAAcE,EACd,SAAU4B,EACV,aAAcH,CAChB,CAAC,CACH,GAAG",
  "names": ["areaOutcome", "result", "waitingCount", "rows", "row", "defaultWait", "ms", "resolve", "run", "deps", "onStep", "wait", "timeoutMs", "areas", "waiting", "count", "outcomes", "ready", "index", "id", "outcome", "answer", "fill", "template", "values", "match", "key", "progressText", "step", "strings", "describe", "summary", "queue"]
}
