@1e0zj/dsh-plugin-mall 0.4.15 → 0.4.16
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 +120 -5
- package/src/index.js +56 -0
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -128,10 +128,66 @@ 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
|
+
// card 预检出了风险卡片,停在那里等用户读完再决定。这期间任务是
|
|
166
|
+
// 终态,但决定还没做,再起一条链就是两条链抢同一个事务。
|
|
167
|
+
//
|
|
168
|
+
// sources 里递进来的必须是 ref.current 这类当下的值:判定发生在点击那一
|
|
169
|
+
// 刻,上一次渲染的快照没有意义。
|
|
170
|
+
function updateChainBusy(sources, name) {
|
|
171
|
+
if (name === null || name === undefined) return false;
|
|
172
|
+
if ((sources.updating || {})[name] === true) return true;
|
|
173
|
+
var installing = sources.installing || {};
|
|
174
|
+
for (var spec in installing) {
|
|
175
|
+
if (installing[spec] === true && specPackageName(spec) === name) return true;
|
|
176
|
+
}
|
|
177
|
+
var card = sources.card;
|
|
178
|
+
if (card && specPackageName(card.spec) === name) return true;
|
|
179
|
+
var jobs = sources.jobs || {};
|
|
180
|
+
for (var id in jobs) {
|
|
181
|
+
var job = jobs[id];
|
|
182
|
+
if (!job || typeof job.spec !== "string") continue;
|
|
183
|
+
if (specPackageName(job.spec) !== name) continue;
|
|
184
|
+
if (isJobActive(job.status)) return true;
|
|
185
|
+
if (Array.isArray(job.needsApproval) && job.needsApproval.length > 0) return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
// ── 更新按钮的防连点判据结束 ────────────────────────────────────────────
|
|
190
|
+
|
|
135
191
|
function kindLabel(kind) {
|
|
136
192
|
if (kind === "bundle") return "宿主插件层";
|
|
137
193
|
if (kind === "client") return "浏览器UI插件";
|
|
@@ -344,7 +400,8 @@ window.__ModuleLoader__.load({
|
|
|
344
400
|
}
|
|
345
401
|
commit(next);
|
|
346
402
|
}, [commit]);
|
|
347
|
-
|
|
403
|
+
// jobsRef 一并交出去:防连点的判定读的是当下的值,不是渲染快照。
|
|
404
|
+
return { jobs: jobs, jobsRef: jobsRef, track: track, drop: drop, setStatus: setStatus, restore: restore };
|
|
348
405
|
}
|
|
349
406
|
|
|
350
407
|
// ── plugin verification badge ───────────────────────────────────────────
|
|
@@ -715,6 +772,7 @@ window.__ModuleLoader__.load({
|
|
|
715
772
|
var togglable = entry !== undefined && dep.name !== MARKET_PACKAGE;
|
|
716
773
|
var enabled = entry === undefined ? true : entry.enabled !== false;
|
|
717
774
|
var toggling = (props.toggling || {})[dep.name] === true;
|
|
775
|
+
var updating = props.isUpdating(dep.name);
|
|
718
776
|
return h("div", { key: dep.name, className: "mkt_depRow" },
|
|
719
777
|
h("span", { className: "mkt_desc" + (enabled ? "" : " mkt_depOff") }, dep.name + "@" + dep.version),
|
|
720
778
|
h("span", { className: "mkt_depActions" },
|
|
@@ -728,8 +786,11 @@ window.__ModuleLoader__.load({
|
|
|
728
786
|
}, h("span", { className: "mkt_switchKnob" })) : null,
|
|
729
787
|
upd && upd.hasUpdate ? h("button", {
|
|
730
788
|
className: "mkt_btn mkt_btnSm",
|
|
731
|
-
|
|
732
|
-
|
|
789
|
+
// 和 doUpdate 里的守卫读同一个判据,两边不会漂移——不一致
|
|
790
|
+
// 就会出现「按钮看着能点,点了什么也不发生」。
|
|
791
|
+
disabled: updating,
|
|
792
|
+
onClick: function () { props.onUpdate(dep.name, dep.name + "@" + upd.latest); },
|
|
793
|
+
}, updating ? "更新中…" : "更新至 " + upd.latest) : null,
|
|
733
794
|
h("button", {
|
|
734
795
|
className: "mkt_btn mkt_btnDanger mkt_btnSm",
|
|
735
796
|
disabled: busy,
|
|
@@ -816,6 +877,19 @@ window.__ModuleLoader__.load({
|
|
|
816
877
|
var preflight = _preflight[0];
|
|
817
878
|
var setPreflight = _preflight[1];
|
|
818
879
|
|
|
880
|
+
// 更新按钮防连点用的三份当下值(见 updateChainBusy)。ref 而不是 state:
|
|
881
|
+
// 判定发生在点击那一刻,而 setState 到重渲染之间还有一拍能点。ref 不触发
|
|
882
|
+
// 渲染,所以每次改动配一次 bumpBusy,让按钮当场变样。
|
|
883
|
+
var updatingRef = useRef({}); // 包名 -> true,点击到预检 job 建立之间
|
|
884
|
+
var installingRef = useRef({}); // spec -> true,安装 RPC 在途
|
|
885
|
+
var preflightRef = useRef(null);
|
|
886
|
+
preflightRef.current = preflight; // 渲染期同步,同 resultsRef 的写法
|
|
887
|
+
var _busyTick = useState(0);
|
|
888
|
+
var setBusyTick = _busyTick[1];
|
|
889
|
+
var bumpBusy = useCallback(function () {
|
|
890
|
+
setBusyTick(function (n) { return n + 1; });
|
|
891
|
+
}, []);
|
|
892
|
+
|
|
819
893
|
// Opaque one-shot approval tokens issued from needsApproval outcomes (spec -> token).
|
|
820
894
|
// Cleared on success, unrelated failure, cancel, modal close, or spec change.
|
|
821
895
|
// No boolean warning-consent map is kept.
|
|
@@ -983,6 +1057,17 @@ window.__ModuleLoader__.load({
|
|
|
983
1057
|
var polling = useJobPolling(call, onJobSettled, onApprovalToken, onPreflightSettled);
|
|
984
1058
|
var track = polling.track;
|
|
985
1059
|
var jobs = polling.jobs;
|
|
1060
|
+
var jobsRef = polling.jobsRef;
|
|
1061
|
+
|
|
1062
|
+
// 按钮的 disabled 和 handler 里的守卫都读这一个函数,两边不会漂移。
|
|
1063
|
+
var updateBusy = useCallback(function (packageName) {
|
|
1064
|
+
return updateChainBusy({
|
|
1065
|
+
updating: updatingRef.current,
|
|
1066
|
+
installing: installingRef.current,
|
|
1067
|
+
card: preflightRef.current,
|
|
1068
|
+
jobs: jobsRef.current,
|
|
1069
|
+
}, packageName);
|
|
1070
|
+
}, [jobsRef]);
|
|
986
1071
|
var dropJob = polling.drop;
|
|
987
1072
|
var setJobStatus = polling.setStatus;
|
|
988
1073
|
|
|
@@ -1000,6 +1085,10 @@ window.__ModuleLoader__.load({
|
|
|
1000
1085
|
}, []);
|
|
1001
1086
|
|
|
1002
1087
|
var doRawInstall = useCallback(function (spec, extra, carryFromId) {
|
|
1088
|
+
// ref 与 state 同时置位:state 负责触发渲染,ref 才是判定读的那一份
|
|
1089
|
+
// (见 updateChainBusy)。这一段是预检 job 已落终态、安装 job 还没
|
|
1090
|
+
// track 上的空档,中间断一拍更新按钮就会复活。
|
|
1091
|
+
installingRef.current[spec] = true;
|
|
1003
1092
|
setInstalling(function (prev) {
|
|
1004
1093
|
var next = Object.assign({}, prev, { [spec]: true });
|
|
1005
1094
|
return next;
|
|
@@ -1012,6 +1101,7 @@ window.__ModuleLoader__.load({
|
|
|
1012
1101
|
delete approvalTokensRef.current[spec];
|
|
1013
1102
|
reportError("tasks", e);
|
|
1014
1103
|
}).finally(function () {
|
|
1104
|
+
delete installingRef.current[spec];
|
|
1015
1105
|
setInstalling(function (prev) {
|
|
1016
1106
|
var next = Object.assign({}, prev);
|
|
1017
1107
|
delete next[spec];
|
|
@@ -1048,13 +1138,17 @@ window.__ModuleLoader__.load({
|
|
|
1048
1138
|
|
|
1049
1139
|
// 点安装 = 立刻起一个预检 job,面板马上有东西看、日志实时流。
|
|
1050
1140
|
// 结论由轮询经 onPreflightSettled 交回上面的 handler,这里不等待。
|
|
1051
|
-
|
|
1141
|
+
// onStarted:RPC 落定(不论成败)时回调一次。更新按钮用它把「点击 →
|
|
1142
|
+
// job 建立」这一段的锁交给 job,市场卡片不传,行为一字未变。
|
|
1143
|
+
var preflightAndInstall = useCallback(function (spec, onStarted) {
|
|
1052
1144
|
delete approvalTokensRef.current[spec];
|
|
1053
1145
|
setError(null);
|
|
1054
1146
|
call("preflight", { spec: spec }).then(function (value) {
|
|
1055
1147
|
track(value.jobId, value.spec || spec);
|
|
1056
1148
|
}).catch(function (e) {
|
|
1057
1149
|
reportError("tasks", e);
|
|
1150
|
+
}).finally(function () {
|
|
1151
|
+
if (typeof onStarted === "function") onStarted();
|
|
1058
1152
|
});
|
|
1059
1153
|
}, [call, reportError, track]);
|
|
1060
1154
|
|
|
@@ -1062,6 +1156,26 @@ window.__ModuleLoader__.load({
|
|
|
1062
1156
|
preflightAndInstall("github:" + repo);
|
|
1063
1157
|
}, [preflightAndInstall]);
|
|
1064
1158
|
|
|
1159
|
+
// 已装列表的「更新至 X」。此前它是那一行里唯一没有 disabled 的按钮,
|
|
1160
|
+
// 而起链要走联网的 registry 解析,那几秒里 jobs 还是空的、按钮还是可点
|
|
1161
|
+
// 的——连点几下就是几条预检+安装链,profile 事务队列会把它们排队串行
|
|
1162
|
+
// 跑完,同一个版本被反复装好几遍。
|
|
1163
|
+
//
|
|
1164
|
+
// 守卫按包名(updateChainBusy 的注释说明了为什么不能按 spec),同步的
|
|
1165
|
+
// ref 挡住同一拍里的第二次点击;RPC 返回只解开 updating 那一段,之后由
|
|
1166
|
+
// 对应的 job 接管,锁一直持续到 job 落终态。
|
|
1167
|
+
var doUpdate = useCallback(function (packageName, spec) {
|
|
1168
|
+
if (updateBusy(packageName)) return;
|
|
1169
|
+
updatingRef.current[packageName] = true;
|
|
1170
|
+
bumpBusy();
|
|
1171
|
+
preflightAndInstall(spec, function () {
|
|
1172
|
+
// preflight RPC 已返回:jobs 里已经有条目接手,这里才允许松手。
|
|
1173
|
+
// 顺序反过来会闪出一拍「谁都不认为它在忙」的可点窗口。
|
|
1174
|
+
delete updatingRef.current[packageName];
|
|
1175
|
+
bumpBusy();
|
|
1176
|
+
});
|
|
1177
|
+
}, [preflightAndInstall, updateBusy, bumpBusy]);
|
|
1178
|
+
|
|
1065
1179
|
var doRestart = useCallback(function () {
|
|
1066
1180
|
clearRestartPing();
|
|
1067
1181
|
setRestarting(true);
|
|
@@ -1185,7 +1299,8 @@ window.__ModuleLoader__.load({
|
|
|
1185
1299
|
toggling: toggling,
|
|
1186
1300
|
onToggle: doToggle,
|
|
1187
1301
|
onUninstall: doUninstall,
|
|
1188
|
-
|
|
1302
|
+
onUpdate: doUpdate,
|
|
1303
|
+
isUpdating: updateBusy,
|
|
1189
1304
|
error: error && error.scope === "installed" ? error.message : null,
|
|
1190
1305
|
}),
|
|
1191
1306
|
h(JobsPanel, {
|
package/src/index.js
CHANGED
|
@@ -3309,6 +3309,62 @@ 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: {}, card: null, 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({ card: { spec: "foo@1.2.3" } }, "foo") === true);
|
|
3354
|
+
|
|
3355
|
+
check("更新防连点:job 落终态后解锁,用户可以重试",
|
|
3356
|
+
busy({ jobs: { a: { spec: "foo@1.2.3", status: "failed" } } }, "foo") === false);
|
|
3357
|
+
check("更新防连点:completed 后解锁",
|
|
3358
|
+
busy({ jobs: { a: { spec: "foo@1.2.3", status: "completed" } } }, "foo") === false);
|
|
3359
|
+
check("更新防连点:别的包在跑不锁本行",
|
|
3360
|
+
busy({ jobs: { a: running("bar@1.0.0") } }, "foo") === false);
|
|
3361
|
+
check("更新防连点:风险卡片停在别的包上不锁本行",
|
|
3362
|
+
busy({ card: { spec: "bar@1.0.0" } }, "foo") === false);
|
|
3363
|
+
check("更新防连点:什么都没在途 → 不锁", busy({}, "foo") === false);
|
|
3364
|
+
check("更新防连点:包名为 null 时不误判",
|
|
3365
|
+
busy({ jobs: { a: running("foo@1.2.3") } }, null) === false);
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3312
3368
|
let cancelRefused = false;
|
|
3313
3369
|
try {
|
|
3314
3370
|
sessionTracker.cancel(sessionJobId, "session-beta");
|