dsh-unlag 1.9.36 → 1.9.38

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 (3) hide show
  1. package/client.js +3 -2
  2. package/index.js +31 -4
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -15,8 +15,8 @@ window.__ModuleLoader__.load({
15
15
  * repeating one generic sentence.
16
16
  */
17
17
  const REASONS = zh
18
- ? { list: '读不到启动信息', slow: '启动信息读得太慢', row: '启动信息不完整', src: '启动脚本没有地址', graph: '已装插件清单读不到', plugin: '有插件已不在运行', script: '启动脚本读不到', empty: '启动脚本是空的', covers: '启动脚本缺插件', parse: '启动脚本内容不完整', verify: '插件后端没有应答' }
19
- : { list: 'startup info is unavailable', slow: 'the startup list answered too slowly', row: 'startup info is incomplete', src: 'a startup script has no address', graph: 'the plugin list cannot be read', plugin: 'a plugin is no longer running', script: 'a startup script cannot be read', empty: 'a startup script is empty', covers: 'a startup script misses a plugin', parse: 'a startup script is incomplete', verify: 'the plugin is not answering' };
18
+ ? { list: '读不到启动信息', slow: '启动信息读得太慢', row: '启动信息不完整', src: '启动脚本没有地址', graph: '已装插件清单读不到', plugin: '有插件已不在运行', script: '启动脚本读不到', empty: '启动脚本是空的', covers: '启动脚本缺插件', parse: '启动脚本内容不完整', size: '启动脚本太大', verify: '插件后端没有应答' }
19
+ : { list: 'startup info is unavailable', slow: 'the startup list answered too slowly', row: 'startup info is incomplete', src: 'a startup script has no address', graph: 'the plugin list cannot be read', plugin: 'a plugin is no longer running', script: 'a startup script cannot be read', empty: 'a startup script is empty', covers: 'a startup script misses a plugin', parse: 'a startup script is incomplete', size: 'a startup script is too large', verify: 'the plugin is not answering' };
20
20
 
21
21
  /** The last reason a readiness check refused for, or null after a check that passed. */
22
22
  let holdReason = null;
@@ -244,6 +244,7 @@ window.__ModuleLoader__.load({
244
244
  missing: 'script', // ui-copy-ignore
245
245
  network: 'script', // ui-copy-ignore
246
246
  parse: 'parse', // ui-copy-ignore
247
+ size: 'script', // ui-copy-ignore
247
248
  register: 'plugin', // ui-copy-ignore
248
249
  slow: 'slow', // ui-copy-ignore
249
250
  dependencies: 'covers',
package/index.js CHANGED
@@ -91,6 +91,14 @@ const DEFAULT_ROOTS = [join(homedir(), '.dsh', 'plugins')]
91
91
  const MAX_DEFERRALS = 40
92
92
  const MAX_STARTUP_ROWS = 64
93
93
  const MAX_STARTUP_ROW_CHARS = 4096
94
+ /**
95
+ * The largest one startup script may be before a check refuses it. A real client bundle is far
96
+ * below this; a body past it is either a mistake or an attempt to make a check read (and compile)
97
+ * an unbounded amount of text inside the process that owns every running turn.
98
+ */
99
+ const MAX_SCRIPT_BYTES = 8 * 1024 * 1024
100
+ /** The same bound for one whole startup list, so a list of large bodies cannot add up either. */
101
+ const MAX_TOTAL_SCRIPT_BYTES = 32 * 1024 * 1024
94
102
 
95
103
  /**
96
104
  * The most plugin ids one bundle address may name. A real startup list is far below this; a made-up
@@ -98,6 +106,18 @@ const MAX_STARTUP_ROW_CHARS = 4096
98
106
  * is work the request itself must not be able to grow without bound.
99
107
  */
100
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
+ }
101
121
  /**
102
122
  * How long one startup check may spend reading the list. Each address is bounded on its own, but a
103
123
  * list of them is not: dozens of slow addresses would otherwise hold a press past the moment the
@@ -966,10 +986,11 @@ async function currentEntryBody(clientModules, graph, id) {
966
986
  * answering with them in the requested order registers exactly the plugins the document expects,
967
987
  * so a boot that would otherwise 404 and end the app comes up instead.
968
988
  */
969
- async function regroupedBundleAnswer(clientModules, graph, request) {
989
+ async function regroupedBundleAnswer(clientModules, graph, request, deadline = Date.now() + answerBudgetMs()) {
970
990
  if (request.sourceMap) return null
971
991
  const parts = []
972
992
  for (const id of request.ids) {
993
+ if (Date.now() > deadline) return null
973
994
  const body = await currentEntryBody(clientModules, graph, id)
974
995
  if (body === null) return null
975
996
  parts.push(body)
@@ -997,7 +1018,7 @@ async function regroupedBundleAnswer(clientModules, graph, request) {
997
1018
  * booting from an old startup list that meets one ends the whole app: a boot that comes up without
998
1019
  * a plugin it no longer has beats that.
999
1020
  */
1000
- async function survivingBundleAnswer(clientModules, graph, request) {
1021
+ async function survivingBundleAnswer(clientModules, graph, request, deadline = Date.now() + answerBudgetMs()) {
1001
1022
  if (request.sourceMap) return null
1002
1023
  const entries = Array.isArray(graph?.entries) ? graph.entries : []
1003
1024
  const served = new Set(entries.filter((row) => typeof row?.id === 'string').map((row) => row.id))
@@ -1005,6 +1026,7 @@ async function survivingBundleAnswer(clientModules, graph, request) {
1005
1026
  if (gone.length === 0) return null
1006
1027
  const parts = []
1007
1028
  for (const id of request.ids) {
1029
+ if (Date.now() > deadline) return null
1008
1030
  if (served.has(id)) {
1009
1031
  const body = await currentEntryBody(clientModules, graph, id)
1010
1032
  if (body === null) return null
@@ -1023,7 +1045,7 @@ async function survivingBundleAnswer(clientModules, graph, request) {
1023
1045
  * that really contains every plugin it claims is handed back; anything unsure answers null, so the
1024
1046
  * route then behaves exactly as before and an unknown id still ends as a plain 404.
1025
1047
  */
1026
- async function currentBundleAnswer(clientModules, method, url) {
1048
+ async function currentBundleAnswer(clientModules, method, url, deadline = Date.now() + answerBudgetMs()) {
1027
1049
  const request = bundleRequest(url)
1028
1050
  if (request === null) return null
1029
1051
  const graph = clientModules.graph()
@@ -1049,7 +1071,7 @@ async function currentBundleAnswer(clientModules, method, url) {
1049
1071
  }
1050
1072
  }
1051
1073
  }
1052
- return (await regroupedBundleAnswer(clientModules, graph, request)) ?? survivingBundleAnswer(clientModules, graph, request)
1074
+ return (await regroupedBundleAnswer(clientModules, graph, request, deadline)) ?? survivingBundleAnswer(clientModules, graph, request, deadline)
1053
1075
  }
1054
1076
 
1055
1077
  /** Answer one stale request over the plain request/response route an app registers. */
@@ -1468,6 +1490,7 @@ export async function verifyStartupRows(graph, fetchBody, rows, budgetMs = MAX_V
1468
1490
  const listed = new Set()
1469
1491
  const bodies = []
1470
1492
  const began = Date.now()
1493
+ let totalBytes = 0
1471
1494
  for (const item of wanted) {
1472
1495
  if (Date.now() - began > budgetMs) {
1473
1496
  return { status: 'blocked', reason: 'slow', detail: 'the startup list took longer than this check waits' }
@@ -1485,6 +1508,10 @@ export async function verifyStartupRows(graph, fetchBody, rows, budgetMs = MAX_V
1485
1508
  }
1486
1509
  const bytes = Buffer.from(await response.arrayBuffer())
1487
1510
  if (bytes.length === 0) return { status: 'blocked', reason: 'empty', detail: `${item.url} answers an empty body` }
1511
+ totalBytes += bytes.length
1512
+ if (bytes.length > MAX_SCRIPT_BYTES || totalBytes > MAX_TOTAL_SCRIPT_BYTES) {
1513
+ return { status: 'blocked', reason: 'size', detail: `${item.url} answers ${String(bytes.length)} bytes; this check reads at most ${String(MAX_SCRIPT_BYTES)} per script and ${String(MAX_TOTAL_SCRIPT_BYTES)} in all` }
1514
+ }
1488
1515
  const text = bytes.toString('utf8')
1489
1516
  const complaint = parseAsScript(text, item.url)
1490
1517
  if (complaint !== null) return { status: 'blocked', reason: 'parse', detail: `${item.url}: ${complaint}` }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.36",
3
+ "version": "1.9.38",
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",