@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/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
- throw new Error((data && data.error) || "config write failed");
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 snapshot = useSyncExternalStore(
177
- function (onChange) {
178
- var unsubscribe = store.subscribe(onChange);
179
- store.refresh();
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.setEnabled(next).catch(function (error) {
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 = createProxyStore();
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({