dsh-unlag 1.9.65 → 1.9.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +69 -36
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1668,10 +1668,25 @@ export async function verifyStartupRows(graph, fetchBody, rows, budgetMs = MAX_V
1668
1668
  } catch (error) {
1669
1669
  return { status: 'blocked', reason: 'network', detail: `${item.url}: ${String(error?.message ?? error)}` }
1670
1670
  }
1671
- if (response === null || response === undefined || response.status !== 200) {
1672
- return { status: 'blocked', reason: 'missing', detail: `${item.url} answers ${String(response?.status ?? 'nothing')}` }
1671
+ // Reading the answer is the same kind of step as fetching it: a response whose status or body
1672
+ // cannot be read is one this check cannot approve, and saying which script failed beats
1673
+ // abandoning the whole list without a name in the reason.
1674
+ let status = null
1675
+ let readable = true
1676
+ try {
1677
+ status = response?.status ?? null
1678
+ } catch {
1679
+ readable = false
1680
+ }
1681
+ if (!readable || response === null || response === undefined || status !== 200) {
1682
+ return { status: 'blocked', reason: 'missing', detail: `${item.url} answers ${readable ? String(status ?? 'nothing') : 'nothing readable'}` }
1683
+ }
1684
+ let bytes = null
1685
+ try {
1686
+ bytes = Buffer.from(await response.arrayBuffer())
1687
+ } catch (error) {
1688
+ return { status: 'blocked', reason: 'missing', detail: `${item.url} cannot be read: ${String(error?.message ?? error)}` }
1673
1689
  }
1674
- const bytes = Buffer.from(await response.arrayBuffer())
1675
1690
  if (bytes.length === 0) return { status: 'blocked', reason: 'empty', detail: `${item.url} answers an empty body` }
1676
1691
  totalBytes += bytes.length
1677
1692
  if (bytes.length > MAX_SCRIPT_BYTES || totalBytes > MAX_TOTAL_SCRIPT_BYTES) {
@@ -1754,6 +1769,45 @@ export async function verifyStartup(clientModules, rows) {
1754
1769
  return verifyStartupRows(graph, (url) => readStartupScript(clientModules, url), rows)
1755
1770
  }
1756
1771
 
1772
+ /** What one press is told when the update it waits for did not answer in time. */
1773
+ const STILL_APPLYING = Object.freeze({ status: 'applying', warnings: [], pending: [], restartReason: null, failedEntries: [] });
1774
+
1775
+ /**
1776
+ * The answer a press waits for, bounded in time. The update is asked for at most once and its
1777
+ * answer is settled the same way whether it is a promise, a plain value, or a promise that cannot
1778
+ * even be awaited: the page is always told one status, and nothing a plugin swap answers can turn
1779
+ * the route into an error.
1780
+ * @returns {{ outcome: object, timeRanOut: boolean, work: Promise<unknown> }}
1781
+ */
1782
+ export async function answerReload(controller, limitMs) {
1783
+ let work = null
1784
+ try {
1785
+ work = Promise.resolve(controller.reloadNow())
1786
+ } catch (error) {
1787
+ log(`a press could not be started: ${error?.message ?? error}`)
1788
+ return { outcome: { status: 'failed', warnings: [], pending: [], restartReason: null, failedEntries: [] }, timeRanOut: false, work: Promise.resolve() }
1789
+ }
1790
+ let timer = null
1791
+ let timeRanOut = false
1792
+ const ranOut = new Promise((resolve) => {
1793
+ timer = setTimeout(() => {
1794
+ timeRanOut = true
1795
+ resolve(STILL_APPLYING)
1796
+ }, limitMs)
1797
+ })
1798
+ try {
1799
+ const outcome = await Promise.race([work.catch(() => null), ranOut])
1800
+ // Reading the status can throw too; an answer that cannot be read is not one the page can use.
1801
+ try {
1802
+ if (outcome !== null && typeof outcome === 'object' && typeof outcome.status === 'string') return { outcome, timeRanOut, work }
1803
+ } catch {}
1804
+ // An update that answers nothing usable is still a press that has to be answered.
1805
+ return { outcome: { status: 'failed', warnings: [], pending: [], restartReason: null, failedEntries: [] }, timeRanOut, work }
1806
+ } finally {
1807
+ if (timer !== null) clearTimeout(timer)
1808
+ }
1809
+ }
1810
+
1757
1811
  /**
1758
1812
  * The entry points the header button calls. Each answers with the current switches so the page
1759
1813
  * never guesses: with both off it does nothing, and a turn still running is reported rather than
@@ -1888,42 +1942,21 @@ function serveButton(ctx, controller, config) {
1888
1942
  if (buttonAllowed(req) === false) return sendJson(res, 403, { status: 'forbidden' })
1889
1943
  if (!settings.reloadPlugins) return sendJson(res, 200, { status: 'off' })
1890
1944
  const limit = Number(process.env.DSH_UNLAG_ANSWER_MS ?? '') || RELOAD_ANSWER_MS
1891
- let timer = null
1892
- let timedOut = false
1893
- const stillApplying = new Promise((resolve) => {
1894
- timer = setTimeout(() => {
1895
- // Only the timer's own answer means the update outlasted the page: an update that
1896
- // says "applying" itself is a press that arrived during another one, not a slow one.
1897
- timedOut = true
1898
- resolve({ status: 'applying', warnings: [], pending: [], restartReason: null, failedEntries: [] })
1899
- }, limit)
1900
- })
1901
- let answeredEarly = false
1902
- try {
1903
- const work = controller.reloadNow()
1904
- /**
1905
- * The answer the page waits for is bounded, but the update is not. Whatever the update
1906
- * did after the page was told it is still applying is written down here: a replacement
1907
- * that failed a moment later must not look like one that succeeded.
1908
- */
1945
+ const { outcome, timeRanOut, work } = await answerReload(controller, limit)
1946
+ /**
1947
+ * The answer the page waits for is bounded, but the update is not. Whatever the update
1948
+ * did after the page was told it is still applying is written down here: a replacement
1949
+ * that failed a moment later must not look like one that succeeded.
1950
+ */
1951
+ if (outcome.status === 'applying') {
1952
+ if (timeRanOut) log('the update outlasted the answer the page waits for: the press is told it is still applying')
1953
+ else log('a press arrived while an update is already applying: the press is told it is still applying')
1909
1954
  work.then(
1910
- (late) => {
1911
- if (answeredEarly) log(`the update finished after the page was told it is still applying: ${String(late?.status ?? 'no status')}`)
1912
- },
1913
- (error) => {
1914
- if (answeredEarly) log(`the update failed after the page was told it is still applying: ${error?.message ?? error}`)
1915
- },
1955
+ (late) => log(`the update finished after the page was told it is still applying: ${String(late?.status ?? 'no status')}`),
1956
+ (error) => log(`the update failed after the page was told it is still applying: ${error?.message ?? error}`),
1916
1957
  )
1917
- const outcome = await Promise.race([work, stillApplying])
1918
- if (outcome?.status === 'applying') {
1919
- answeredEarly = true
1920
- if (timedOut) log('the update outlasted the answer the page waits for: the press is told it is still applying')
1921
- else log('a press arrived while an update is already applying: the press is told it is still applying')
1922
- }
1923
- sendJson(res, 200, outcome)
1924
- } finally {
1925
- if (timer !== null) clearTimeout(timer)
1926
1958
  }
1959
+ sendJson(res, 200, outcome)
1927
1960
  } catch (error) {
1928
1961
  log(`cannot apply a plugin reload: ${error?.message ?? error}`)
1929
1962
  sendJson(res, 500, { status: 'failed', detail: String(error?.message ?? error) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.65",
3
+ "version": "1.9.67",
4
4
  "type": "module",
5
5
  "description": "A press applies plugin edits right away and brings a laggy interface back; automatic reloading stays off unless you turn it on, so the press is the only trigger, and nothing restarts.",
6
6
  "license": "MIT",