@yanglaofish/dsh-proxy-pro 1.1.0 → 2.0.1
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/docs/LESSONS.md +1217 -994
- package/lib/client.js +67 -16
- package/lib/index.js +901 -831
- package/lib/proxy-core.js +43 -18
- package/package.json +5 -5
package/lib/client.js
CHANGED
|
@@ -35,6 +35,7 @@ window.__ModuleLoader__.load({
|
|
|
35
35
|
var react = require("react");
|
|
36
36
|
var useState = react.useState;
|
|
37
37
|
var useEffect = react.useEffect;
|
|
38
|
+
var useCallback = react.useCallback;
|
|
38
39
|
var useRef = react.useRef;
|
|
39
40
|
var useSyncExternalStore = react.useSyncExternalStore;
|
|
40
41
|
|
|
@@ -115,22 +116,51 @@ window.__ModuleLoader__.load({
|
|
|
115
116
|
// scope (`{ status, value, writable }`), so the render code below is
|
|
116
117
|
// unchanged; an older host simply has no /config route and the panel
|
|
117
118
|
// reports itself unavailable instead of crashing.
|
|
119
|
+
//
|
|
120
|
+
// ONE store for the whole client half: the header capsule and the settings
|
|
121
|
+
// page live in different React trees (the settings shell renders its own
|
|
122
|
+
// root), and per-apply stores left them showing different snapshots —
|
|
123
|
+
// "header ON but panel OFF" (LESSONS §39). A module-level singleton makes
|
|
124
|
+
// every contributor observe exactly one state.
|
|
125
|
+
var sharedProxyStore = null
|
|
126
|
+
|
|
127
|
+
function proxyStore() {
|
|
128
|
+
if (sharedProxyStore === null) sharedProxyStore = createProxyStore()
|
|
129
|
+
return sharedProxyStore
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
function createProxyStore() {
|
|
119
133
|
var listeners = [];
|
|
120
|
-
var snapshot = { status: "loading", value: undefined, writable: false };
|
|
134
|
+
var snapshot = { status: "loading", value: undefined, writable: false, error: "" };
|
|
121
135
|
var inflight = null;
|
|
136
|
+
/** Cheap equality so an unchanged payload does not notify: every emit
|
|
137
|
+
* re-renders its subscribers, and a needless emit inside a render path
|
|
138
|
+
* is how a store turns into a request storm (review fix, LESSONS §40). */
|
|
139
|
+
function unchanged(a, b) {
|
|
140
|
+
return a.status === b.status
|
|
141
|
+
&& a.writable === b.writable
|
|
142
|
+
&& a.error === b.error
|
|
143
|
+
&& JSON.stringify(a.value) === JSON.stringify(b.value);
|
|
144
|
+
}
|
|
122
145
|
function emit(next) {
|
|
146
|
+
if (unchanged(snapshot, next)) return;
|
|
123
147
|
snapshot = next;
|
|
124
148
|
listeners.slice().forEach(function (fn) {
|
|
125
149
|
try { fn(); } catch (error) { console.error("[dsh-proxy-pro] listener failed:", error); }
|
|
126
150
|
});
|
|
127
151
|
}
|
|
152
|
+
/** Keep the last known state but surface why a write was refused: a
|
|
153
|
+
* silent rollback is how a rejected settings write looks like a UI bug
|
|
154
|
+
* (LESSONS §36 — "点一下马上弹回"). */
|
|
155
|
+
function fail(message) {
|
|
156
|
+
emit({ status: snapshot.status, value: snapshot.value, writable: snapshot.writable, error: message });
|
|
157
|
+
}
|
|
128
158
|
function accept(data) {
|
|
129
159
|
if (data === null || data.ok !== true || data.config === undefined || data.config === null) {
|
|
130
|
-
emit({ status: "unavailable", value: undefined, writable: false });
|
|
160
|
+
emit({ status: "unavailable", value: undefined, writable: false, error: (data && data.error) || "" });
|
|
131
161
|
return false;
|
|
132
162
|
}
|
|
133
|
-
emit({ status: "ready", value: data.config, writable: data.writable !== false });
|
|
163
|
+
emit({ status: "ready", value: data.config, writable: data.writable !== false, error: "" });
|
|
134
164
|
return true;
|
|
135
165
|
}
|
|
136
166
|
function refresh() {
|
|
@@ -151,9 +181,14 @@ window.__ModuleLoader__.load({
|
|
|
151
181
|
body: JSON.stringify(body),
|
|
152
182
|
}).then(function (data) {
|
|
153
183
|
if (data === null || data.ok !== true) {
|
|
154
|
-
|
|
184
|
+
var message = (data && data.error) || "配置写入失败(宿主未返回原因)";
|
|
185
|
+
fail(message);
|
|
186
|
+
throw new Error(message);
|
|
155
187
|
}
|
|
156
188
|
accept(data);
|
|
189
|
+
}, function (error) {
|
|
190
|
+
fail("无法连接宿主配置接口:" + String(error && error.message ? error.message : error));
|
|
191
|
+
throw error;
|
|
157
192
|
});
|
|
158
193
|
}
|
|
159
194
|
return {
|
|
@@ -171,22 +206,23 @@ window.__ModuleLoader__.load({
|
|
|
171
206
|
};
|
|
172
207
|
}
|
|
173
208
|
|
|
174
|
-
/** Subscribe to the host-backed config store → `{ value, writable, ready }`.
|
|
209
|
+
/** Subscribe to the host-backed config store → `{ value, writable, ready }`.
|
|
210
|
+
* The subscribe/getSnapshot callbacks are identity-stable (`useCallback`)
|
|
211
|
+
* and the first load lives in an effect: calling `refresh()` from inside
|
|
212
|
+
* subscribe re-subscribed on every render and looped GET /config forever
|
|
213
|
+
* (review fix, LESSONS §40). */
|
|
175
214
|
function useProxySettings(store) {
|
|
176
|
-
var
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return unsubscribe;
|
|
181
|
-
},
|
|
182
|
-
function () { return store.getSnapshot(); }
|
|
183
|
-
);
|
|
215
|
+
var subscribe = useCallback(function (onChange) { return store.subscribe(onChange); }, [store]);
|
|
216
|
+
var getSnapshot = useCallback(function () { return store.getSnapshot(); }, [store]);
|
|
217
|
+
var snapshot = useSyncExternalStore(subscribe, getSnapshot);
|
|
218
|
+
useEffect(function () { store.refresh(); }, [store]);
|
|
184
219
|
var ready = snapshot.status === "ready" && snapshot.value !== undefined;
|
|
185
220
|
return {
|
|
186
221
|
value: ready ? snapshot.value : undefined,
|
|
187
222
|
writable: snapshot.writable === true && ready,
|
|
188
223
|
ready: snapshot.status === "unavailable" ? false : snapshot.status === "ready",
|
|
189
224
|
unavailable: snapshot.status === "unavailable",
|
|
225
|
+
error: snapshot.error || "",
|
|
190
226
|
};
|
|
191
227
|
}
|
|
192
228
|
|
|
@@ -269,9 +305,14 @@ window.__ModuleLoader__.load({
|
|
|
269
305
|
if (!settings.writable) return;
|
|
270
306
|
var next = !shownEnabled;
|
|
271
307
|
setOptimistic(next);
|
|
272
|
-
store
|
|
308
|
+
// The shared store receives the host's answer for this write, so the
|
|
309
|
+
// optimistic echo ends as soon as the round trip settles — success or
|
|
310
|
+
// failure. Waiting for "value === optimistic" left the capsule stuck ON
|
|
311
|
+
// whenever the host refused the write (LESSONS §39).
|
|
312
|
+
store.setEnabled(next).then(function () {
|
|
313
|
+
setOptimistic(null);
|
|
314
|
+
}, function (error) {
|
|
273
315
|
console.error("[dsh-proxy-pro] toggle failed:", error);
|
|
274
|
-
// Roll back to the committed value on failure.
|
|
275
316
|
setOptimistic(null);
|
|
276
317
|
});
|
|
277
318
|
// Toggle doubles as a manual "recheck now" even when the settings
|
|
@@ -285,6 +326,7 @@ window.__ModuleLoader__.load({
|
|
|
285
326
|
var title = active
|
|
286
327
|
? "代理已开启(界面开关)· 点击关闭"
|
|
287
328
|
: "代理已关闭 · 点击开启";
|
|
329
|
+
if (settings.error) title = title + "\n最近一次写入失败:" + settings.error;
|
|
288
330
|
return react.createElement(
|
|
289
331
|
"button",
|
|
290
332
|
{
|
|
@@ -429,6 +471,15 @@ window.__ModuleLoader__.load({
|
|
|
429
471
|
}))
|
|
430
472
|
),
|
|
431
473
|
|
|
474
|
+
// --- last write failure (never fail silently: LESSONS §36) ---
|
|
475
|
+
settings.error
|
|
476
|
+
? react.createElement("div", { className: "dsxprx-status", "data-active": "false", "data-proxy-error": true },
|
|
477
|
+
react.createElement("span", { className: "dsxprx-dot" }),
|
|
478
|
+
react.createElement("div", { className: "dsxprx-statusbody" },
|
|
479
|
+
react.createElement("span", { className: "dsxprx-statusline" }, "配置写入失败"),
|
|
480
|
+
react.createElement("span", { className: "dsxprx-subline" }, settings.error)))
|
|
481
|
+
: null,
|
|
482
|
+
|
|
432
483
|
// --- mode ---
|
|
433
484
|
react.createElement("div", { className: "dsxprx-field" },
|
|
434
485
|
react.createElement("label", { htmlFor: "dsxprx-mode" }, "代理地址来源"),
|
|
@@ -533,7 +584,7 @@ window.__ModuleLoader__.load({
|
|
|
533
584
|
* @param ctx - client cordis context.
|
|
534
585
|
*/
|
|
535
586
|
function apply(ctx) {
|
|
536
|
-
var store =
|
|
587
|
+
var store = proxyStore();
|
|
537
588
|
ctx.effect(function () { return function () { /* no timers to clear */ }; }, "dsh-proxy-pro: config store");
|
|
538
589
|
ctx.slots.inject("settings.section", function () {
|
|
539
590
|
return ctx.slots.register({
|