{
  "version": 3,
  "sources": ["downloads_restore_queue_core.js"],
  "sourcesContent": ["// @ts-check\n/*\n * static/js/downloads_restore_queue_core.js \u2014 the \"Download all\" queue\n * (SNOW-1140).\n *\n * A signed-in user on a new device sees their other device's areas under\n * \"Not on this device\" in the Manage downloads sheet. Each row has its own\n * \"Download here\"; \"Download all\" runs those same per-row paths for every\n * restorable row, ONE AT A TIME. This module is the ordering and the stop\n * rules, and nothing else: no DOM, no strings, no storage. Every effect is\n * an injected dependency, so the rules are unit-tested directly\n * (tests/js/test_downloads_restore_queue_core.js) and the sheet\n * (map_downloads_manager.js) supplies the real ones.\n *\n * ## Why one at a time\n *\n * Every pinned download on the page takes one lock\n * (``window.pwaBasemapDownloads.isBusy()`` \u2014 ``pinnedDownloadBusy`` in\n * map_basemap_downloads.js): the runs share one progress grid and one\n * storage budget, and a second start while the first holds the lock is\n * refused with ``'busy'``. Both start paths resolve once the run has\n * STARTED, not once it has finished, so the queue waits for the lock to\n * clear before it starts the next. The wait listens for the page's\n * ``snowdesk:downloads-changed`` broadcast and re-checks ``isBusy()``; a\n * poll backs the event up, because a run that fails records nothing and so\n * broadcasts nothing; and an upper bound stops a hung run from wedging the\n * queue for the life of the page.\n *\n * ## The stop rules, in the order they are asked\n *\n *   offline  The network mode reads offline before a start \u2014 the queue\n *            stops and the rest stay rows. Asked before and after the wait,\n *            because the wait can be long.\n *   timeout  The lock did not clear within the bound.\n *   failed   The PREVIOUS run settled and its area is not on this device \u2014\n *            it failed, or the user declined an eviction it asked about.\n *            That run has already said so (the shared download-error toast,\n *            or the user's own \"no\"), so this stop carries no reason of its\n *            own to show; ``silent`` is true.\n *   refusal  A start resolved ``started: false`` \u2014 over budget, quota,\n *            sign-in, busy, an area too big for this device. The refusal's\n *            own ``reason`` travels back, so the caller words it with the\n *            same toast the row's own \"Download here\" would have shown.\n *\n * An area that became present while the queue ran (another tab, a tap on\n * its own row) is SKIPPED, not re-fetched: running again would replace a\n * good download.\n *\n * Exposes ``window.pwaDownloadsRestoreQueueCore``:\n *\n *   runQueue(items, deps, options) \u2192 Promise<QueueResult>\n *   waitForIdle(deps, options)     \u2192 Promise<boolean>\n *   DEFAULT_TIMEOUT_MS, DEFAULT_POLL_MS\n */\n(function () {\n  'use strict';\n\n  // Fifteen minutes for ONE run to release the lock. A region at the\n  // ceiling over a slow connection takes minutes, not tens of them; a run\n  // still holding the lock after this is stuck (or waiting on an eviction\n  // banner nobody is answering), and the queue gives up rather than start\n  // the next behind it.\n  var DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;\n\n  // The backstop for the broadcast. A run that fails releases the lock\n  // without recording anything, so no ``snowdesk:downloads-changed`` fires\n  // for it; a cheap re-check every couple of seconds catches that.\n  var DEFAULT_POLL_MS = 2000;\n\n  /**\n   * @typedef {{areaId: string}} QueueItem\n   *   One restorable row. The queue reads only ``areaId``; everything else\n   *   on the item is the caller's, handed back to ``deps.start``.\n   *\n   * @typedef {{started: boolean, reason?: string, ok?: boolean}} StartOutcome\n   *\n   * @typedef {Object} QueueDeps\n   * @property {() => boolean} online Whether the app may use the network.\n   * @property {() => boolean} isBusy Whether a pinned download holds the\n   *   page-wide lock.\n   * @property {(listener: () => void) => (() => void)} subscribe Call\n   *   ``listener`` whenever what this device holds may have changed;\n   *   returns the unsubscribe.\n   * @property {() => Promise<Set<string>>} presentIds The area ids on this\n   *   device now.\n   * @property {(item: any) => Promise<StartOutcome>} start Start one item.\n   *\n   * @typedef {{timeoutMs?: number, pollMs?: number}} WaitOptions\n   *\n   * @typedef {Object} QueueResult\n   * @property {number} started How many runs the queue started.\n   * @property {number} skipped How many items were already present.\n   * @property {string|null} stopped ``null`` when the queue ran to the end;\n   *   otherwise ``'offline'``, ``'timeout'``, ``'failed'`` or the refusing\n   *   start's own reason (``'unknown'`` when it gave none).\n   * @property {boolean} silent Whether the stop has already been reported\n   *   by the run itself, so the caller should say nothing.\n   * @property {any} [item] The item the queue stopped at.\n   */\n\n  /**\n   * Resolve once the page-wide download lock is free, or the bound passes.\n   *\n   * @param {QueueDeps} deps\n   * @param {WaitOptions} [options]\n   * @returns {Promise<boolean>} ``true`` when the lock is free, ``false``\n   *   when the bound passed first.\n   */\n  function waitForIdle(deps, options) {\n    var opts = options || {};\n    var timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;\n    var pollMs = typeof opts.pollMs === 'number' ? opts.pollMs : DEFAULT_POLL_MS;\n    if (!deps.isBusy()) return Promise.resolve(true);\n    return new Promise(function (resolve) {\n      var finished = false;\n      /** @type {() => void} */\n      var unsubscribe = function () {};\n      /** @type {ReturnType<typeof setInterval> | undefined} */\n      var poll;\n      /** @type {ReturnType<typeof setTimeout> | undefined} */\n      var deadline;\n      /** @param {boolean} value */\n      function done(value) {\n        if (finished) return;\n        finished = true;\n        unsubscribe();\n        if (poll !== undefined) clearInterval(poll);\n        if (deadline !== undefined) clearTimeout(deadline);\n        resolve(value);\n      }\n      function check() {\n        if (!deps.isBusy()) done(true);\n      }\n      unsubscribe = deps.subscribe(check);\n      poll = setInterval(check, pollMs);\n      deadline = setTimeout(function () {\n        done(false);\n      }, timeoutMs);\n    });\n  }\n\n  /** The value a start that overran its bound resolves to. */\n  var START_TIMED_OUT = { started: false, reason: 'timeout' };\n\n  /**\n   * ``deps.start(item)``, bounded by the same ``timeoutMs`` as the lock\n   * wait (SNOW-1140 review). A custom-area restore resolves only once its\n   * run SETTLES, not when it is dispatched, so a run that keeps reporting\n   * progress without finishing would otherwise hold the queue on this\n   * await for ever and never reach ``waitForIdle``'s bound. On overrun\n   * the queue stops as ``'timeout'``; the run itself carries on, and\n   * still holds the page-wide lock, so nothing else is started under it.\n   *\n   * @param {QueueDeps} deps\n   * @param {QueueItem} item\n   * @param {WaitOptions} [options]\n   * @returns {Promise<StartOutcome>}\n   */\n  function boundedStart(deps, item, options) {\n    var opts = options || {};\n    var timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;\n    return new Promise(function (resolve, reject) {\n      var timer = setTimeout(function () {\n        resolve(START_TIMED_OUT);\n      }, timeoutMs);\n      Promise.resolve()\n        .then(function () {\n          return deps.start(item);\n        })\n        .then(\n          function (outcome) {\n            clearTimeout(timer);\n            resolve(outcome);\n          },\n          function (err) {\n            clearTimeout(timer);\n            reject(err);\n          },\n        );\n    });\n  }\n\n  /**\n   * Run every item through ``deps.start``, one at a time, until the list\n   * ends or a stop rule fires (see the module header for the rules).\n   *\n   * @param {Array<QueueItem>} items In the order they are listed.\n   * @param {QueueDeps} deps\n   * @param {WaitOptions} [options] The bound and poll for each wait.\n   * @returns {Promise<QueueResult>} Never rejects: a throwing dependency\n   *   stops the queue as ``'failed'``, not silently.\n   */\n  async function runQueue(items, deps, options) {\n    var list = Array.isArray(items) ? items : [];\n    /** @type {QueueResult} */\n    var result = { started: 0, skipped: 0, stopped: null, silent: false };\n    /** @type {QueueItem | null} */\n    var previous = null;\n\n    /**\n     * @param {string} reason\n     * @param {any} item\n     * @param {boolean} [silent]\n     * @returns {QueueResult}\n     */\n    function stop(reason, item, silent) {\n      result.stopped = reason;\n      result.item = item;\n      result.silent = !!silent;\n      return result;\n    }\n\n    try {\n      for (var i = 0; i < list.length; i += 1) {\n        var item = list[i];\n        if (!item || !item.areaId) continue;\n        if (!deps.online()) return stop('offline', item);\n        var idle = await waitForIdle(deps, options);\n        if (!idle) return stop('timeout', item);\n        // The wait can be long; the mode may have changed under it.\n        if (!deps.online()) return stop('offline', item);\n        var present = await deps.presentIds();\n        // The previous run has settled (the lock is free). Its area not\n        // being here means it failed or was declined, and it said so.\n        if (previous && !present.has(previous.areaId)) return stop('failed', previous, true);\n        previous = null;\n        if (present.has(item.areaId)) {\n          result.skipped += 1;\n          continue;\n        }\n        var outcome = await boundedStart(deps, item, options);\n        if (!outcome || !outcome.started) {\n          return stop((outcome && outcome.reason) || 'unknown', item);\n        }\n        result.started += 1;\n        // A restore that resolves after settling says whether it recorded\n        // the area; a false there is a run that already reported itself.\n        if (outcome.ok === false) return stop('failed', item, true);\n        previous = item;\n      }\n    } catch (_err) {\n      return stop('failed', null);\n    }\n    return result;\n  }\n\n  window.pwaDownloadsRestoreQueueCore = Object.freeze({\n    DEFAULT_TIMEOUT_MS: DEFAULT_TIMEOUT_MS,\n    DEFAULT_POLL_MS: DEFAULT_POLL_MS,\n    waitForIdle: waitForIdle,\n    runQueue: runQueue,\n  });\n})();\n"],
  "mappings": "CAsDC,UAAY,CACX,aAOA,IAAIA,EAAqB,IAKrBC,EAAkB,IAyCtB,SAASC,EAAYC,EAAMC,EAAS,CAClC,IAAIC,EAAOD,GAAW,CAAC,EACnBE,EAAY,OAAOD,EAAK,WAAc,SAAWA,EAAK,UAAYL,EAClEO,EAAS,OAAOF,EAAK,QAAW,SAAWA,EAAK,OAASJ,EAC7D,OAAKE,EAAK,OAAO,EACV,IAAI,QAAQ,SAAUK,EAAS,CACpC,IAAIC,EAAW,GAEXC,EAAc,UAAY,CAAC,EAE3BC,EAEAC,EAEJ,SAASC,EAAKC,EAAO,CACfL,IACJA,EAAW,GACXC,EAAY,EACRC,IAAS,QAAW,cAAcA,CAAI,EACtCC,IAAa,QAAW,aAAaA,CAAQ,EACjDJ,EAAQM,CAAK,EACf,CACA,SAASC,GAAQ,CACVZ,EAAK,OAAO,GAAGU,EAAK,EAAI,CAC/B,CACAH,EAAcP,EAAK,UAAUY,CAAK,EAClCJ,EAAO,YAAYI,EAAOR,CAAM,EAChCK,EAAW,WAAW,UAAY,CAChCC,EAAK,EAAK,CACZ,EAAGP,CAAS,CACd,CAAC,EA1B0B,QAAQ,QAAQ,EAAI,CA2BjD,CAGA,IAAIU,EAAkB,CAAE,QAAS,GAAO,OAAQ,SAAU,EAgB1D,SAASC,EAAad,EAAMe,EAAMd,EAAS,CACzC,IAAIC,EAAOD,GAAW,CAAC,EACnBE,EAAY,OAAOD,EAAK,WAAc,SAAWA,EAAK,UAAYL,EACtE,OAAO,IAAI,QAAQ,SAAUQ,EAASW,EAAQ,CAC5C,IAAIC,EAAQ,WAAW,UAAY,CACjCZ,EAAQQ,CAAe,CACzB,EAAGV,CAAS,EACZ,QAAQ,QAAQ,EACb,KAAK,UAAY,CAChB,OAAOH,EAAK,MAAMe,CAAI,CACxB,CAAC,EACA,KACC,SAAUG,EAAS,CACjB,aAAaD,CAAK,EAClBZ,EAAQa,CAAO,CACjB,EACA,SAAUC,EAAK,CACb,aAAaF,CAAK,EAClBD,EAAOG,CAAG,CACZ,CACF,CACJ,CAAC,CACH,CAYA,eAAeC,EAASC,EAAOrB,EAAMC,EAAS,CAC5C,IAAIqB,EAAO,MAAM,QAAQD,CAAK,EAAIA,EAAQ,CAAC,EAEvCE,EAAS,CAAE,QAAS,EAAG,QAAS,EAAG,QAAS,KAAM,OAAQ,EAAM,EAEhEC,EAAW,KAQf,SAASC,EAAKC,EAAQX,EAAMY,EAAQ,CAClC,OAAAJ,EAAO,QAAUG,EACjBH,EAAO,KAAOR,EACdQ,EAAO,OAAS,CAAC,CAACI,EACXJ,CACT,CAEA,GAAI,CACF,QAAS,EAAI,EAAG,EAAID,EAAK,OAAQ,GAAK,EAAG,CACvC,IAAIP,EAAOO,EAAK,CAAC,EACjB,GAAI,GAACP,GAAQ,CAACA,EAAK,QACnB,IAAI,CAACf,EAAK,OAAO,EAAG,OAAOyB,EAAK,UAAWV,CAAI,EAC/C,IAAIa,EAAO,MAAM7B,EAAYC,EAAMC,CAAO,EAC1C,GAAI,CAAC2B,EAAM,OAAOH,EAAK,UAAWV,CAAI,EAEtC,GAAI,CAACf,EAAK,OAAO,EAAG,OAAOyB,EAAK,UAAWV,CAAI,EAC/C,IAAIc,EAAU,MAAM7B,EAAK,WAAW,EAGpC,GAAIwB,GAAY,CAACK,EAAQ,IAAIL,EAAS,MAAM,EAAG,OAAOC,EAAK,SAAUD,EAAU,EAAI,EAEnF,GADAA,EAAW,KACPK,EAAQ,IAAId,EAAK,MAAM,EAAG,CAC5BQ,EAAO,SAAW,EAClB,QACF,CACA,IAAIL,EAAU,MAAMJ,EAAad,EAAMe,EAAMd,CAAO,EACpD,GAAI,CAACiB,GAAW,CAACA,EAAQ,QACvB,OAAOO,EAAMP,GAAWA,EAAQ,QAAW,UAAWH,CAAI,EAK5D,GAHAQ,EAAO,SAAW,EAGdL,EAAQ,KAAO,GAAO,OAAOO,EAAK,SAAUV,EAAM,EAAI,EAC1DS,EAAWT,EACb,CACF,MAAe,CACb,OAAOU,EAAK,SAAU,IAAI,CAC5B,CACA,OAAOF,CACT,CAEA,OAAO,6BAA+B,OAAO,OAAO,CAClD,mBAAoB1B,EACpB,gBAAiBC,EACjB,YAAaC,EACb,SAAUqB,CACZ,CAAC,CACH,GAAG",
  "names": ["DEFAULT_TIMEOUT_MS", "DEFAULT_POLL_MS", "waitForIdle", "deps", "options", "opts", "timeoutMs", "pollMs", "resolve", "finished", "unsubscribe", "poll", "deadline", "done", "value", "check", "START_TIMED_OUT", "boundedStart", "item", "reject", "timer", "outcome", "err", "runQueue", "items", "list", "result", "previous", "stop", "reason", "silent", "idle", "present"]
}
