dsh-panel-control 0.3.16 → 0.4.0
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 +8 -0
- package/client.js +1 -6
- package/index.js +7 -123
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,6 +109,14 @@ python -m http.server 8787 --bind 127.0.0.1 # 在仓库根目录
|
|
|
109
109
|
- 文案改成「刷新后仍然缺:重启 DSH 才会应用新的插件清单」——这才是真正有效的动作。
|
|
110
110
|
- 缺口修复后自动清除记忆,下次真出现新缺口还会提示。
|
|
111
111
|
|
|
112
|
+
## 0.4.0:不再有任何"等一会儿"的机制
|
|
113
|
+
|
|
114
|
+
前面几版为了对付"启动后入口不见了",加过看门狗、自愈改写、延时判断。这些都是**事后补偿**,代价是行为不可预测(改写自己的脚本会让加载器热替换,顺手把邻居插件的重复注册打挂)。0.4.0 把它们全部删掉了:
|
|
115
|
+
|
|
116
|
+
- 没有定时器、没有自查重试、不会改写任何文件。Host 半只做三件事:装路由、读一次插件图并记一行结论、做一次"我这个包的脚本取得到吗"的自检(都是**启动瞬间**完成的同步调用)。
|
|
117
|
+
- Client 半和别的插件一样:`dsh.client.immediately = true`,页面启动时随清单一起加载,立刻把入口挂到侧栏。
|
|
118
|
+
- 安装方式也建议和别的插件一致:用 **npm 安装**(`npm i dsh-panel-control`),而不是 `link:` 本地目录。`link:` 指向的是一个我随时在改的开发目录,任何一次编辑都会让页面手里的版本号作废;装成包之后文件是静止的,不存在这个窗口。
|
|
119
|
+
|
|
112
120
|
## 为什么重启后偶尔看不到入口(0.3.16)
|
|
113
121
|
|
|
114
122
|
DSH 给每个插件的脚本算一个**版本号**,来源是文件的 `mtime / ctime / 大小`,注册表只认它自己在组装那一刻读到的那个版本。页面拿到的是**启动时那份清单**,只要 `client.js` 在清单发出去之后被重写过(编辑器保存、`git` 换行归一化、或者早先那个"自愈"改写),页面手里的版本号就成了作废的号,请求回 404,而页面**不会报错**——入口就这么没了。
|
package/client.js
CHANGED
|
@@ -340,11 +340,6 @@ function build(require) {
|
|
|
340
340
|
return;
|
|
341
341
|
}
|
|
342
342
|
const missing = hostIds.filter((id) => !boot.ids.has(id));
|
|
343
|
-
// The Host can hand this very plugin to a page whose list predates it (see
|
|
344
|
-
// `recovery nudge` in the Host half). In that case the page is still missing
|
|
345
|
-
// an entry, but it is this one and it is already on screen: nagging the user
|
|
346
|
-
// to reload for it would be noise. Every other gap still counts.
|
|
347
|
-
const selfOnly = missing.length === 1 && missing[0] === PACKAGE && value.recovered === true;
|
|
348
343
|
// Only a real gap is worth a banner. A revision that merely differs proves
|
|
349
344
|
// nothing: the Host rebuilds that graph whenever any plugin file changes, so
|
|
350
345
|
// comparing revisions nagged people whose page was missing nothing at all.
|
|
@@ -353,7 +348,7 @@ function build(require) {
|
|
|
353
348
|
// Already said on an earlier load and a reload did not help: keep quiet, the
|
|
354
349
|
// diagnostics still list it.
|
|
355
350
|
const repeated = gap.length > 0 && readNagged() === gap;
|
|
356
|
-
const stale =
|
|
351
|
+
const stale = missing.length > 0 && !repeated;
|
|
357
352
|
if (stale) rememberNagged(gap);
|
|
358
353
|
else if (!missing.length) rememberNagged('');
|
|
359
354
|
if (!stale) forgetReloadMark();
|
package/index.js
CHANGED
|
@@ -45,31 +45,6 @@ const CLIENT_LOG = path.join(STATE_DIR, 'client.log');
|
|
|
45
45
|
const MAX_PREFS_BYTES = 256 * 1024;
|
|
46
46
|
const LOG_KEEP_BYTES = 256 * 1024;
|
|
47
47
|
|
|
48
|
-
/** The Client half this Host half ships with, used by the recovery nudge below. */
|
|
49
|
-
const CLIENT_FILE = fileURLToPath(new URL('./client.js', import.meta.url));
|
|
50
|
-
|
|
51
|
-
/** Set `DPC_NUDGE=off` to keep the Host strictly read-only about its own files. */
|
|
52
|
-
/**
|
|
53
|
-
* The nudge is opt-in, and that default is deliberate.
|
|
54
|
-
*
|
|
55
|
-
* Rewriting `client.js` makes the loader hot-swap this module into the live
|
|
56
|
-
* page. That re-evaluation runs the whole slot reconcile again, and a plugin
|
|
57
|
-
* whose registration is not idempotent then throws on its second
|
|
58
|
-
* `register({ id: 'workspace-control' })` and loses its sidebar entry: healing
|
|
59
|
-
* this plugin broke a neighbour's. A page reload fixes the frozen list without
|
|
60
|
-
* touching anyone else, so the safe path is the banner, and the nudge stays
|
|
61
|
-
* behind DPC_NUDGE=on for people who need it and accept the risk.
|
|
62
|
-
*/
|
|
63
|
-
const NUDGE_ENABLED = ['on', '1', 'true', 'yes'].includes(
|
|
64
|
-
String(process.env.DPC_NUDGE || '').trim().toLowerCase(),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
/** Set once the nudge below actually rewrote the Client file in this process. */
|
|
68
|
-
let recoveredAt = 0;
|
|
69
|
-
|
|
70
|
-
/** `String.fromCharCode(10)`: a literal newline in source is easy to mangle. */
|
|
71
|
-
const NL = String.fromCharCode(10);
|
|
72
|
-
|
|
73
48
|
/** Identifies one Host process, so a log spanning several launches shows which
|
|
74
49
|
* apply/mount lines belong together. */
|
|
75
50
|
const BOOT = Math.random().toString(36).slice(2, 8);
|
|
@@ -513,10 +488,6 @@ function inventorySnapshot(scope) {
|
|
|
513
488
|
rev: graph.rev,
|
|
514
489
|
release: typeof graph.release === 'string' ? graph.release : null,
|
|
515
490
|
ids: entries.map((entry) => String(entry && entry.id)).filter(Boolean),
|
|
516
|
-
// True when this process had to hand itself to the page: the page's list is
|
|
517
|
-
// still stale, but the entry it lacks is this very plugin, so there is
|
|
518
|
-
// nothing left for the user to do.
|
|
519
|
-
recovered: recoveredAt > 0,
|
|
520
491
|
},
|
|
521
492
|
};
|
|
522
493
|
}
|
|
@@ -577,6 +548,9 @@ let serveLoggedAt = 0;
|
|
|
577
548
|
*/
|
|
578
549
|
const servedBytes = new Map();
|
|
579
550
|
const SERVED_BYTES_KEPT = 5;
|
|
551
|
+
/** Requests to log per Host process: enough to see a boot, not a chatty stream. */
|
|
552
|
+
let bundleAsked = 0;
|
|
553
|
+
const BUNDLE_ASK_LIMIT = 8;
|
|
580
554
|
|
|
581
555
|
function revisionOf(url) {
|
|
582
556
|
const match = /[?&]rev=([A-Za-z0-9._-]+)/.exec(String(url || ''));
|
|
@@ -591,6 +565,10 @@ function serveCurrentRevision(scope) {
|
|
|
591
565
|
servePatched = modules;
|
|
592
566
|
modules.bundleResource = async function bundleResourceWithFallback(method, url) {
|
|
593
567
|
const answer = await original(method, url);
|
|
568
|
+
if (String(url || '').includes(PACKAGE) && bundleAsked < BUNDLE_ASK_LIMIT) {
|
|
569
|
+
bundleAsked += 1;
|
|
570
|
+
logHost(`bundle: asked rev ${String(revisionOf(url))} -> ${answer && answer.status}`);
|
|
571
|
+
}
|
|
594
572
|
if (answer && answer.status === 200 && method === 'GET' && String(url || '').includes(PACKAGE)) {
|
|
595
573
|
const rev = revisionOf(url);
|
|
596
574
|
if (rev && typeof answer.body === 'string') {
|
|
@@ -676,91 +654,6 @@ async function probeOwnBundle(scope) {
|
|
|
676
654
|
}
|
|
677
655
|
}
|
|
678
656
|
|
|
679
|
-
/**
|
|
680
|
-
* Say it out loud when the page never came back.
|
|
681
|
-
*
|
|
682
|
-
* The Host is up (this half ran) and yet the Client half reports nothing: the
|
|
683
|
-
* window was built from a plugin list that no longer matches the code on disk,
|
|
684
|
-
* so its script request went nowhere and the page silently omits the entry. The
|
|
685
|
-
* user sees a plugin that "does not start", so write the diagnosis where it can
|
|
686
|
-
* be found -- once per launch.
|
|
687
|
-
*
|
|
688
|
-
* This runs in a timer, outside every request and outside Cordis' own error
|
|
689
|
-
* handling: anything thrown here kills the whole Host process. Every branch is
|
|
690
|
-
* therefore contained, and the caller adds one more net.
|
|
691
|
-
*/
|
|
692
|
-
let watchdogSpoken = false;
|
|
693
|
-
|
|
694
|
-
async function watchFirstClientBoot(startedAt) {
|
|
695
|
-
try {
|
|
696
|
-
// Several applies can share one process (hot swap); only the first one speaks.
|
|
697
|
-
if (watchdogSpoken) return;
|
|
698
|
-
watchdogSpoken = true;
|
|
699
|
-
let text = '';
|
|
700
|
-
try {
|
|
701
|
-
text = await fs.readFile(CLIENT_LOG, 'utf8');
|
|
702
|
-
} catch (error) {
|
|
703
|
-
if (!error || error.code !== 'ENOENT') throw error;
|
|
704
|
-
}
|
|
705
|
-
const since = startedAt - 4_000;
|
|
706
|
-
let newest = 0;
|
|
707
|
-
let reported = false;
|
|
708
|
-
for (const line of text.split(String.fromCharCode(10))) {
|
|
709
|
-
if (!line.includes('"kind":"boot"')) continue;
|
|
710
|
-
const stamp = Date.parse(line.slice(0, line.indexOf(' ')));
|
|
711
|
-
if (!Number.isFinite(stamp)) continue;
|
|
712
|
-
if (stamp > newest) newest = stamp;
|
|
713
|
-
if (stamp >= since) reported = true;
|
|
714
|
-
}
|
|
715
|
-
if (reported) return;
|
|
716
|
-
// A page that reported shortly before this apply is an already-running page:
|
|
717
|
-
// it did load this plugin, and a Host hot swap (which happens on every edit
|
|
718
|
-
// while developing) must not be reported as "the window never got the code".
|
|
719
|
-
if (newest >= startedAt - 120_000) return;
|
|
720
|
-
logHost(
|
|
721
|
-
'client half did not report within 30s: the window is running a plugin list from before this code ' +
|
|
722
|
-
'(a rebuilt interface can take this long to finish loading; press the refresh button if the entry is missing)',
|
|
723
|
-
);
|
|
724
|
-
// The page will not ask for a script that is not in its frozen list, but it
|
|
725
|
-
// does watch this package's files: one byte-level change to the Client entry
|
|
726
|
-
// makes the loader hand the module to the live page. Flip the trailing
|
|
727
|
-
// newline, so the file stays byte-stable instead of growing on every launch.
|
|
728
|
-
if (!NUDGE_ENABLED) {
|
|
729
|
-
logHost('recovery nudge stayed off (opt in with DPC_NUDGE=on); a page reload is the safe fix');
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
try {
|
|
733
|
-
// The page may have reported while this timer was waiting: never rewrite a
|
|
734
|
-
// file the running page just loaded, because the resulting hot swap is
|
|
735
|
-
// what re-runs everybody's registration.
|
|
736
|
-
const late = await fs.readFile(CLIENT_LOG, 'utf8').catch(() => '');
|
|
737
|
-
const quiet = late
|
|
738
|
-
.split(String.fromCharCode(10))
|
|
739
|
-
.some((line) => {
|
|
740
|
-
if (!line.includes('"kind":"boot"')) return false;
|
|
741
|
-
const stamp = Date.parse(line.slice(0, line.indexOf(' ')));
|
|
742
|
-
return Number.isFinite(stamp) && stamp >= since;
|
|
743
|
-
});
|
|
744
|
-
if (quiet) {
|
|
745
|
-
logHost('recovery nudge skipped: the page reported while the watchdog was waiting');
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
const source = await fs.readFile(CLIENT_FILE, 'utf8');
|
|
749
|
-
const next = source.endsWith(NL + NL) ? source.slice(0, -1) : source + NL;
|
|
750
|
-
await fs.writeFile(CLIENT_FILE, next, 'utf8');
|
|
751
|
-
recoveredAt = Date.now();
|
|
752
|
-
logHost(
|
|
753
|
-
`recovery nudge: rewrote ${path.basename(CLIENT_FILE)} (${next.length - source.length} byte) so the ` +
|
|
754
|
-
'loader hands this plugin to the running page; the sidebar entry should appear within a second',
|
|
755
|
-
);
|
|
756
|
-
} catch (error) {
|
|
757
|
-
logHost(`recovery nudge failed: ${describeError(error)}`);
|
|
758
|
-
}
|
|
759
|
-
} catch (error) {
|
|
760
|
-
logHost(`client boot watchdog failed: ${describeError(error)}`);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
657
|
/** Register the route; returns null while `connection` is not available yet so
|
|
765
658
|
* that the caller can retry on a later tick. */
|
|
766
659
|
function mount(scope) {
|
|
@@ -798,15 +691,6 @@ export function apply(ctx) {
|
|
|
798
691
|
logHost(`route mounted at ${ROUTE_PATH} (attempt ${attempts})`);
|
|
799
692
|
logInventoryOnApply(scope);
|
|
800
693
|
serveCurrentRevision(scope);
|
|
801
|
-
const startedAt = Date.now();
|
|
802
|
-
// A timer callback that throws is fatal for the Host: contain it here and
|
|
803
|
-
// again inside the async body.
|
|
804
|
-
const watchdog = setTimeout(() => {
|
|
805
|
-
void watchFirstClientBoot(startedAt).catch((error) =>
|
|
806
|
-
logHost(`client boot watchdog failed: ${describeError(error)}`),
|
|
807
|
-
);
|
|
808
|
-
}, 30_000);
|
|
809
|
-
if (typeof watchdog.unref === 'function') watchdog.unref();
|
|
810
694
|
return;
|
|
811
695
|
}
|
|
812
696
|
if (!disposed) retry = setTimeout(() => attach(scope), 1000);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-panel-control",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Manage the global panel navigation at the top of the DSH sidebar: reorder, hide and group every entry, with the layout persisted per installation.",
|
|
6
6
|
"keywords": [
|