{
  "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  const STAGES = ['map', 'bulletins', 'weather', 'things'];\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: 'stage', n: number, total: number, id: string, name: StageName}\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   * bulletins, the weather or the reader's own things.\n   *\n   * @typedef {'map'|'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): 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); a host that cannot name its stages\n   *   ignores it.\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      const n = index + 1;\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.\n      let lastStage = '';\n      /** @param {StageName} name */\n      const onStage = (name) => {\n        if (STAGES.indexOf(name) < 0 || name === lastStage) return;\n        lastStage = name;\n        onStep({ phase: 'stage', n: n, total: areas.length, id: id, name: name });\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 };\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-bulletins``, ``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", "ready", "index", "id", "n", "lastStage", "onStage", "name", "outcome", "answer", "fill", "template", "values", "match", "key", "progressText", "step", "strings", "describe", "summary", "queue"]
}
