dsh-unlag 1.9.22 → 1.9.24

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/README.md +2 -0
  2. package/index.js +78 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,8 @@ dsh plugin install dsh-unlag
65
65
 
66
66
  官方的插件资源路由对未知修订返回 404。本插件对旧页面记着的旧修订返回当前内容:桌面版在窗口启动时固定了启动清单,页面按旧修订请求脚本是常态,返回 404 会让新页面起不来、App 直接退出。这是本插件的兼容措施,不是官方行为;不需要时可以整个关掉(设置里两行都关)。
67
67
 
68
+ 同一原因下的第二种情况:启动清单里记着的插件在配置里已经被关掉或删掉了,官方路由对那一行只会 404。本插件改为返回一段空脚本占位,页面起来时不再加载这个插件,App 照常起来;日志会写明哪个插件被留下,`/dsh-unlag/verify` 的结论里也带 `leftOut` 字段。
69
+
68
70
  ## 排错
69
71
 
70
72
  - 日志:`~/.dsh/unlag-reload.log`,换代码/跳过/被拦各一行,含原因。
package/index.js CHANGED
@@ -85,6 +85,13 @@ const DEFAULT_ROOTS = [join(homedir(), '.dsh', 'plugins')]
85
85
  const MAX_DEFERRALS = 40
86
86
  const MAX_STARTUP_ROWS = 64
87
87
  const MAX_STARTUP_ROW_CHARS = 4096
88
+
89
+ /**
90
+ * The most plugin ids one bundle address may name. A real startup list is far below this; a made-up
91
+ * address far above it would otherwise have the plugin read one body per id, and answering a request
92
+ * is work the request itself must not be able to grow without bound.
93
+ */
94
+ const MAX_BUNDLE_IDS = 128
88
95
  const MIN_INTERVAL_MS = 1500
89
96
  /** The route the app publishes client bundles under; the same key the module table registers. */
90
97
  const BUNDLE_ROUTE = '/plugins'
@@ -848,8 +855,10 @@ function bundleRequest(rawUrl) {
848
855
  id = id.endsWith('/client.js') ? id.slice(0, -'/client.js'.length) : id
849
856
  if (id !== '') ids.push(id)
850
857
  }
851
- if (ids.length === 0) return null
852
- return { ids, sourceMap, rev: revAt < 0 ? '' : rest.slice(revAt + '&rev='.length) }
858
+ if (ids.length === 0 || ids.length > MAX_BUNDLE_IDS) return null
859
+ // The same plugin twice in one address is one plugin: read its body once.
860
+ const unique = [...new Set(ids)]
861
+ return { ids: unique, sourceMap, rev: revAt < 0 ? '' : rest.slice(revAt + '&rev='.length) }
853
862
  }
854
863
 
855
864
  /**
@@ -940,6 +949,35 @@ async function regroupedBundleAnswer(clientModules, graph, request) {
940
949
  return { body, contentType: 'text/javascript; charset=utf-8', ids: request.ids }
941
950
  }
942
951
 
952
+ /**
953
+ * A stale request can name a plugin the profile no longer has at all: it was turned off or removed
954
+ * after the document that asks was made. Every plugin in the request is still served on its own, so
955
+ * the ones that remain are answered and the ones that are gone take a comment's place, which a
956
+ * script can hold without registering anything. The app's own answer here is 404, and a document
957
+ * booting from an old startup list that meets one ends the whole app: a boot that comes up without
958
+ * a plugin it no longer has beats that.
959
+ */
960
+ async function survivingBundleAnswer(clientModules, graph, request) {
961
+ if (request.sourceMap) return null
962
+ const entries = Array.isArray(graph?.entries) ? graph.entries : []
963
+ const served = new Set(entries.filter((row) => typeof row?.id === 'string').map((row) => row.id))
964
+ const gone = request.ids.filter((id) => !served.has(id))
965
+ if (gone.length === 0) return null
966
+ const parts = []
967
+ for (const id of request.ids) {
968
+ if (served.has(id)) {
969
+ const body = await currentEntryBody(clientModules, graph, id)
970
+ if (body === null) return null
971
+ parts.push(body)
972
+ continue
973
+ }
974
+ parts.push(Buffer.from(`/* ${id.replace(/[^a-z0-9@/._-]/giu, '')} is not part of the plugin set served now */`))
975
+ }
976
+ if (parts.length === 0) return null
977
+ const body = Buffer.concat(parts.flatMap((part, index) => (index === parts.length - 1 ? [part] : [part, Buffer.from('\n;\n')])))
978
+ return { body, contentType: 'text/javascript; charset=utf-8', ids: request.ids, gone }
979
+ }
980
+
943
981
  /**
944
982
  * The plugin set one stale request names, answered at the revision the app serves now. Only a body
945
983
  * that really contains every plugin it claims is handed back; anything unsure answers null, so the
@@ -971,7 +1009,7 @@ async function currentBundleAnswer(clientModules, method, url) {
971
1009
  }
972
1010
  }
973
1011
  }
974
- return regroupedBundleAnswer(clientModules, graph, request)
1012
+ return (await regroupedBundleAnswer(clientModules, graph, request)) ?? survivingBundleAnswer(clientModules, graph, request)
975
1013
  }
976
1014
 
977
1015
  /** Answer one stale request over the plain request/response route an app registers. */
@@ -988,7 +1026,16 @@ async function serveCurrentBundle(clientModules, req, res) {
988
1026
  'content-length': String(answer.body.length),
989
1027
  })
990
1028
  res.end(method === 'HEAD' ? undefined : answer.body)
991
- log(`served ${answer.ids.join(', ')} at the current revision: a page asked for an older one`)
1029
+ if (Array.isArray(answer.gone) && answer.gone.length > 0) {
1030
+ const kept = answer.ids.filter((id) => !answer.gone.includes(id))
1031
+ log(
1032
+ kept.length === 0
1033
+ ? `answered an older set with an empty script in place of ${answer.gone.join(', ')}: the app would have ended on the table's 404`
1034
+ : `served ${kept.join(', ')} at the current revision and left out ${answer.gone.join(', ')}: a page asked for an older set`,
1035
+ )
1036
+ } else {
1037
+ log(`served ${answer.ids.join(', ')} at the current revision: a page asked for an older one`)
1038
+ }
992
1039
  return true
993
1040
  } catch (error) {
994
1041
  log(`cannot serve a bundle from an older revision: ${error?.message ?? error}`)
@@ -1037,7 +1084,16 @@ async function serveCurrentBundleResource(clientModules, request) {
1037
1084
  if (method !== 'GET' && method !== 'HEAD') return null
1038
1085
  const answer = await currentBundleAnswer(clientModules, method, request?.url)
1039
1086
  if (answer === null) return null
1040
- log(`served ${answer.ids.join(', ')} at the current revision: a page asked for an older one`)
1087
+ if (Array.isArray(answer.gone) && answer.gone.length > 0) {
1088
+ const kept = answer.ids.filter((id) => !answer.gone.includes(id))
1089
+ log(
1090
+ kept.length === 0
1091
+ ? `answered an older set with an empty script in place of ${answer.gone.join(', ')}: the app would have ended on the table's 404`
1092
+ : `served ${kept.join(', ')} at the current revision and left out ${answer.gone.join(', ')}: a page asked for an older set`,
1093
+ )
1094
+ } else {
1095
+ log(`served ${answer.ids.join(', ')} at the current revision: a page asked for an older one`)
1096
+ }
1041
1097
  return new Response(method === 'HEAD' ? null : answer.body, {
1042
1098
  status: 200,
1043
1099
  headers: {
@@ -1361,10 +1417,19 @@ export async function verifyStartupRows(graph, fetchBody, rows) {
1361
1417
  wanted.push({ url, ids: request === null ? [] : request.ids })
1362
1418
  }
1363
1419
  if (wanted.length === 0) return { status: 'blocked', reason: 'list', detail: 'the startup list names no script' }
1420
+ /**
1421
+ * A row can name a plugin the profile no longer has: the document that made it was composed
1422
+ * before that plugin was turned off. The route answers such a row with a stand-in that registers
1423
+ * nothing, so the boot comes up without the plugin; the check reads the same way and does not
1424
+ * demand a registration the set cannot give.
1425
+ */
1426
+ const served = new Set(entries.keys())
1427
+ const leftOut = new Set()
1364
1428
  const listed = new Set()
1365
1429
  const bodies = []
1366
1430
  for (const item of wanted) {
1367
- for (const id of item.ids) listed.add(id)
1431
+ for (const id of item.ids) if (served.has(id)) listed.add(id)
1432
+ for (const id of item.ids) if (!served.has(id)) leftOut.add(id)
1368
1433
  let response = null
1369
1434
  try {
1370
1435
  response = await fetchBody(item.url)
@@ -1380,8 +1445,9 @@ export async function verifyStartupRows(graph, fetchBody, rows) {
1380
1445
  const complaint = parseAsScript(text, item.url)
1381
1446
  if (complaint !== null) return { status: 'blocked', reason: 'parse', detail: `${item.url}: ${complaint}` }
1382
1447
  const registered = registeredIds(text)
1383
- const missing = item.ids.filter((id) => !registered.has(id))
1384
- if (item.ids.length > 0 && missing.length > 0) {
1448
+ const required = item.ids.filter((id) => served.has(id))
1449
+ const missing = required.filter((id) => !registered.has(id))
1450
+ if (required.length > 0 && missing.length > 0) {
1385
1451
  return { status: 'blocked', reason: 'register', detail: `${item.url} does not register ${missing.join(', ')}` }
1386
1452
  }
1387
1453
  bodies.push({
@@ -1418,6 +1484,7 @@ export async function verifyStartupRows(graph, fetchBody, rows) {
1418
1484
  reason: 'ok', // ui-copy-ignore
1419
1485
  revision: typeof graph?.rev === 'string' ? graph.rev : null,
1420
1486
  scripts: bodies.map((body) => ({ url: body.url, digest: body.digest, ids: body.ids })),
1487
+ ...(leftOut.size > 0 ? { leftOut: [...leftOut] } : {}),
1421
1488
  }
1422
1489
  }
1423
1490
 
@@ -1508,6 +1575,9 @@ function serveButton(ctx, controller, config) {
1508
1575
  } catch (error) {
1509
1576
  verdict = { status: 'blocked', reason: 'list', detail: String(error?.message ?? error) }
1510
1577
  }
1578
+ if (Array.isArray(verdict?.leftOut) && verdict.leftOut.length > 0) {
1579
+ log(`a startup row names a plugin this app no longer serves: ${verdict.leftOut.join(', ')}; the page comes up without it`)
1580
+ }
1511
1581
  sendJson(res, 200, verdict)
1512
1582
  } catch (error) {
1513
1583
  log(`cannot answer ${BASE_PATH}/verify: ${error?.message ?? error}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.22",
3
+ "version": "1.9.24",
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",