@1e0zj/dsh-plugin-mall 0.4.15 → 0.4.17
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/package.json +1 -1
- package/src/client.js +171 -30
- package/src/index.js +64 -0
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -128,10 +128,73 @@ window.__ModuleLoader__.load({
|
|
|
128
128
|
return String(e && e.message ? e.message : e);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// ── 更新按钮的防连点判据 ────────────────────────────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// 纯函数,不碰 React;index.js 的 --self-test 把这一整段从本文件里原样抠
|
|
134
|
+
// 出来跑(同下面 session nonce 那段的做法),所以改坏了这里测试会红。
|
|
135
|
+
// 往段里加函数会自动被测到,把函数搬出去则当场 ReferenceError。
|
|
136
|
+
|
|
131
137
|
function isJobActive(status) {
|
|
132
138
|
return status === "running" || status === "stopping";
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
// spec → npm 包名。**按包名、不按 spec** 是这段的关键:同一条更新链在不同
|
|
142
|
+
// 阶段的 spec 并不一样(点的是 foo@1.2.3,服务端防抢注解析后 track 存下的
|
|
143
|
+
// 可能是别的形态),按 spec 盯就会在解析之后丢失目标。
|
|
144
|
+
// 与服务端 npmNameOf(github.js)同一套形状;非 npm 形态返回 null。
|
|
145
|
+
function specPackageName(spec) {
|
|
146
|
+
if (typeof spec !== "string" || spec.length === 0) return null;
|
|
147
|
+
if (spec.charAt(0) === "@") {
|
|
148
|
+
var scoped = /^(@[^/@\s]+\/[^/@\s]+?)(?:@[^/\s]+)?$/.exec(spec);
|
|
149
|
+
return scoped === null ? null : scoped[1];
|
|
150
|
+
}
|
|
151
|
+
var bare = /^([^@/\s:]+)(?:@[^/\s]+)?$/.exec(spec);
|
|
152
|
+
return bare === null ? null : bare[1];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 这个包上有没有一条没了结的更新链。三段接力,中间不能断:
|
|
156
|
+
//
|
|
157
|
+
// updating 点击 → 预检 job 建立。这段 jobs 里还什么都没有,只有同步
|
|
158
|
+
// 的 ref 挡得住同一拍里的第二次点击。
|
|
159
|
+
// installing 预检落终态 → 安装 job 建立。预检那条已经不 active 了,
|
|
160
|
+
// 安装那条还没 track 上,中间这一拍靠它顶住。
|
|
161
|
+
// jobs job 全程。**RPC 返回只意味着 job 建好了,pnpm 还没开始跑**
|
|
162
|
+
// ——锁必须持续到 job 落终态,不是到 RPC 返回。
|
|
163
|
+
// needsApproval 的 status 是 failed,但那是等用户决定的暂停,
|
|
164
|
+
// 事务没结束,算未了结。
|
|
165
|
+
// cards 预检出了风险卡片,停在那里等用户读完再决定。这期间任务是
|
|
166
|
+
// 终态,但决定还没做,再起一条链就是两条链抢同一个事务。
|
|
167
|
+
// **按 jobId 存字典、不是单槽位**:同时可以停着好几张卡片
|
|
168
|
+
// (批量更新必然如此,手动连点两个包也够)。单槽位时后来的
|
|
169
|
+
// 会顶掉前一张,被顶掉那条从此没有任何入口能做决定,job 却
|
|
170
|
+
// 一直是未了结状态——那一行的更新按钮再也点不亮。
|
|
171
|
+
//
|
|
172
|
+
// sources 里递进来的必须是 ref.current 这类当下的值:判定发生在点击那一
|
|
173
|
+
// 刻,上一次渲染的快照没有意义。
|
|
174
|
+
function updateChainBusy(sources, name) {
|
|
175
|
+
if (name === null || name === undefined) return false;
|
|
176
|
+
if ((sources.updating || {})[name] === true) return true;
|
|
177
|
+
var installing = sources.installing || {};
|
|
178
|
+
for (var spec in installing) {
|
|
179
|
+
if (installing[spec] === true && specPackageName(spec) === name) return true;
|
|
180
|
+
}
|
|
181
|
+
var cards = sources.cards || {};
|
|
182
|
+
for (var cardId in cards) {
|
|
183
|
+
var card = cards[cardId];
|
|
184
|
+
if (card && specPackageName(card.spec) === name) return true;
|
|
185
|
+
}
|
|
186
|
+
var jobs = sources.jobs || {};
|
|
187
|
+
for (var id in jobs) {
|
|
188
|
+
var job = jobs[id];
|
|
189
|
+
if (!job || typeof job.spec !== "string") continue;
|
|
190
|
+
if (specPackageName(job.spec) !== name) continue;
|
|
191
|
+
if (isJobActive(job.status)) return true;
|
|
192
|
+
if (Array.isArray(job.needsApproval) && job.needsApproval.length > 0) return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
// ── 更新按钮的防连点判据结束 ────────────────────────────────────────────
|
|
197
|
+
|
|
135
198
|
function kindLabel(kind) {
|
|
136
199
|
if (kind === "bundle") return "宿主插件层";
|
|
137
200
|
if (kind === "client") return "浏览器UI插件";
|
|
@@ -344,7 +407,8 @@ window.__ModuleLoader__.load({
|
|
|
344
407
|
}
|
|
345
408
|
commit(next);
|
|
346
409
|
}, [commit]);
|
|
347
|
-
|
|
410
|
+
// jobsRef 一并交出去:防连点的判定读的是当下的值,不是渲染快照。
|
|
411
|
+
return { jobs: jobs, jobsRef: jobsRef, track: track, drop: drop, setStatus: setStatus, restore: restore };
|
|
348
412
|
}
|
|
349
413
|
|
|
350
414
|
// ── plugin verification badge ───────────────────────────────────────────
|
|
@@ -607,7 +671,7 @@ window.__ModuleLoader__.load({
|
|
|
607
671
|
if (ids.length === 0 && !props.error) return null;
|
|
608
672
|
var hasSettled = ids.some(function (id) {
|
|
609
673
|
var job = props.jobs[id];
|
|
610
|
-
var hasDecision =
|
|
674
|
+
var hasDecision = props.preflights[id] !== undefined
|
|
611
675
|
|| (job.needsApproval && job.needsApproval.length > 0);
|
|
612
676
|
return !isJobActive(job.status) && !hasDecision;
|
|
613
677
|
});
|
|
@@ -620,7 +684,7 @@ window.__ModuleLoader__.load({
|
|
|
620
684
|
ids.map(function (id) {
|
|
621
685
|
var job = props.jobs[id];
|
|
622
686
|
var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
|
|
623
|
-
var preflightDecision = props.
|
|
687
|
+
var preflightDecision = props.preflights[id] || null;
|
|
624
688
|
var awaitingDecision = preflightDecision !== null || (job.needsApproval && job.needsApproval.length > 0);
|
|
625
689
|
return h("div", { key: id, className: "mkt_job" },
|
|
626
690
|
// 不再显示 market-N 这种内部 id——对用户没有意义,反而让人以为
|
|
@@ -640,12 +704,12 @@ window.__ModuleLoader__.load({
|
|
|
640
704
|
? h(ApprovalRequest, {
|
|
641
705
|
spec: job.spec,
|
|
642
706
|
needsApproval: job.needsApproval,
|
|
643
|
-
busy: props.
|
|
707
|
+
busy: props.specBusy(job.spec),
|
|
644
708
|
onApprove: function (names) {
|
|
645
709
|
// 不先 drop:旧条目由重试任务的 track(carryFromId) 原子接管
|
|
646
710
|
// (撤条目 + 日志接续一拍完成)。install RPC 现在只做本地校验、
|
|
647
711
|
// 立刻返回新 job id,但保留这个次序仍然是它最稳的形态——
|
|
648
|
-
// 万一 RPC 失败,旧条目和日志还在。等待期间按钮由
|
|
712
|
+
// 万一 RPC 失败,旧条目和日志还在。等待期间按钮由 specBusy
|
|
649
713
|
// 态显示「继续中…」。
|
|
650
714
|
props.onApprove(job.spec, names, job.approvalToken, id);
|
|
651
715
|
},
|
|
@@ -655,9 +719,9 @@ window.__ModuleLoader__.load({
|
|
|
655
719
|
? h(PreflightCard, {
|
|
656
720
|
spec: preflightDecision.spec,
|
|
657
721
|
report: preflightDecision.report,
|
|
658
|
-
busy: props.
|
|
659
|
-
onConfirm: props.onConfirmPreflight,
|
|
660
|
-
onClose: props.onClosePreflight,
|
|
722
|
+
busy: props.specBusy(preflightDecision.spec),
|
|
723
|
+
onConfirm: function () { props.onConfirmPreflight(id); },
|
|
724
|
+
onClose: function () { props.onClosePreflight(id); },
|
|
661
725
|
})
|
|
662
726
|
: job.detail ? h("div", { className: job.status === "failed" ? "mkt_error" : "mkt_desc" }, job.detail) : null,
|
|
663
727
|
// 预检完成不提示重启——它没有改动任何东西,接下来才是安装。
|
|
@@ -715,6 +779,7 @@ window.__ModuleLoader__.load({
|
|
|
715
779
|
var togglable = entry !== undefined && dep.name !== MARKET_PACKAGE;
|
|
716
780
|
var enabled = entry === undefined ? true : entry.enabled !== false;
|
|
717
781
|
var toggling = (props.toggling || {})[dep.name] === true;
|
|
782
|
+
var updating = props.isUpdating(dep.name);
|
|
718
783
|
return h("div", { key: dep.name, className: "mkt_depRow" },
|
|
719
784
|
h("span", { className: "mkt_desc" + (enabled ? "" : " mkt_depOff") }, dep.name + "@" + dep.version),
|
|
720
785
|
h("span", { className: "mkt_depActions" },
|
|
@@ -728,8 +793,11 @@ window.__ModuleLoader__.load({
|
|
|
728
793
|
}, h("span", { className: "mkt_switchKnob" })) : null,
|
|
729
794
|
upd && upd.hasUpdate ? h("button", {
|
|
730
795
|
className: "mkt_btn mkt_btnSm",
|
|
731
|
-
|
|
732
|
-
|
|
796
|
+
// 和 doUpdate 里的守卫读同一个判据,两边不会漂移——不一致
|
|
797
|
+
// 就会出现「按钮看着能点,点了什么也不发生」。
|
|
798
|
+
disabled: updating,
|
|
799
|
+
onClick: function () { props.onUpdate(dep.name, dep.name + "@" + upd.latest); },
|
|
800
|
+
}, updating ? "更新中…" : "更新至 " + upd.latest) : null,
|
|
733
801
|
h("button", {
|
|
734
802
|
className: "mkt_btn mkt_btnDanger mkt_btnSm",
|
|
735
803
|
disabled: busy,
|
|
@@ -812,9 +880,32 @@ window.__ModuleLoader__.load({
|
|
|
812
880
|
var _hostStartedAt = useState(0);
|
|
813
881
|
var hostStartedAt = _hostStartedAt[0];
|
|
814
882
|
var setHostStartedAt = _hostStartedAt[1];
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
var
|
|
883
|
+
// jobId -> { spec, report, jobId }。字典而不是单个:见 updateChainBusy
|
|
884
|
+
// 的 cards 一节——同时可以有好几张待决策的风险卡片。
|
|
885
|
+
var _preflights = useState({});
|
|
886
|
+
var preflights = _preflights[0];
|
|
887
|
+
var setPreflights = _preflights[1];
|
|
888
|
+
var dropPreflight = useCallback(function (jobId) {
|
|
889
|
+
setPreflights(function (prev) {
|
|
890
|
+
if (!(jobId in prev)) return prev;
|
|
891
|
+
var next = Object.assign({}, prev);
|
|
892
|
+
delete next[jobId];
|
|
893
|
+
return next;
|
|
894
|
+
});
|
|
895
|
+
}, []);
|
|
896
|
+
|
|
897
|
+
// 更新按钮防连点用的三份当下值(见 updateChainBusy)。ref 而不是 state:
|
|
898
|
+
// 判定发生在点击那一刻,而 setState 到重渲染之间还有一拍能点。ref 不触发
|
|
899
|
+
// 渲染,所以每次改动配一次 bumpBusy,让按钮当场变样。
|
|
900
|
+
var updatingRef = useRef({}); // 包名 -> true,点击到预检 job 建立之间
|
|
901
|
+
var installingRef = useRef({}); // spec -> true,安装 RPC 在途
|
|
902
|
+
var preflightsRef = useRef({});
|
|
903
|
+
preflightsRef.current = preflights; // 渲染期同步,同 resultsRef 的写法
|
|
904
|
+
var _busyTick = useState(0);
|
|
905
|
+
var setBusyTick = _busyTick[1];
|
|
906
|
+
var bumpBusy = useCallback(function () {
|
|
907
|
+
setBusyTick(function (n) { return n + 1; });
|
|
908
|
+
}, []);
|
|
818
909
|
|
|
819
910
|
// Opaque one-shot approval tokens issued from needsApproval outcomes (spec -> token).
|
|
820
911
|
// Cleared on success, unrelated failure, cancel, modal close, or spec change.
|
|
@@ -983,6 +1074,17 @@ window.__ModuleLoader__.load({
|
|
|
983
1074
|
var polling = useJobPolling(call, onJobSettled, onApprovalToken, onPreflightSettled);
|
|
984
1075
|
var track = polling.track;
|
|
985
1076
|
var jobs = polling.jobs;
|
|
1077
|
+
var jobsRef = polling.jobsRef;
|
|
1078
|
+
|
|
1079
|
+
// 按钮的 disabled 和 handler 里的守卫都读这一个函数,两边不会漂移。
|
|
1080
|
+
var updateBusy = useCallback(function (packageName) {
|
|
1081
|
+
return updateChainBusy({
|
|
1082
|
+
updating: updatingRef.current,
|
|
1083
|
+
installing: installingRef.current,
|
|
1084
|
+
cards: preflightsRef.current,
|
|
1085
|
+
jobs: jobsRef.current,
|
|
1086
|
+
}, packageName);
|
|
1087
|
+
}, [jobsRef]);
|
|
986
1088
|
var dropJob = polling.drop;
|
|
987
1089
|
var setJobStatus = polling.setStatus;
|
|
988
1090
|
|
|
@@ -1000,6 +1102,10 @@ window.__ModuleLoader__.load({
|
|
|
1000
1102
|
}, []);
|
|
1001
1103
|
|
|
1002
1104
|
var doRawInstall = useCallback(function (spec, extra, carryFromId) {
|
|
1105
|
+
// ref 与 state 同时置位:state 负责触发渲染,ref 才是判定读的那一份
|
|
1106
|
+
// (见 updateChainBusy)。这一段是预检 job 已落终态、安装 job 还没
|
|
1107
|
+
// track 上的空档,中间断一拍更新按钮就会复活。
|
|
1108
|
+
installingRef.current[spec] = true;
|
|
1003
1109
|
setInstalling(function (prev) {
|
|
1004
1110
|
var next = Object.assign({}, prev, { [spec]: true });
|
|
1005
1111
|
return next;
|
|
@@ -1012,6 +1118,7 @@ window.__ModuleLoader__.load({
|
|
|
1012
1118
|
delete approvalTokensRef.current[spec];
|
|
1013
1119
|
reportError("tasks", e);
|
|
1014
1120
|
}).finally(function () {
|
|
1121
|
+
delete installingRef.current[spec];
|
|
1015
1122
|
setInstalling(function (prev) {
|
|
1016
1123
|
var next = Object.assign({}, prev);
|
|
1017
1124
|
delete next[spec];
|
|
@@ -1041,20 +1148,29 @@ window.__ModuleLoader__.load({
|
|
|
1041
1148
|
// 安全:接过预检的日志、撤掉预检条目,面板上只剩这一个任务。
|
|
1042
1149
|
doRawInstall(spec, {}, jobId);
|
|
1043
1150
|
} else {
|
|
1044
|
-
|
|
1151
|
+
// 按 jobId 落格,不覆盖别的包停着的卡片。
|
|
1152
|
+
setPreflights(function (prev) {
|
|
1153
|
+
var next = Object.assign({}, prev);
|
|
1154
|
+
next[jobId] = { spec: spec, report: report, jobId: jobId };
|
|
1155
|
+
return next;
|
|
1156
|
+
});
|
|
1045
1157
|
}
|
|
1046
1158
|
};
|
|
1047
1159
|
}, [doRawInstall, reportError]);
|
|
1048
1160
|
|
|
1049
1161
|
// 点安装 = 立刻起一个预检 job,面板马上有东西看、日志实时流。
|
|
1050
1162
|
// 结论由轮询经 onPreflightSettled 交回上面的 handler,这里不等待。
|
|
1051
|
-
|
|
1163
|
+
// onStarted:RPC 落定(不论成败)时回调一次。更新按钮用它把「点击 →
|
|
1164
|
+
// job 建立」这一段的锁交给 job,市场卡片不传,行为一字未变。
|
|
1165
|
+
var preflightAndInstall = useCallback(function (spec, onStarted) {
|
|
1052
1166
|
delete approvalTokensRef.current[spec];
|
|
1053
1167
|
setError(null);
|
|
1054
1168
|
call("preflight", { spec: spec }).then(function (value) {
|
|
1055
1169
|
track(value.jobId, value.spec || spec);
|
|
1056
1170
|
}).catch(function (e) {
|
|
1057
1171
|
reportError("tasks", e);
|
|
1172
|
+
}).finally(function () {
|
|
1173
|
+
if (typeof onStarted === "function") onStarted();
|
|
1058
1174
|
});
|
|
1059
1175
|
}, [call, reportError, track]);
|
|
1060
1176
|
|
|
@@ -1062,6 +1178,26 @@ window.__ModuleLoader__.load({
|
|
|
1062
1178
|
preflightAndInstall("github:" + repo);
|
|
1063
1179
|
}, [preflightAndInstall]);
|
|
1064
1180
|
|
|
1181
|
+
// 已装列表的「更新至 X」。此前它是那一行里唯一没有 disabled 的按钮,
|
|
1182
|
+
// 而起链要走联网的 registry 解析,那几秒里 jobs 还是空的、按钮还是可点
|
|
1183
|
+
// 的——连点几下就是几条预检+安装链,profile 事务队列会把它们排队串行
|
|
1184
|
+
// 跑完,同一个版本被反复装好几遍。
|
|
1185
|
+
//
|
|
1186
|
+
// 守卫按包名(updateChainBusy 的注释说明了为什么不能按 spec),同步的
|
|
1187
|
+
// ref 挡住同一拍里的第二次点击;RPC 返回只解开 updating 那一段,之后由
|
|
1188
|
+
// 对应的 job 接管,锁一直持续到 job 落终态。
|
|
1189
|
+
var doUpdate = useCallback(function (packageName, spec) {
|
|
1190
|
+
if (updateBusy(packageName)) return;
|
|
1191
|
+
updatingRef.current[packageName] = true;
|
|
1192
|
+
bumpBusy();
|
|
1193
|
+
preflightAndInstall(spec, function () {
|
|
1194
|
+
// preflight RPC 已返回:jobs 里已经有条目接手,这里才允许松手。
|
|
1195
|
+
// 顺序反过来会闪出一拍「谁都不认为它在忙」的可点窗口。
|
|
1196
|
+
delete updatingRef.current[packageName];
|
|
1197
|
+
bumpBusy();
|
|
1198
|
+
});
|
|
1199
|
+
}, [preflightAndInstall, updateBusy, bumpBusy]);
|
|
1200
|
+
|
|
1065
1201
|
var doRestart = useCallback(function () {
|
|
1066
1202
|
clearRestartPing();
|
|
1067
1203
|
setRestarting(true);
|
|
@@ -1185,18 +1321,22 @@ window.__ModuleLoader__.load({
|
|
|
1185
1321
|
toggling: toggling,
|
|
1186
1322
|
onToggle: doToggle,
|
|
1187
1323
|
onUninstall: doUninstall,
|
|
1188
|
-
|
|
1324
|
+
onUpdate: doUpdate,
|
|
1325
|
+
isUpdating: updateBusy,
|
|
1189
1326
|
error: error && error.scope === "installed" ? error.message : null,
|
|
1190
1327
|
}),
|
|
1191
1328
|
h(JobsPanel, {
|
|
1192
1329
|
jobs: jobs,
|
|
1193
1330
|
error: error && error.scope === "tasks" ? error.message : null,
|
|
1194
|
-
|
|
1195
|
-
|
|
1331
|
+
preflights: preflights,
|
|
1332
|
+
// 这张卡片/这条批准请求对应的 install RPC 在不在途。原来是「取
|
|
1333
|
+
// installing 里的第一个 spec」的单值,同时两条链在途时只有一条显示
|
|
1334
|
+
// 得对;按 spec 问就没有这个假设了。
|
|
1335
|
+
specBusy: function (spec) { return installing[spec] === true; },
|
|
1196
1336
|
onClear: function () {
|
|
1197
1337
|
Object.keys(jobs).forEach(function (id) {
|
|
1198
1338
|
var job = jobs[id];
|
|
1199
|
-
var hasDecision = job && (
|
|
1339
|
+
var hasDecision = job && (preflights[id] !== undefined
|
|
1200
1340
|
|| (job.needsApproval && job.needsApproval.length > 0));
|
|
1201
1341
|
if (!job || isJobActive(job.status) || hasDecision) return;
|
|
1202
1342
|
var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
|
|
@@ -1215,33 +1355,34 @@ window.__ModuleLoader__.load({
|
|
|
1215
1355
|
});
|
|
1216
1356
|
},
|
|
1217
1357
|
onApprove: doApprove,
|
|
1218
|
-
onConfirmPreflight: function () {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
var report = preflight.report;
|
|
1223
|
-
setPreflight(null);
|
|
1358
|
+
onConfirmPreflight: function (jobId) {
|
|
1359
|
+
var decision = preflights[jobId];
|
|
1360
|
+
if (!decision) return;
|
|
1361
|
+
dropPreflight(jobId);
|
|
1224
1362
|
// consentDigest 来自预检 job 的 extras,装的时候与当前报告比对:
|
|
1225
1363
|
// 用户点「继续」到安装真正开跑之间候选包或 profile 变了,
|
|
1226
1364
|
// 布尔同意不得沿用,要重新看新的警告。
|
|
1227
|
-
doRawInstall(spec, { acceptWarnings: true, acceptedReportDigest: report.consentDigest },
|
|
1365
|
+
doRawInstall(decision.spec, { acceptWarnings: true, acceptedReportDigest: decision.report.consentDigest }, jobId);
|
|
1228
1366
|
},
|
|
1229
|
-
onClosePreflight: function () {
|
|
1230
|
-
|
|
1231
|
-
|
|
1367
|
+
onClosePreflight: function (jobId) {
|
|
1368
|
+
var decision = preflights[jobId];
|
|
1369
|
+
if (decision && decision.spec) delete approvalTokensRef.current[decision.spec];
|
|
1370
|
+
dropPreflight(jobId);
|
|
1232
1371
|
},
|
|
1233
1372
|
onDismiss: function (id) {
|
|
1234
1373
|
var job = jobs[id];
|
|
1235
1374
|
var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
|
|
1236
1375
|
if (job && job.spec) delete approvalTokensRef.current[job.spec];
|
|
1237
1376
|
call("jobDismiss", { jobId: id, token: token }).catch(function () {});
|
|
1377
|
+
// job 条目撤了,挂在它上面的卡片没有宿主了。不一并撤的话
|
|
1378
|
+
// updateChainBusy 的 cards 分支会一直锁着那个包名。
|
|
1379
|
+
dropPreflight(id);
|
|
1238
1380
|
dropJob(id);
|
|
1239
1381
|
},
|
|
1240
1382
|
onDrop: dropJob,
|
|
1241
1383
|
onRestart: function () { setRestartConfirm(true); },
|
|
1242
1384
|
restarting: restarting,
|
|
1243
1385
|
hostStartedAt: hostStartedAt,
|
|
1244
|
-
approving: Object.keys(installing).filter(function (s) { return installing[s]; })[0],
|
|
1245
1386
|
}),
|
|
1246
1387
|
h("div", { className: "mkt_list" },
|
|
1247
1388
|
results == null
|
package/src/index.js
CHANGED
|
@@ -3309,6 +3309,70 @@ export async function runSelfTests() {
|
|
|
3309
3309
|
storageTouches === 0, `touches=${storageTouches}`);
|
|
3310
3310
|
}
|
|
3311
3311
|
|
|
3312
|
+
// 已装列表「更新至 X」的防连点判据。同样从 src/client.js 原样抠出来跑
|
|
3313
|
+
// ——它在浏览器里是 React 闭包,测不到;提成纯函数就是为了能在这里钉住。
|
|
3314
|
+
//
|
|
3315
|
+
// 钉住的两件事,各对应一个真实的洞:
|
|
3316
|
+
// 1. 按包名判定 —— 按 spec 判定会在服务端解析之后丢失目标;
|
|
3317
|
+
// 2. 锁到 job 终态 —— 只锁 RPC 往返的话,jobId 一返回按钮就复活,而那
|
|
3318
|
+
// 时 pnpm 还一步都没跑。
|
|
3319
|
+
{
|
|
3320
|
+
const clientSource = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "client.js"), "utf8");
|
|
3321
|
+
const begin = clientSource.indexOf("// ── 更新按钮的防连点判据 ─");
|
|
3322
|
+
const end = clientSource.indexOf("// ── 更新按钮的防连点判据结束 ─");
|
|
3323
|
+
if (begin === -1 || end === -1 || end < begin) throw new Error("client.js 里找不到「更新按钮的防连点判据」整段");
|
|
3324
|
+
// 整段抠,不是逐个函数挑:依赖(isJobActive、specPackageName)就在段内,
|
|
3325
|
+
// 往段里加函数会自动被测到,搬出段外则当场 ReferenceError。
|
|
3326
|
+
const front = new Function([
|
|
3327
|
+
clientSource.slice(begin, end),
|
|
3328
|
+
"return { specPackageName, updateChainBusy };",
|
|
3329
|
+
].join("\n"))();
|
|
3330
|
+
|
|
3331
|
+
const busy = (patch, name) => front.updateChainBusy(
|
|
3332
|
+
Object.assign({ updating: {}, installing: {}, cards: {}, jobs: {} }, patch), name);
|
|
3333
|
+
const running = (spec) => ({ spec, status: "running" });
|
|
3334
|
+
|
|
3335
|
+
check("更新防连点:裸包名折算", front.specPackageName("foo") === "foo");
|
|
3336
|
+
check("更新防连点:带版本折算", front.specPackageName("foo@1.2.3") === "foo");
|
|
3337
|
+
check("更新防连点:scoped 折算", front.specPackageName("@a/b@9.9.9") === "@a/b");
|
|
3338
|
+
check("更新防连点:非 npm 形态返回 null", front.specPackageName("github:o/r") === null);
|
|
3339
|
+
|
|
3340
|
+
check("更新防连点:点击到 job 建立之间靠同步 ref 挡住(第二次点击)",
|
|
3341
|
+
busy({ updating: { foo: true } }, "foo") === true);
|
|
3342
|
+
check("更新防连点:预检 job 在跑时锁着",
|
|
3343
|
+
busy({ jobs: { a: running("foo@1.2.3") } }, "foo") === true);
|
|
3344
|
+
check("更新防连点:预检落终态、安装 RPC 在途的空档也锁着",
|
|
3345
|
+
busy({ installing: { "foo@1.2.3": true }, jobs: { a: { spec: "foo@1.2.3", status: "completed" } } }, "foo") === true);
|
|
3346
|
+
check("更新防连点:安装 job 在跑时锁着(RPC 早返回了,pnpm 才刚开始)",
|
|
3347
|
+
busy({ jobs: { a: running("foo@1.2.3") } }, "foo") === true);
|
|
3348
|
+
check("更新防连点:stopping 也算在跑",
|
|
3349
|
+
busy({ jobs: { a: { spec: "foo@1.2.3", status: "stopping" } } }, "foo") === true);
|
|
3350
|
+
check("更新防连点:停在批准卡片上(failed + needsApproval)算未了结",
|
|
3351
|
+
busy({ jobs: { a: { spec: "foo@1.2.3", status: "failed", needsApproval: [{ name: "x" }] } } }, "foo") === true);
|
|
3352
|
+
check("更新防连点:停在风险卡片上时锁着(决定还没做,再起一条就是两条链)",
|
|
3353
|
+
busy({ cards: { j1: { spec: "foo@1.2.3" } } }, "foo") === true);
|
|
3354
|
+
// 单槽位时代的真实卡死:第二张卡片顶掉第一张,被顶掉那条再没有入口能
|
|
3355
|
+
// 做决定,job 却一直未了结——那一行的更新按钮永远点不亮。
|
|
3356
|
+
check("更新防连点:多张卡片同时停着,各锁各的包(批量更新的必然形状)",
|
|
3357
|
+
busy({ cards: { j1: { spec: "foo@1.2.3" }, j2: { spec: "bar@2.0.0" } } }, "foo") === true
|
|
3358
|
+
&& busy({ cards: { j1: { spec: "foo@1.2.3" }, j2: { spec: "bar@2.0.0" } } }, "bar") === true);
|
|
3359
|
+
check("更新防连点:撤掉其中一张不影响另一张的锁",
|
|
3360
|
+
busy({ cards: { j2: { spec: "bar@2.0.0" } } }, "foo") === false
|
|
3361
|
+
&& busy({ cards: { j2: { spec: "bar@2.0.0" } } }, "bar") === true);
|
|
3362
|
+
|
|
3363
|
+
check("更新防连点:job 落终态后解锁,用户可以重试",
|
|
3364
|
+
busy({ jobs: { a: { spec: "foo@1.2.3", status: "failed" } } }, "foo") === false);
|
|
3365
|
+
check("更新防连点:completed 后解锁",
|
|
3366
|
+
busy({ jobs: { a: { spec: "foo@1.2.3", status: "completed" } } }, "foo") === false);
|
|
3367
|
+
check("更新防连点:别的包在跑不锁本行",
|
|
3368
|
+
busy({ jobs: { a: running("bar@1.0.0") } }, "foo") === false);
|
|
3369
|
+
check("更新防连点:风险卡片停在别的包上不锁本行",
|
|
3370
|
+
busy({ cards: { j1: { spec: "bar@1.0.0" } } }, "foo") === false);
|
|
3371
|
+
check("更新防连点:什么都没在途 → 不锁", busy({}, "foo") === false);
|
|
3372
|
+
check("更新防连点:包名为 null 时不误判",
|
|
3373
|
+
busy({ jobs: { a: running("foo@1.2.3") } }, null) === false);
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3312
3376
|
let cancelRefused = false;
|
|
3313
3377
|
try {
|
|
3314
3378
|
sessionTracker.cancel(sessionJobId, "session-beta");
|