dsh-life-pack 0.3.6 → 0.3.13

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/dist/client.js CHANGED
@@ -518,6 +518,112 @@ window.__ModuleLoader__.load({
518
518
  return lines;
519
519
  }
520
520
  //#endregion
521
+ //#region src/update-queue.ts
522
+ /** 任一行 installing 即忙:题头检查更新禁用、行按钮全局互斥、checkAll/act 入口直接返回的同一判据。 */
523
+ function hasInstallingRow(rows) {
524
+ for (const key of Object.keys(rows)) if (rows[key]?.phase === "installing") return true;
525
+ return false;
526
+ }
527
+ /** 按目标顺序逐家查→装→收尾:一家收尾才起下一家;失败记 failed 继续;待重启跳过安装。
528
+ *
529
+ * 跳过安装只有一种:快照事实已是待重启(`restartPendingOf`)——由横幅点名,不在这里装第二遍。
530
+ * 不可装(已是最新/拦下且无安装动作)记 ready 收尾;缺席且无最新版本号记 ready 收尾(等下一次检查)。
531
+ * 本函数不汇总排队数、不碰他行(`patch` 只收本家 key)。
532
+ */
533
+ async function runSerialUpdateAll(targets, deps) {
534
+ for (const target of targets) {
535
+ const current = deps.getRow(target.key);
536
+ deps.patch(target.key, {
537
+ phase: "checking",
538
+ outcome: current?.outcome ?? null,
539
+ failure: null
540
+ });
541
+ const checked = await deps.check(target);
542
+ if (!checked.ok) {
543
+ deps.patch(target.key, {
544
+ phase: "failed",
545
+ outcome: null,
546
+ failure: checked
547
+ });
548
+ continue;
549
+ }
550
+ if (restartPendingOf(checked.value.snapshot)) {
551
+ deps.patch(target.key, {
552
+ phase: "ready",
553
+ outcome: checked.value,
554
+ failure: null
555
+ });
556
+ continue;
557
+ }
558
+ const verdict = verdictOf(target, checked.value.snapshot);
559
+ if (!(verdict.action === "update" || verdict.action === "install" || verdict.action === "retry")) {
560
+ deps.patch(target.key, {
561
+ phase: "ready",
562
+ outcome: checked.value,
563
+ failure: null
564
+ });
565
+ continue;
566
+ }
567
+ deps.patch(target.key, {
568
+ phase: "installing",
569
+ outcome: checked.value,
570
+ failure: null
571
+ });
572
+ if (isAbsent(target)) {
573
+ const version = checked.value.snapshot.latestVersion;
574
+ if (!version) {
575
+ deps.patch(target.key, {
576
+ phase: "ready",
577
+ outcome: checked.value,
578
+ failure: null
579
+ });
580
+ continue;
581
+ }
582
+ const done = await deps.installAbsent(target, version);
583
+ if (done.ok) {
584
+ const refreshed = await deps.checkStatus(target);
585
+ deps.patch(target.key, refreshed.ok ? {
586
+ phase: "ready",
587
+ outcome: refreshed.value,
588
+ failure: null
589
+ } : {
590
+ phase: "failed",
591
+ outcome: null,
592
+ failure: refreshed
593
+ });
594
+ } else deps.patch(target.key, {
595
+ phase: "failed",
596
+ outcome: checked.value,
597
+ failure: {
598
+ ...done,
599
+ manual: done.manual ?? checked.value.manual ?? null
600
+ }
601
+ });
602
+ continue;
603
+ }
604
+ const done = await deps.installPresent(target);
605
+ if (done.ok) {
606
+ const refreshed = await deps.checkStatus(target);
607
+ deps.patch(target.key, refreshed.ok ? {
608
+ phase: "ready",
609
+ outcome: refreshed.value,
610
+ failure: null
611
+ } : {
612
+ phase: "failed",
613
+ outcome: null,
614
+ failure: refreshed
615
+ });
616
+ } else deps.patch(target.key, {
617
+ phase: "failed",
618
+ outcome: checked.value,
619
+ failure: {
620
+ ...done,
621
+ manual: done.manual ?? checked.value.manual ?? null
622
+ }
623
+ });
624
+ }
625
+ }
626
+ //#endregion
521
627
  //#region src/update-panel.ts
522
628
  /** 爱生活面板的更新界面:检查更新按钮、七家结果行、待重启横幅、缺席卡的「装上」。
523
629
  *
@@ -794,13 +900,21 @@ window.__ModuleLoader__.load({
794
900
  outcome: null,
795
901
  failure: null
796
902
  };
797
- /** 面板的更新状态源:挂载时取七个目标的表,之后按需查/装(组件与按钮共用一个 face)。 */
903
+ /** 面板的更新状态源:挂载时取七个目标的表,之后按需查/装(组件与按钮共用一个 face)。
904
+ *
905
+ * 并发形状(#925 拍板四条,用户已确认):
906
+ * - 一键全部更新按目标顺序串行(查→装→轮询到收尾,一家收尾才起下一家;失败记 failed 继续);
907
+ * - 任一行 installing 时 checkAll 入口直接返回、题头禁用;
908
+ * - act 入口有忙直接返回(有忙拒 update-busy 且不改 rows,含同家第二点);
909
+ * - 每行各自显示(patch 只写本家 key),题头不汇总排队数。
910
+ */
798
911
  function useUpdateRows(getCall) {
799
912
  const [targets, setTargets] = react.useState([]);
800
913
  const [rows, setRows] = react.useState({});
801
914
  const [pollMs, setPollMs] = react.useState(1e3);
802
915
  const [loadError, setLoadError] = react.useState(null);
803
916
  const [checking, setChecking] = react.useState(false);
917
+ const [updatingAll, setUpdatingAll] = react.useState(false);
804
918
  const [open, setOpen] = react.useState(false);
805
919
  const alive = react.useRef(true);
806
920
  react.useEffect(() => () => {
@@ -825,21 +939,31 @@ window.__ModuleLoader__.load({
825
939
  [key]: next
826
940
  }));
827
941
  }, []);
942
+ const rowsRef = react.useRef(rows);
943
+ rowsRef.current = rows;
944
+ const targetsRef = react.useRef(targets);
945
+ targetsRef.current = targets;
946
+ const pollMsRef = react.useRef(pollMs);
947
+ pollMsRef.current = pollMs;
948
+ const updatingAllRef = react.useRef(false);
828
949
  return {
829
950
  targets,
830
951
  rows,
831
952
  pollMs,
832
953
  loadError,
833
954
  checking,
955
+ updatingAll,
834
956
  open,
835
957
  close: () => setOpen(false),
836
958
  checkAll: react.useCallback(() => {
959
+ if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
837
960
  setChecking(true);
838
961
  setOpen(true);
839
- const list = targets;
962
+ const list = targetsRef.current;
963
+ const snapshot = rowsRef.current;
840
964
  for (const target of list) patch(target.key, {
841
965
  phase: "checking",
842
- outcome: rows[target.key]?.outcome ?? null,
966
+ outcome: snapshot[target.key]?.outcome ?? null,
843
967
  failure: null
844
968
  });
845
969
  Promise.all(list.map(async (target) => {
@@ -853,15 +977,41 @@ window.__ModuleLoader__.load({
853
977
  outcome: null,
854
978
  failure: result
855
979
  });
856
- })).finally(() => setChecking(false));
980
+ })).finally(() => {
981
+ if (alive.current) setChecking(false);
982
+ });
983
+ }, [getCall, patch]),
984
+ updateAll: react.useCallback(() => {
985
+ if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
986
+ const list = targetsRef.current;
987
+ if (list.length === 0) return;
988
+ updatingAllRef.current = true;
989
+ setUpdatingAll(true);
990
+ setOpen(true);
991
+ (async () => {
992
+ try {
993
+ await runSerialUpdateAll(list, {
994
+ check: (target) => checkTarget(getCall(), target),
995
+ checkStatus: (target) => checkTarget(getCall(), target, target.phones?.status),
996
+ installAbsent: (target, version) => installAbsent(getCall(), target, version),
997
+ installPresent: (target) => updateInstalled(getCall(), target, pollMsRef.current),
998
+ patch,
999
+ getRow: (key) => rowsRef.current[key]
1000
+ });
1001
+ await reload();
1002
+ } finally {
1003
+ updatingAllRef.current = false;
1004
+ if (alive.current) setUpdatingAll(false);
1005
+ }
1006
+ })();
857
1007
  }, [
858
1008
  getCall,
859
1009
  patch,
860
- rows,
861
- targets
1010
+ reload
862
1011
  ]),
863
1012
  act: react.useCallback((target, action) => {
864
- const current = rows[target.key] ?? IDLE$1;
1013
+ if (hasInstallingRow(rowsRef.current) || updatingAllRef.current) return;
1014
+ const current = rowsRef.current[target.key] ?? IDLE$1;
865
1015
  if (action === "recheck") {
866
1016
  patch(target.key, {
867
1017
  phase: "checking",
@@ -907,7 +1057,7 @@ window.__ModuleLoader__.load({
907
1057
  });
908
1058
  version = checked.value.snapshot.latestVersion;
909
1059
  }
910
- const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMs);
1060
+ const result = absent ? await installAbsent(getCall(), target, version ?? "") : await updateInstalled(getCall(), target, pollMsRef.current);
911
1061
  if (result.ok) {
912
1062
  const refreshed = await checkTarget(getCall(), target, target.phones?.status);
913
1063
  patch(target.key, refreshed.ok ? {
@@ -934,27 +1084,42 @@ window.__ModuleLoader__.load({
934
1084
  }, [
935
1085
  getCall,
936
1086
  patch,
937
- pollMs,
938
- reload,
939
- rows
1087
+ reload
940
1088
  ])
941
1089
  };
942
1090
  }
943
- /** 标题右侧的「检查更新」:一个按钮一次查七家(吃 `useUpdateRows` 的表)。 */
1091
+ /** 标题右侧两件:「检查更新」一次查七家 + 「全部更新」按顺序串行装(吃 `useUpdateRows` 的表)。
1092
+ *
1093
+ * 禁用形状(#925 第 2 条):任一行 installing 时两件都点不得;人话沿用 `update-busy` 原句
1094
+ * (挂在 title 上,按钮文本仍是动作名)。题头不汇总排队数:串行进度只在各行自己显示。
1095
+ */
944
1096
  function CheckUpdateButton(props) {
945
- const { checking, checkAll, targets } = props.face;
946
- const disabled = checking || targets.length === 0;
947
- return react.createElement("button", {
1097
+ const { checking, updatingAll, checkAll, updateAll, targets, rows } = props.face;
1098
+ const busy = hasInstallingRow(rows) || updatingAll;
1099
+ const checkDisabled = checking || updatingAll || busy || targets.length === 0;
1100
+ const allDisabled = checking || updatingAll || busy || targets.length === 0;
1101
+ const busyText = reasonText("update-busy");
1102
+ return react.createElement("span", { style: PANEL_STYLE.actions }, react.createElement("button", {
948
1103
  type: "button",
949
- style: disabled ? {
1104
+ style: checkDisabled ? {
950
1105
  ...PANEL_STYLE.btn,
951
1106
  opacity: .55,
952
1107
  cursor: "default"
953
1108
  } : PANEL_STYLE.btn,
954
- disabled,
1109
+ disabled: checkDisabled,
955
1110
  onClick: checkAll,
956
- title: "检查总管与六家插件的版本"
957
- }, checking ? "检查中…" : "检查更新");
1111
+ title: busy ? busyText : "检查总管与六家插件的版本"
1112
+ }, checking ? "检查中…" : "检查更新"), react.createElement("button", {
1113
+ type: "button",
1114
+ style: allDisabled ? {
1115
+ ...PANEL_STYLE.btn,
1116
+ opacity: .55,
1117
+ cursor: "default"
1118
+ } : PANEL_STYLE.btn,
1119
+ disabled: allDisabled,
1120
+ onClick: updateAll,
1121
+ title: busy ? busyText : "按顺序逐家装上更新(一家收尾才起下一家)"
1122
+ }, updatingAll ? "更新中…" : "全部更新"));
958
1123
  }
959
1124
  /** 待重启横幅:一行总账,只点名哪几家要重启(理由与做法写在各家卡片上,见 `restartBannerText`)。 */
960
1125
  function RestartBanners(props) {
@@ -977,7 +1142,7 @@ window.__ModuleLoader__.load({
977
1142
  * 手工命令只在「装不了」或「装失败」的行显示:正常用户用不到,摊在每行上既吵又误导。
978
1143
  */
979
1144
  function UpdateResults(props) {
980
- const { targets, rows, loadError, checking, open, close } = props.face;
1145
+ const { targets, rows, loadError, checking, updatingAll, open, close } = props.face;
981
1146
  const dialogRef = react.useRef(null);
982
1147
  react.useEffect(() => {
983
1148
  if (open) dialogRef.current?.focus();
@@ -986,6 +1151,7 @@ window.__ModuleLoader__.load({
986
1151
  if (loadError) return react.createElement("div", { style: PANEL_STYLE.reason }, "更新能力没接上:" + loadError);
987
1152
  if (targets.length === 0) return null;
988
1153
  if (shown.length === 0 && !open) return null;
1154
+ const locked = hasInstallingRow(rows) || updatingAll;
989
1155
  const body = shown.map((target) => {
990
1156
  const row = rows[target.key];
991
1157
  const snapshot = row.outcome?.snapshot ?? null;
@@ -1026,12 +1192,13 @@ window.__ModuleLoader__.load({
1026
1192
  color: "var(--dsw-alias-label-secondary, #9a9a9a)"
1027
1193
  } }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), buttonAction ? react.createElement("button", {
1028
1194
  type: "button",
1029
- style: busy ? {
1195
+ style: locked ? {
1030
1196
  ...PANEL_STYLE.btnPrimary,
1031
1197
  opacity: .55,
1032
1198
  cursor: "default"
1033
1199
  } : PANEL_STYLE.btnPrimary,
1034
- disabled: busy,
1200
+ disabled: locked,
1201
+ title: locked ? reasonText("update-busy") : void 0,
1035
1202
  onClick: () => {
1036
1203
  props.face.act(target, buttonAction);
1037
1204
  }
@@ -1078,6 +1245,7 @@ window.__ModuleLoader__.load({
1078
1245
  const target = props.target;
1079
1246
  const row = target ? props.face.rows[target.key] : null;
1080
1247
  const busy = row?.phase === "installing";
1248
+ const locked = busy || hasInstallingRow(props.face.rows) || props.face.updatingAll;
1081
1249
  const manual = (target ? manualForDisplay(target, row?.failure?.manual ?? row?.outcome?.manual ?? null) : null) ?? props.fallbackCommand;
1082
1250
  const installed = target ? target.installedVersion ?? target.runningVersion : null;
1083
1251
  const state = slotStateOf(target, props.inLedger);
@@ -1097,7 +1265,7 @@ window.__ModuleLoader__.load({
1097
1265
  }
1098
1266
  return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
1099
1267
  type: "button",
1100
- style: busy ? {
1268
+ style: locked ? {
1101
1269
  ...PANEL_STYLE.btn,
1102
1270
  marginTop: 8,
1103
1271
  opacity: .55
@@ -1105,13 +1273,14 @@ window.__ModuleLoader__.load({
1105
1273
  ...PANEL_STYLE.btn,
1106
1274
  marginTop: 8
1107
1275
  },
1108
- disabled: busy,
1276
+ disabled: locked,
1277
+ title: locked ? reasonText("update-busy") : void 0,
1109
1278
  onClick: () => {
1110
1279
  if (target) props.face.act(target, "install");
1111
1280
  }
1112
1281
  }, busy ? "正在装,请稍候…" : "装上") : null, retryAction === "recheck" && target ? react.createElement("button", {
1113
1282
  type: "button",
1114
- style: busy ? {
1283
+ style: locked ? {
1115
1284
  ...PANEL_STYLE.btn,
1116
1285
  marginTop: 8,
1117
1286
  marginLeft: 8,
@@ -1121,7 +1290,8 @@ window.__ModuleLoader__.load({
1121
1290
  marginTop: 8,
1122
1291
  marginLeft: 8
1123
1292
  },
1124
- disabled: busy,
1293
+ disabled: locked,
1294
+ title: locked ? reasonText("update-busy") : void 0,
1125
1295
  onClick: () => props.face.act(target, "recheck")
1126
1296
  }, ACTION_LABEL.recheck) : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
1127
1297
  }
@@ -2759,6 +2929,27 @@ window.__ModuleLoader__.load({
2759
2929
  return /* @__PURE__ */ new Set([...previous, active]);
2760
2930
  });
2761
2931
  }, [active]);
2932
+ /** 预热:section 一挂载,就把其余几家**错峰**挂上(隐藏面板照常挂载、只切 `hidden`),
2933
+ * 让各家那一次配置读提前跑掉——用户点哪个页签,多半值已经在了。
2934
+ * 错峰(每 150ms 一家)是为了不让六家子进程同时抢开工:首帧那一口气优先留给框架。
2935
+ * 只在页签账本非空时起一次(`rows` 要等各家 slot 注入后才齐)。
2936
+ *
2937
+ * **一次性**(p10 定时器门的例外依据):每个页签只挂一次(`warmed` 闸门 + 一批固定数量的定时器),
2938
+ * 不是轮询、也不是重试——它没有「次数」可言;卸载即 `clearTimeout` 清干净。
2939
+ * 不做轮询是本意:这里只补一次提前量,取数与刷新仍旧由各家用例自己的通道走。 */
2940
+ const warmed = react.useRef(false);
2941
+ const tabCount = rows.length;
2942
+ react.useEffect(() => {
2943
+ if (warmed.current || tabCount === 0) return;
2944
+ warmed.current = true;
2945
+ const STAGGER_MS = 150;
2946
+ const timers = MANAGER_TABS.filter((tab) => rows.some((row) => row.id === tab.plugin)).map((tab, index) => setTimeout(() => {
2947
+ setVisitedIds((previous) => previous.has(tab.plugin) ? previous : /* @__PURE__ */ new Set([...previous, tab.plugin]));
2948
+ }, STAGGER_MS * (index + 1)));
2949
+ return () => {
2950
+ for (const timer of timers) clearTimeout(timer);
2951
+ };
2952
+ }, [tabCount]);
2762
2953
  function labelFor(tab) {
2763
2954
  const row = rows.find((r) => r.id === tab.plugin);
2764
2955
  return row && row.label.length > 0 ? row.label : tab.title;