@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 +1 -1
- package/src/client.js +24 -7
- package/test/hooks-destructure.test.mjs +28 -0
package/package.json
CHANGED
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
|
|
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
|
|
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
|
-
|
|
1502
|
+
// 勾选即存:与分类开关同模式,列表整体替换,连续操作以最新一次请求为准;
|
|
1503
|
+
// seq 守卫须跨渲染存活(useRef),组件体内 let 声明每次渲染重置使守卫失效
|
|
1504
|
+
const imPersistSeqRef = useRef(0)
|
|
1489
1505
|
async function persistImTargets(next, okMessage) {
|
|
1490
|
-
|
|
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 ===
|
|
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
|
+
})
|