{
  "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 *   STAGES\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  // The stage names a sync may report, in the order it fetches them. A\n  // name outside the list is dropped rather than painted as a blank.\n  // SNOW-1487: `terrain` sits between the map and the content, as a sync\n  // adds the terrain data after the map.\n  const STAGES = ['map', 'terrain', 'bulletins', 'weather', 'things'];\n\n  /** @typedef {'ready'|'partial'|'absent'} Outcome */\n  /** @typedef {{tiles?: string, content?: boolean}} SyncResult */\n  /**\n   * SNOW-1487: how far a stage has got \u2014 files landed of files needed.\n   *\n   * @typedef {{done: number, total: number}} StageCounts\n   */\n  /**\n   * @typedef {{phase: 'queue'} | {phase: 'area', n: number, total: number, id: string}\n   *   | {phase: 'stage', n: number, total: number, id: string, name: StageName,\n   *      counts?: StageCounts}\n   *   | {phase: 'done', id: string, outcome: Outcome}} Step\n   */\n  /**\n   * What an area's sync is fetching now (SNOW-1415): the tiles, the\n   * terrain data (SNOW-1487), the bulletins, the weather or the reader's\n   * own things.\n   *\n   * @typedef {'map'|'terrain'|'bulletins'|'weather'|'things'} StageName\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, function(StageName, StageCounts=): void): Promise<Outcome>|Outcome} syncOne\n   *   Runs one area. The second argument is told each stage as the area's\n   *   sync starts it (SNOW-1415), and SNOW-1487 its counts as they move;\n   *   a host that cannot name its stages ignores it.\n   * @property {function(Step): void} [onStep] Told of every step as it starts\n   *   and every area as it settles.\n   * @property {AbortSignal} [signal] SNOW-1487: once aborted, no further\n   *   area is started; the ones left are `skipped`.\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   * @property {string[]} skipped SNOW-1487: the areas a stop left unsynced.\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    /** @type {string[]} */\n    const skipped = [];\n    let ready = 0;\n    for (let index = 0; index < areas.length; index += 1) {\n      const id = areas[index];\n      const n = index + 1;\n      // SNOW-1487: a stop leaves every area not yet started as it was.\n      if (deps.signal && deps.signal.aborted) {\n        skipped.push(...areas.slice(index));\n        break;\n      }\n      onStep({ phase: 'area', n: n, total: areas.length, id: id });\n      // A host that reports progress repeats the stage it is on, so only\n      // a change of stage is a step \u2014 or, SNOW-1487, a change of counts\n      // within it, which is how a row says \"212 of 528\".\n      let lastStage = '';\n      let lastCounts = '';\n      /**\n       * @param {StageName} name\n       * @param {StageCounts} [counts]\n       */\n      const onStage = (name, counts) => {\n        if (STAGES.indexOf(name) < 0) return;\n        const known = !!counts && Number.isFinite(counts.done) && Number.isFinite(counts.total);\n        const key = known ? counts.done + '/' + counts.total : '';\n        if (name === lastStage && key === lastCounts) return;\n        if (name === lastStage && !known) return;\n        lastStage = name;\n        lastCounts = key;\n        /** @type {Step} */\n        const step = { phase: 'stage', n: n, total: areas.length, id: id, name: name };\n        if (known) step.counts = { done: counts.done, total: counts.total };\n        onStep(step);\n      };\n      /** @type {Outcome} */\n      let outcome = 'partial';\n      try {\n        const answer = await deps.syncOne(id, onStage);\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, skipped: skipped };\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   * A ``stage`` step names what is being fetched (\"Weather (2 of 3)\")\n   * from ``sync-stage-<name>``, falling back to the plain count where the\n   * caller has no word for that stage.\n   *\n   * @param {Step|null} step\n   * @param {Record<string, string>} strings Carries ``ready-running``,\n   *   ``ready-progress`` and, optionally, ``sync-stage-map``,\n   *   ``sync-stage-terrain``, ``sync-stage-bulletins``,\n   *   ``sync-stage-weather`` and ``sync-stage-things``.\n   * @returns {string}\n   */\n  function progressText(step, strings) {\n    if (step && step.phase === 'stage' && strings['sync-stage-' + step.name]) {\n      return fill(strings['sync-stage-' + step.name], { n: step.n, total: step.total });\n    }\n    if (step && (step.phase === 'area' || step.phase === 'stage')) {\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    STAGES: STAGES,\n  });\n})();\n"],
  "mappings": "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",
  "names": ["STAGES", "areaOutcome", "result", "waitingCount", "rows", "row", "defaultWait", "ms", "resolve", "run", "deps", "onStep", "wait", "timeoutMs", "areas", "waiting", "count", "outcomes", "skipped", "ready", "index", "id", "n", "lastStage", "lastCounts", "onStage", "name", "counts", "known", "key", "step", "outcome", "answer", "fill", "template", "values", "match", "progressText", "strings", "describe", "summary", "queue"]
}
