dsh-m 0.9.77 → 0.9.78

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/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.9.78 性能:收藏 stale 检查零网络 + 市场页零增量提交守卫(钢人 C3)
12
+
13
+ - **问题(钢人报告 C3 节)**:收藏 stale 判定只需要「id 还在不在目录」的成员布尔,却穿透富 market interface 的探测段——每次收藏区访问/切换(800ms debounce 合并后)仍是 N 条收藏 × full 查询,含每命中条 npm/github latest **网络扇出**且产物全部丢弃;叠加 toggle 后 refreshViews 四连响应无条件 `setData`——数据层精心维持的引用稳定性一次 setState 即蒸发。
14
+ - **修复**:`lookupLive` 补 `probeMode:"cache-only"`(ADR-0013 第一段模式,零网络零探测;existence = items 成员判定,语义零变化);`fetchPage` 增设**零增量提交守卫**——新增 `marketResponseEquivalent` 纯函数(JSON 值语义递归比较,ZoneChips setGeom 同款纪律),内容等价则跳过 `setData`(dataRef/loading 语义不动)。报告可选项「客户端 stale TTL 缓存」明确不做:C1 物化后 cache-only 查询已是内存级。
15
+ - **验证**:1389 项测试全绿(执行基线 1382 + 新增 7)、typecheck 零错误;真机差分冒烟 5 组用例(npm 主清单/自身/社区条目/不存在 id):cache-only 与 full 的 id 集合与 total 逐项一致(红点语义不变的充分条件),cache-only ~30ms vs full 冷跑 ~250ms(零探测指纹);实施计划经独立评审两轮闭环(F1-1 计划基线勘误)。
16
+
11
17
  ### 0.9.76 修复:三项展示层退役与吸顶跳动根治——「对话」徽章/「版本无关」徽章退役 + 分类行滚动锚定自激断路
12
18
 
13
19
  - **① 「对话」来源徽章退役(主人实拍:界面全点按钮却全员误标「对话」)**:`meta.session` 是 0.7.0 终审·新伤1 的**泵所有权标记**(restore 跳过 / replaceAll 不回卷 / persist 时剥离),GUI 的 `runOp` 与对话区镜像两路无条件同打同一标——它区分不了来源,且重载后徽章整体蒸发。修复:opRowVm 删 session 派生、行渲染删徽章、CSS 与 i18n 键退役;`meta.session` 本职不动(数据模型零变更),来源区分留待未来 `meta.origin` 承载。
@@ -559,6 +565,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
559
565
 
560
566
  ## English
561
567
 
568
+ ### Performance in 0.9.78 — favorites stale checks go cache-only: zero-network favorites access + zero-increment commit guard (steelman C3)
569
+
570
+ - **Problem (steelman report, card C3)**: the favorites stale verdict only needs a "is this id still in the catalog" membership boolean, yet it pierces the rich market interface's probe segment - every favorites visit/toggle (after the 800ms debounce merge) still fires N favorites × full queries including a per-hit npm/github latest **network fan-out** whose products are entirely discarded. On top of that, the unconditional `setData` after refreshViews' four-in-a-row responses (the post-toggle norm) evaporates the reference stability the data layer carefully maintains.
571
+ - **Fix**: `lookupLive` now passes `probeMode:"cache-only"` (ADR-0013 first-phase mode: zero network, zero probing; existence = items membership, semantics unchanged); `fetchPage` gains a **zero-increment commit guard** - a new `marketResponseEquivalent` pure function (recursive JSON-value comparison, ZoneChips setGeom discipline) skips `setData` when content is equivalent (dataRef/loading semantics untouched). The report's optional "client-side stale TTL cache" is explicitly not done: after C1 materialization, cache-only queries are already memory-grade.
572
+ - **Verification**: 1389 tests green (execution baseline 1382 + 7 added), typecheck clean; live-instance differential smoke across 5 cases (npm primary / self / community entry / nonexistent id): id sets and totals identical between cache-only and full (the sufficient condition for unchanged red-dot semantics), cache-only ~30ms vs cold full ~250ms (the zero-probe fingerprint); the implementation plan was closed over two independent review rounds (F1-1 plan-baseline correction).
573
+
562
574
  ### Fix in 0.9.76 — three presentation-layer retirements and sticky-chips jitter fixed at the root: "Chat" badge / "Version-independent" badge retired + scroll-anchoring self-excitation cut on the category row
563
575
 
564
576
  - **① "Chat" source badge retired (owner screenshot: every row flagged despite all-UI clicks)**: `meta.session` is the **pump-ownership marker** from the 0.7.0 final review (restore skip / replaceAll no-rollback / stripped on persist) — both the GUI `runOp` path and the chat-mirror path set it unconditionally, so it cannot distinguish origins, and the badge vanishes entirely after a page reload. Fix: opRowVm drops the session derivation; the row badge, its CSS and i18n keys retire; `meta.session` keeps its day job (zero data-model change); a future `meta.origin` would be the honest carrier for source attribution.
package/lib/client.js CHANGED
@@ -34,6 +34,7 @@ __export(market_state_exports, {
34
34
  countBeyondRows: () => countBeyondRows,
35
35
  createZoneState: () => createZoneState,
36
36
  marketNotice: () => marketNotice,
37
+ marketResponseEquivalent: () => marketResponseEquivalent,
37
38
  mergeLatestFields: () => mergeLatestFields,
38
39
  normalizeMarketQuery: () => normalizeMarketQuery,
39
40
  normalizeMarketResponse: () => normalizeMarketResponse,
@@ -235,6 +236,25 @@ function mergeLatestFields(nextItems, prevItems) {
235
236
  });
236
237
  return touched ? out : nextItems;
237
238
  }
239
+ function marketResponseEquivalent(a, b) {
240
+ if (a === b) return true;
241
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
242
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
243
+ if (Array.isArray(a)) {
244
+ if (a.length !== b.length) return false;
245
+ for (let i = 0; i < a.length; i++) {
246
+ if (!marketResponseEquivalent(a[i], b[i])) return false;
247
+ }
248
+ return true;
249
+ }
250
+ const ka = Object.keys(a).filter((k) => a[k] !== void 0);
251
+ const kb = Object.keys(b).filter((k) => b[k] !== void 0);
252
+ if (ka.length !== kb.length) return false;
253
+ for (const k of ka) {
254
+ if (!Object.prototype.hasOwnProperty.call(b, k) || !marketResponseEquivalent(a[k], b[k])) return false;
255
+ }
256
+ return true;
257
+ }
238
258
  function chipRows(offsets) {
239
259
  if (!Array.isArray(offsets)) return 0;
240
260
  let rows = 0;
@@ -1866,7 +1886,7 @@ var h = React.createElement;
1866
1886
  var { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } = React;
1867
1887
  var PLUGIN_ID = "dsh-m";
1868
1888
  var API = "/dshm";
1869
- var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZES2, pageItems: pageItems2, createZoneState: createZoneState2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, zoneChips: zoneChips2, marketNotice: marketNotice2, searchSourceOf: searchSourceOf2, mergeLatestFields: mergeLatestFields2, chipRows: chipRows2, countBeyondRows: countBeyondRows2, clipTopOf: clipTopOf2, visibleRowTop: visibleRowTop2, autoExpandDecision: autoExpandDecision2 } = (init_market_state(), __toCommonJS(market_state_exports));
1889
+ var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZES2, pageItems: pageItems2, createZoneState: createZoneState2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, zoneChips: zoneChips2, marketNotice: marketNotice2, searchSourceOf: searchSourceOf2, mergeLatestFields: mergeLatestFields2, chipRows: chipRows2, countBeyondRows: countBeyondRows2, clipTopOf: clipTopOf2, visibleRowTop: visibleRowTop2, autoExpandDecision: autoExpandDecision2, marketResponseEquivalent: marketResponseEquivalent2 } = (init_market_state(), __toCommonJS(market_state_exports));
1870
1890
  var { readMarketSnapshot: readMarketSnapshot2, writeMarketSnapshot: writeMarketSnapshot2, isDefaultFirstPageQuery: isDefaultFirstPageQuery2 } = (init_market_snapshot(), __toCommonJS(market_snapshot_exports));
1871
1891
  var { backdropCloseHandlers: backdropCloseHandlers2 } = (init_backdrop(), __toCommonJS(backdrop_exports));
1872
1892
  var { lbStep: lbStep2, lbNeighbors: lbNeighbors2, swipeDir: swipeDir2 } = (init_lightbox(), __toCommonJS(lightbox_exports));
@@ -2944,11 +2964,12 @@ function useMarketData(zone = "community") {
2944
2964
  return api("market", params, ac.signal).then((raw) => {
2945
2965
  if (genRef.current !== gen || ac.signal.aborted) return;
2946
2966
  const next = normalizeMarketResponse2(raw);
2947
- const prevItems = dataRef.current && Array.isArray(dataRef.current.items) ? dataRef.current.items : null;
2967
+ const prevData = dataRef.current;
2968
+ const prevItems = prevData && Array.isArray(prevData.items) ? prevData.items : null;
2948
2969
  const mergedItems = mergeLatestFields2(next.items, prevItems);
2949
2970
  const merged = mergedItems === next.items ? next : { ...next, items: mergedItems };
2950
2971
  dataRef.current = merged;
2951
- setData(merged);
2972
+ if (!marketResponseEquivalent2(prevData, merged)) setData(merged);
2952
2973
  setLoading(false);
2953
2974
  if (probeMode === "cache-only" && next.latestComplete === false) {
2954
2975
  return fetchPage(nextQuery, force, true, "full");
@@ -4730,7 +4751,7 @@ function FavoriteZone({ favorites, onOpen }) {
4730
4751
  void (async () => {
4731
4752
  const lookupLive = async (fav) => {
4732
4753
  try {
4733
- const res = await api("market", { query: fav.id, source: "all", limit: 8 });
4754
+ const res = await api("market", { query: fav.id, source: "all", limit: 8, probeMode: "cache-only" });
4734
4755
  const items = res && Array.isArray(res.items) ? res.items : [];
4735
4756
  return items.some((x) => x && x.id === fav.id);
4736
4757
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.77",
3
+ "version": "0.9.78",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",