@mzzsfy/dsh-turn-notify 0.8.0 → 0.8.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mzzsfy/dsh-turn-notify",
3
3
  "description": "消息通知:六类回合事件送达声音、系统弹窗、页内提示、webhook 与 IM;内置八种合成音色,自定义音效,按事件分类指定;多窗口只响一次",
4
- "version": "0.8.0",
4
+ "version": "0.8.1",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
package/src/client.js CHANGED
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
8
8
  id: '@mzzsfy/dsh-turn-notify',
9
9
  factory(require) {
10
10
  const React = require('react')
11
- const { useState, useEffect } = React
11
+ const { useState, useEffect, useRef } = React
12
12
 
13
13
  // 通知出口:公共依赖 @mzzsfy/dsh-toast,可选消费——共享依赖包的模块表注入
14
14
  // 全仓收敛到唯一权威消费方(session-manager),本插件不再代挂占位条目;
@@ -827,13 +827,28 @@ window.__ModuleLoader__.load({
827
827
 
828
828
  // 宿主重渲染会重建行节点抹掉高亮类,观察器在同一帧内补齐;
829
829
  // applySessionHighlights 幂等(类齐不写 DOM),观察器链自然收敛;
830
- // 令牌承载 observer:HMR 重建闭包后先断开旧代再挂新代,旧闭包不滞留
830
+ // 令牌承载 observer 与 pending 帧:HMR 重建闭包后先断开旧代 observer、
831
+ // cancel 旧代 pending 帧(帧回调持有旧代高亮 Map,不取消会写幽灵高亮),
832
+ // 旧闭包不滞留;rAF 合批:流式期间 DOM 变更批远高于帧率,同帧多批合并
833
+ // 为一次重应用(同帧补齐语义不变),无高亮条目时不排帧
831
834
  const KEY_HL_OBSERVER = 'turn-notify:hl-observer'
835
+ const KEY_HL_FRAME = 'turn-notify:hl-frame'
832
836
  function ensureHighlightObserver() {
833
837
  if (typeof document === 'undefined' || typeof document.body === 'undefined' || typeof MutationObserver === 'undefined') return
834
838
  if (window[KEY_HL_OBSERVER] !== undefined && typeof window[KEY_HL_OBSERVER].disconnect === 'function') window[KEY_HL_OBSERVER].disconnect()
839
+ if (typeof window[KEY_HL_FRAME] === 'number') {
840
+ cancelAnimationFrame(window[KEY_HL_FRAME])
841
+ }
842
+ // 哨兵 0 = 无在途帧(规范 rAF 句柄自 1 起,cancel 0 为 no-op);新代显式归零,
843
+ // 防 undefined 初始态使在途判定恒真、帧永不排程
844
+ window[KEY_HL_FRAME] = 0
835
845
  const observer = new MutationObserver(() => {
836
- if (sessionHighlightEnabled && sessionHighlights.size > 0) applySessionHighlights()
846
+ if (!sessionHighlightEnabled || sessionHighlights.size === 0) return
847
+ if (window[KEY_HL_FRAME] !== 0) return
848
+ window[KEY_HL_FRAME] = requestAnimationFrame(() => {
849
+ window[KEY_HL_FRAME] = 0
850
+ if (sessionHighlightEnabled && sessionHighlights.size > 0) applySessionHighlights()
851
+ })
837
852
  })
838
853
  observer.observe(document.body, { childList: true, subtree: true })
839
854
  window[KEY_HL_OBSERVER] = observer
@@ -1484,14 +1499,16 @@ window.__ModuleLoader__.load({
1484
1499
  // 已绑 bot chips 由 imBoundBotIds 去重(见 LOGIC 段同源函数)
1485
1500
  const imBoundBots = imBoundBotIds(config.imTargets)
1486
1501
 
1487
- // 勾选即存:与分类开关同模式,列表整体替换,连续操作以最新一次请求为准
1488
- let imPersistSeq = 0
1502
+ // 勾选即存:与分类开关同模式,列表整体替换,连续操作以最新一次请求为准;
1503
+ // seq 守卫须跨渲染存活(useRef),组件体内 let 声明每次渲染重置使守卫失效
1504
+ const imPersistSeqRef = useRef(0)
1489
1505
  async function persistImTargets(next, okMessage) {
1490
- const seq = ++imPersistSeq
1506
+ imPersistSeqRef.current += 1
1507
+ const seq = imPersistSeqRef.current
1491
1508
  setConfig({ ...config, imTargets: next })
1492
1509
  try {
1493
1510
  const res = await api('/api/turn-notify/config', { method: 'POST', body: JSON.stringify({ imTargets: next }) })
1494
- if (seq === imPersistSeq) {
1511
+ if (seq === imPersistSeqRef.current) {
1495
1512
  setConfig({ ...DEFAULT_CONFIG, ...res })
1496
1513
  if (res.soundMapping) setMappingState(res.soundMapping)
1497
1514
  if (okMessage !== undefined) patch(okMessage)
@@ -0,0 +1,28 @@
1
+ // client 半区 React hooks 解构静态断言:client.js 为经典 bundle,组件渲染路径
2
+ // 无运行时测试,hooks 未从 React 解构的缺陷(useRef is not a function)只能靠
3
+ // 源码静态比对拦截——组件体内使用的全部 hooks 必须出现在解构清单。
4
+ import test from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { readFileSync } from 'node:fs'
7
+ import { join, dirname } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
11
+ const source = readFileSync(join(root, 'src', 'client.js'), 'utf8')
12
+
13
+ test('client.js 组件使用的全部 React hooks 均在解构清单中', () => {
14
+ // Given 解构行(const { ... } = React)与全源码 hooks 调用
15
+ const destructure = /const\s*\{([^}]+)\}\s*=\s*React\b/.exec(source)
16
+ assert.notEqual(destructure, null, 'React 解构行必须存在')
17
+ const declared = new Set(destructure[1].split(',').map((name) => name.trim()).filter(Boolean))
18
+ const used = [...source.matchAll(/\b(use[A-Z]\w*)\s*\(/g)].map((match) => match[1])
19
+ assert.ok(used.length > 0, '至少存在一个 hooks 调用,否则断言失效')
20
+ // Then 每个使用的 hooks 都已声明
21
+ const missing = [...new Set(used)].filter((name) => !declared.has(name))
22
+ assert.deepEqual(missing, [], '未解构的 hooks(渲染即 ReferenceError)')
23
+ })
24
+
25
+ test('client.js 不存在 React.useXxx 直调形态(绕过解构清单)', () => {
26
+ const direct = [...source.matchAll(/\bReact\.(use[A-Z]\w*)\s*\(/g)].map((match) => match[1])
27
+ assert.deepEqual(direct, [], 'hooks 一律经解构清单使用,便于静态断言')
28
+ })