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.
- package/README.md +2 -0
- package/index.js +78 -8
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1384
|
-
|
|
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.
|
|
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",
|