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 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 = !selfOnly && missing.length > 0 && !repeated;
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.16",
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": [