dsh-unlag 1.9.114 → 1.9.116

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 +38 -10
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -100,15 +100,19 @@ const MAX_STARTUP_ROW_CHARS = 8192
100
100
  * The largest one startup script may be before a check refuses it. The app serves a whole profile as
101
101
  * a few bundles, and those grow with every installed plugin: one profile's largest bundle measured
102
102
  * 8.7 MB, which a smaller bound refused as if it were an attack and left the page unrebuildable.
103
- * A body past this bound is a mistake or an attempt to make a check read (and compile) an unbounded
104
- * amount of text inside the process that owns every running turn.
103
+ * Compiling a body costs several times its size in memory before it is thrown away, and up to three
104
+ * checks may run at once, so the bound is set close to what a profile actually serves: this one
105
+ * measured 15.7 MB across three bundles, its largest 9.2 MB. A body past 24 MB is not a profile
106
+ * this app ships with, and reading one would let a request make the process that owns every running
107
+ * turn hold text no page boots from.
105
108
  */
106
- const MAX_SCRIPT_BYTES = 64 * 1024 * 1024
109
+ const MAX_SCRIPT_BYTES = 24 * 1024 * 1024
107
110
  /**
108
- * The same bound for one whole startup list, so a list of large bodies cannot add up either. Only
109
- * the compile happens under it: what a check keeps per body is the ids it names, never its text.
111
+ * The same bound for one whole startup list, so a list of large bodies cannot add up either. Two
112
+ * measured profiles' worth: what a check keeps per body is the ids it names, never its text, so
113
+ * the whole list's transient text is what this bounds.
110
114
  */
111
- const MAX_TOTAL_SCRIPT_BYTES = 128 * 1024 * 1024
115
+ const MAX_TOTAL_SCRIPT_BYTES = 48 * 1024 * 1024
112
116
  /**
113
117
  * How large the request that carries the list may be. The list itself is bounded (64 addresses of at
114
118
  * most 8 KB, plus JSON escaping), so a bound below that would refuse a sound startup list from a
@@ -1710,11 +1714,14 @@ export async function verifyStartupRows(graph, fetchBody, rows, budgetMs = MAX_V
1710
1714
  for (const id of item.ids) if (served.has(id)) listed.add(id)
1711
1715
  for (const id of item.ids) if (!served.has(id)) leftOut.add(id)
1712
1716
  let response = null
1713
- try {
1714
- response = await fetchBody(item.url)
1715
- } catch (error) {
1716
- return { status: 'blocked', reason: 'network', detail: `${item.url}: ${String(error?.message ?? error)}` }
1717
+ const read = await withDeadline(() => fetchBody(item.url), budgetMs - (Date.now() - began))
1718
+ if (read.timedOut === true) {
1719
+ return { status: 'blocked', reason: 'slow', detail: `${item.url} did not answer within the time this check waits` }
1720
+ }
1721
+ if ('error' in read) {
1722
+ return { status: 'blocked', reason: 'network', detail: `${item.url}: ${String(read.error?.message ?? read.error)}` }
1717
1723
  }
1724
+ response = read.value
1718
1725
  // Reading the answer is the same kind of step as fetching it: a response whose status or body
1719
1726
  // cannot be read is one this check cannot approve, and saying which script failed beats
1720
1727
  // abandoning the whole list without a name in the reason.
@@ -1822,6 +1829,27 @@ export async function verifyStartup(clientModules, rows) {
1822
1829
  return verifyStartupRows(graph, (url) => readStartupScript(clientModules, url), rows)
1823
1830
  }
1824
1831
 
1832
+ /** The sentinel a read that ran past its time resolves to: a value, never an error. */
1833
+ const LATE = Symbol('late')
1834
+
1835
+ /**
1836
+ * One read the check will not wait past. The budget is counted between rows already; a single
1837
+ * address that never answers would otherwise hold the press past the moment the page stops waiting
1838
+ * for it, and the page would be told the check timed out with nothing naming what hung. Racing the
1839
+ * read keeps the reason the check's own, and the read itself is left to finish with its bytes
1840
+ * dropped, so nothing is left half-read.
1841
+ */
1842
+ async function withDeadline(work, ms) {
1843
+ let timer = null
1844
+ const late = new Promise((resolve) => {
1845
+ timer = setTimeout(() => resolve(LATE), Math.max(0, ms))
1846
+ })
1847
+ const settled = Promise.resolve().then(work).then((value) => ({ value }), (error) => ({ error }))
1848
+ const answer = await Promise.race([settled, late])
1849
+ if (timer !== null) clearTimeout(timer)
1850
+ return answer === LATE ? { timedOut: true } : answer
1851
+ }
1852
+
1825
1853
  /** What one press is told when the update it waits for did not answer in time. */
1826
1854
  const STILL_APPLYING = Object.freeze({ status: 'applying', warnings: [], pending: [], restartReason: null, failedEntries: [] });
1827
1855
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.114",
3
+ "version": "1.9.116",
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",