dsh-unlag 1.9.37 → 1.9.39

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 +37 -7
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -106,6 +106,18 @@ const MAX_TOTAL_SCRIPT_BYTES = 32 * 1024 * 1024
106
106
  * is work the request itself must not be able to grow without bound.
107
107
  */
108
108
  const MAX_BUNDLE_IDS = 128
109
+ /**
110
+ * How long one stale-bundle answer may spend before it gives the request back to the app's own
111
+ * route. Each row is bounded on its own, but a request can name many rows and the route answers
112
+ * whatever a socket asks: without a whole-request bound one made-up address holds a connection for
113
+ * as long as its rows add up.
114
+ */
115
+ const MAX_ANSWER_MS = 15_000
116
+ /** The same bound for a test that must reach it quickly. */
117
+ function answerBudgetMs() {
118
+ const set = Number(process.env.DSH_UNLAG_STALE_MS ?? '')
119
+ return set > 0 ? set : MAX_ANSWER_MS
120
+ }
109
121
  /**
110
122
  * How long one startup check may spend reading the list. Each address is bounded on its own, but a
111
123
  * list of them is not: dozens of slow addresses would otherwise hold a press past the moment the
@@ -180,6 +192,16 @@ function settingsOf(config) {
180
192
  }
181
193
  }
182
194
 
195
+ /**
196
+ * One log line, whatever the message. An address, a plugin id or an error can carry a line break,
197
+ * and a forged line in a file that is read as evidence is worse than a missing one; a message can
198
+ * also be arbitrarily long, and the log is one line per event.
199
+ */
200
+ export function oneLine(value, limit = 2000) {
201
+ const text = String(value ?? '').replace(/[\r\n\u2028\u2029]+/gu, ' ')
202
+ return text.length > limit ? `${text.slice(0, limit)}\u2026` : text
203
+ }
204
+
183
205
  function log(line) {
184
206
  try {
185
207
  const target = logTarget()
@@ -193,7 +215,7 @@ function log(line) {
193
215
  /* a missing or unreadable log is created by the append below */
194
216
  }
195
217
  }
196
- appendFileSync(target, `${new Date().toISOString()} ${line}\n`)
218
+ appendFileSync(target, `${new Date().toISOString()} ${oneLine(line)}\n`)
197
219
  } catch {}
198
220
  }
199
221
 
@@ -974,10 +996,11 @@ async function currentEntryBody(clientModules, graph, id) {
974
996
  * answering with them in the requested order registers exactly the plugins the document expects,
975
997
  * so a boot that would otherwise 404 and end the app comes up instead.
976
998
  */
977
- async function regroupedBundleAnswer(clientModules, graph, request) {
999
+ async function regroupedBundleAnswer(clientModules, graph, request, deadline = Date.now() + answerBudgetMs()) {
978
1000
  if (request.sourceMap) return null
979
1001
  const parts = []
980
1002
  for (const id of request.ids) {
1003
+ if (Date.now() > deadline) return null
981
1004
  const body = await currentEntryBody(clientModules, graph, id)
982
1005
  if (body === null) return null
983
1006
  parts.push(body)
@@ -1005,7 +1028,7 @@ async function regroupedBundleAnswer(clientModules, graph, request) {
1005
1028
  * booting from an old startup list that meets one ends the whole app: a boot that comes up without
1006
1029
  * a plugin it no longer has beats that.
1007
1030
  */
1008
- async function survivingBundleAnswer(clientModules, graph, request) {
1031
+ async function survivingBundleAnswer(clientModules, graph, request, deadline = Date.now() + answerBudgetMs()) {
1009
1032
  if (request.sourceMap) return null
1010
1033
  const entries = Array.isArray(graph?.entries) ? graph.entries : []
1011
1034
  const served = new Set(entries.filter((row) => typeof row?.id === 'string').map((row) => row.id))
@@ -1013,6 +1036,7 @@ async function survivingBundleAnswer(clientModules, graph, request) {
1013
1036
  if (gone.length === 0) return null
1014
1037
  const parts = []
1015
1038
  for (const id of request.ids) {
1039
+ if (Date.now() > deadline) return null
1016
1040
  if (served.has(id)) {
1017
1041
  const body = await currentEntryBody(clientModules, graph, id)
1018
1042
  if (body === null) return null
@@ -1031,7 +1055,7 @@ async function survivingBundleAnswer(clientModules, graph, request) {
1031
1055
  * that really contains every plugin it claims is handed back; anything unsure answers null, so the
1032
1056
  * route then behaves exactly as before and an unknown id still ends as a plain 404.
1033
1057
  */
1034
- async function currentBundleAnswer(clientModules, method, url) {
1058
+ async function currentBundleAnswer(clientModules, method, url, deadline = Date.now() + answerBudgetMs()) {
1035
1059
  const request = bundleRequest(url)
1036
1060
  if (request === null) return null
1037
1061
  const graph = clientModules.graph()
@@ -1057,7 +1081,7 @@ async function currentBundleAnswer(clientModules, method, url) {
1057
1081
  }
1058
1082
  }
1059
1083
  }
1060
- return (await regroupedBundleAnswer(clientModules, graph, request)) ?? survivingBundleAnswer(clientModules, graph, request)
1084
+ return (await regroupedBundleAnswer(clientModules, graph, request, deadline)) ?? survivingBundleAnswer(clientModules, graph, request, deadline)
1061
1085
  }
1062
1086
 
1063
1087
  /** Answer one stale request over the plain request/response route an app registers. */
@@ -1355,7 +1379,10 @@ function releaseBundleShim(shim) {
1355
1379
  }
1356
1380
  // The mark belongs to this wrapper only; a mark left by a newer one stays on the route.
1357
1381
  if (shim.route[SHIM_MARK] === shim.wrapped) delete shim.route[SHIM_MARK]
1358
- } catch {}
1382
+ } catch (error) {
1383
+ // A route left wrapped here would answer pages from a context that is going away.
1384
+ log(`cannot unwrap the bundle route: ${oneLine(error?.message ?? error)}`)
1385
+ }
1359
1386
  }
1360
1387
 
1361
1388
  /**
@@ -1558,7 +1585,10 @@ export async function readStartupScript(clientModules, url) {
1558
1585
  try {
1559
1586
  const answer = await currentBundleAnswer(clientModules, 'GET', url)
1560
1587
  if (answer !== null) return { status: 200, arrayBuffer: async () => answer.body }
1561
- } catch {}
1588
+ } catch (error) {
1589
+ // The fallback is still the right answer; the failure itself must not vanish.
1590
+ log(`cannot answer ${oneLine(url)} at the revision the app serves now: ${oneLine(error?.message ?? error)}; asking the app's own route`)
1591
+ }
1562
1592
  return fetchBundleAt(clientModules, url, 'GET')
1563
1593
  }
1564
1594
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.37",
3
+ "version": "1.9.39",
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",