dsh-mobile-use 1.0.1 → 1.0.4

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.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 坐标换算回归:从 client.js 里切出真实的 pictureRect / turned / devicePoint 源码,
3
+ * 在 node 里用假 DOM 矩形跑。侧栏在右边时坐标曾经被减两次(视觉中心点成 0,600),
4
+ * 这条测试就是那份审阅复现的固化版:改坏就红。
5
+ *
6
+ * node test/coords.test.mjs
7
+ */
8
+
9
+ import fs from 'node:fs'
10
+ import assert from 'node:assert/strict'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ const here = path.dirname(fileURLToPath(import.meta.url))
15
+ const source = fs.readFileSync(path.join(here, '..', 'client.js'), 'utf8')
16
+
17
+ const start = source.indexOf('/* 画面元素:MSE 用 video')
18
+ const end = source.indexOf('/* 平移边界')
19
+ assert.ok(start > 0 && end > start, '没找到几何换算那一段源码')
20
+
21
+ const geometry = source.slice(start, end)
22
+ const factory = new Function(
23
+ 'engine', 'videoRef', 'imgRef', 'canvasRef', 'screen', 'frameSize', 'Math',
24
+ `${geometry}\n return { devicePoint, pictureRect, turned }`,
25
+ )
26
+
27
+ /**
28
+ * 造一个用给定矩形假装的 DOM 元素。
29
+ * @param {number} left - 视口左边缘。
30
+ * @param {number} top - 视口上边缘。
31
+ * @param {number} width - 宽。
32
+ * @param {number} height - 高。
33
+ * @returns {{ getBoundingClientRect: () => any }} 元素桩。
34
+ */
35
+ const box = (left, top, width, height) => ({ getBoundingClientRect: () => ({ left, top, width, height, right: left + width, bottom: top + height }) })
36
+
37
+ /**
38
+ * 造一台设备状态下的换算器。
39
+ * @param {{ device: {w: number, h: number}, frame: {w: number, h: number}, canvas: any, media: any, engine?: string }} config - 现场。
40
+ * @returns {(point: {x: number, y: number}) => {x: number, y: number}} 换算函数。
41
+ */
42
+ const make = ({ device, frame, canvas, media, engine = 'jpeg' }) => {
43
+ const { devicePoint } = factory(engine, { current: null }, { current: media }, { current: canvas }, device, frame, Math)
44
+ return devicePoint
45
+ }
46
+
47
+ let pass = 0
48
+ /**
49
+ * 断言一点。
50
+ * @param {string} label - 检查项。
51
+ * @param {() => void} run - 断言体。
52
+ * @returns {void}
53
+ */
54
+ const it = (label, run) => {
55
+ try {
56
+ run()
57
+ pass += 1
58
+ console.log(` ✅ ${label}`)
59
+ } catch (error) {
60
+ console.log(` ❌ ${label}`)
61
+ throw error
62
+ }
63
+ }
64
+
65
+ console.log('\n坐标换算(源码级)\n')
66
+
67
+ /* 审阅给的复现:右栏里画面框 left=1000/top=100,显示 300x533.33,设备 1080x1920。
68
+ 取画面正中心,正确结果是 (540,960);旧代码会算成 (0,600)。 */
69
+ it('右栏里点画面正中心 = 设备正中心', () => {
70
+ const toDevice = make({
71
+ device: { w: 1080, h: 1920 },
72
+ frame: { w: 300, h: 533.333 },
73
+ canvas: box(1000, 100, 300, 533.333),
74
+ media: box(1000, 100, 300, 533.333),
75
+ })
76
+ assert.deepEqual(toDevice({ x: 150, y: 266.666 }), { x: 540, y: 960 })
77
+ })
78
+
79
+ it('画面有上下留边时按画面矩形换算(不是画布)', () => {
80
+ const toDevice = make({
81
+ device: { w: 1080, h: 1350 },
82
+ frame: { w: 400, h: 500 },
83
+ canvas: box(20, 40, 400, 600),
84
+ media: box(20, 90, 400, 500),
85
+ })
86
+ assert.deepEqual(toDevice({ x: 200, y: 250 }), { x: 540, y: 540 })
87
+ })
88
+
89
+ it('画面有左右留边时按画面矩形换算', () => {
90
+ const toDevice = make({
91
+ device: { w: 1080, h: 1080 },
92
+ frame: { w: 400, h: 400 },
93
+ canvas: box(0, 0, 500, 400),
94
+ media: box(50, 0, 400, 400),
95
+ })
96
+ assert.deepEqual(toDevice({ x: 250, y: 200 }), { x: 540, y: 540 })
97
+ })
98
+
99
+ it('画面外沿夹到边界,不会溢成负数', () => {
100
+ const toDevice = make({
101
+ device: { w: 1080, h: 1920 },
102
+ frame: { w: 300, h: 533 },
103
+ canvas: box(1000, 100, 300, 533),
104
+ media: box(1000, 100, 300, 533),
105
+ })
106
+ assert.deepEqual(toDevice({ x: -50, y: 900 }), { x: 0, y: 1920 })
107
+ })
108
+
109
+ it('横屏画面 + 竖屏设备(转过 90°)映射到长边', () => {
110
+ const toDevice = make({
111
+ device: { w: 2136, h: 3200 },
112
+ frame: { w: 2400, h: 1080 },
113
+ canvas: box(0, 0, 2400, 1080),
114
+ media: box(0, 0, 2400, 1080),
115
+ })
116
+ assert.deepEqual(toDevice({ x: 1200, y: 540 }), { x: 1068, y: 1600 })
117
+ })
118
+
119
+ it('MSE 走 video 元素,抓帧走 img,两条路都拿画面矩形', () => {
120
+ const canvas = box(0, 0, 300, 500)
121
+ const video = box(0, 0, 300, 500)
122
+ const img = box(10, 20, 280, 460)
123
+ const mse = factory('mse', { current: video }, { current: img }, { current: canvas }, { w: 1080, h: 1800 }, { w: 300, h: 500 }, Math)
124
+ assert.deepEqual(mse.devicePoint({ x: 150, y: 250 }), { x: 540, y: 900 })
125
+ const jpeg = factory('jpeg', { current: video }, { current: img }, { current: canvas }, { w: 1080, h: 1800 }, { w: 300, h: 500 }, Math)
126
+ assert.deepEqual(jpeg.devicePoint({ x: 150, y: 250 }), { x: 540, y: 900 })
127
+ })
128
+
129
+ console.log(`\n${pass} 项通过\n`)
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 悬空 this 调用扫描:rename 后残留 `this.oldName()` 会让插件在回调里抛 TypeError,
3
+ * 而宿主不隔离插件异常 —— 一次空转定时器就能打死整个 DSH(2026-10-27 的 killStale 事故)。
4
+ * 只做一件事:文件里每个 `this.foo(` 都要能找到定义(方法简写或 this.foo = 赋值)。
5
+ */
6
+ import { readFileSync } from 'node:fs'
7
+
8
+ const FILES = ['stream.js', 'mirror.js', 'index.js', 'client.js']
9
+
10
+ /** 从源码里收集「哪些名字在这份文件里是可调用的 this 成员」。 */
11
+ function knownMembers(source) {
12
+ const names = new Set()
13
+ /* this.foo = ... 的赋值(含 this.foo = function / 箭头函数)。 */
14
+ for (const m of source.matchAll(/this\.([A-Za-z_$][\w$]*)\s*=(?!=)/g)) names.add(m[1])
15
+ /* 方法简写 / 类字段箭头函数:行首缩进后的 `foo(...) {` 或 `foo = (...) =>`。 */
16
+ for (const m of source.matchAll(/^\s{2,}(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/gm)) names.add(m[1])
17
+ for (const m of source.matchAll(/^\s{2,}([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\(/gm)) names.add(m[1])
18
+ /* 对象字面量里的方法(route 表这类)也给过。 */
19
+ for (const m of source.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*(?:async\s*)?(?:function|\()/g)) names.add(m[1])
20
+ return names
21
+ }
22
+
23
+ /** 常见内置/外部注入成员,不要求在文件内定义。 */
24
+ const BUILTIN = new Set([
25
+ 'constructor', 'then', 'catch', 'finally', 'apply', 'call', 'bind', 'toString', 'valueOf',
26
+ 'hasOwnProperty', 'push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'map', 'filter',
27
+ 'forEach', 'find', 'findIndex', 'some', 'every', 'reduce', 'join', 'split', 'trim',
28
+ 'replace', 'includes', 'startsWith', 'endsWith', 'padStart', 'padEnd', 'toFixed',
29
+ 'getTime', 'toISOString', 'keys', 'values', 'entries', 'set', 'get', 'delete', 'has',
30
+ 'clear', 'add', 'size', 'length', 'emit', 'on', 'once', 'off', 'removeListener', 'end',
31
+ 'write', 'pipe', 'read', 'kill', 'ref', 'unref', 'stdin', 'stdout', 'stderr',
32
+ ])
33
+
34
+ let failures = 0
35
+ const it = (label, run) => {
36
+ try {
37
+ run()
38
+ console.log(` ✅ ${label}`)
39
+ } catch (error) {
40
+ failures += 1
41
+ console.log(` ❌ ${label}\n ${String(error?.message ?? error)}`)
42
+ }
43
+ }
44
+ const assert = (cond, msg) => {
45
+ if (!cond) throw new Error(msg)
46
+ }
47
+
48
+ for (const file of FILES) {
49
+ it(`${file}:this.* 调用都有定义`, () => {
50
+ const source = readFileSync(new URL(`../${file}`, import.meta.url), 'utf8')
51
+ const known = knownMembers(source)
52
+ const dangling = new Set()
53
+ for (const m of source.matchAll(/this\.([A-Za-z_$][\w$]*)\s*\(/g)) {
54
+ const name = m[1]
55
+ if (known.has(name) || BUILTIN.has(name)) continue
56
+ dangling.add(name)
57
+ }
58
+ assert(dangling.size === 0, `悬空调用:${[...dangling].join(', ')}(改名后残留会让插件回调抛 TypeError,host 直接退出)`)
59
+ })
60
+ }
61
+
62
+ /* 自检:故意造一份带残留调用的源码,确认扫描器抓得到,别写成永远绿的摆设。 */
63
+ it('扫描器能抓到悬空调用(自检)', () => {
64
+ const broken = 'class A {\n killOwn() {}\n stop() {\n if (this.n > 0) this.killStale()\n }\n}\n'
65
+ const known = knownMembers(broken)
66
+ const dangling = [...broken.matchAll(/this\.([A-Za-z_$][\w$]*)\s*\(/g)]
67
+ .map((m) => m[1])
68
+ .filter((name) => !known.has(name) && !BUILTIN.has(name))
69
+ assert(dangling.includes('killStale'), '扫描器没抓到 killStale,说明这个测试本身失效了')
70
+ })
71
+
72
+ console.log(failures === 0 ? `\n=== 全部通过 ===` : `\n=== ${failures} 项失败 ===`)
73
+ process.exitCode = failures === 0 ? 0 : 1
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 定时器兜底扫描:宿主不隔离插件异常,定时器回调里任何一个 TypeError 都会冒到顶层,
3
+ * 整个 DSH 以 1 退出(2026-10-27 `this.killStale` 两次事故就是空闲回收定时器 → stop())。
4
+ * 规则:host 侧所有节拍器/回收定时器的回调都要经过 guard(),单拍出错只记日志。
5
+ */
6
+ import { readFileSync } from 'node:fs'
7
+
8
+ /** 以 this 成员形式挂出去的定时器(节拍器与空闲/续录回收)。 */
9
+ const PATTERN = /this\.(timer|pushTimer|idleTimer|restartTimer)\s*=\s*(?:setInterval|setTimeout)\(/g
10
+
11
+ /** 从注册点往后看多远算「同一个回调」。 */
12
+ const WINDOW = 160
13
+
14
+ /** 找出没走 guard 的定时器注册。 @param {string} source - 文件源码。 @returns {string[]} 违规点。 */
15
+ function unguarded(source) {
16
+ const bad = []
17
+ for (const m of source.matchAll(PATTERN)) {
18
+ const tail = source.slice(m.index, m.index + WINDOW)
19
+ if (!tail.includes('guard(')) bad.push(source.slice(m.index, m.index + 60).split('\n')[0])
20
+ }
21
+ return bad
22
+ }
23
+
24
+ let failures = 0
25
+ const it = (label, run) => {
26
+ try {
27
+ run()
28
+ console.log(` ✅ ${label}`)
29
+ } catch (error) {
30
+ failures += 1
31
+ console.log(` ❌ ${label}\n ${String(error?.message ?? error)}`)
32
+ }
33
+ }
34
+ const assert = (cond, msg) => {
35
+ if (!cond) throw new Error(msg)
36
+ }
37
+
38
+ for (const file of ['stream.js', 'mirror.js']) {
39
+ it(`${file}:定时器回调都经过 guard`, () => {
40
+ const bad = unguarded(readFileSync(new URL(`../${file}`, import.meta.url), 'utf8'))
41
+ assert(bad.length === 0, `未兜底的定时器:${bad.join(' | ')}`)
42
+ })
43
+ }
44
+
45
+ it('自检:没兜底的定时器会被抓出来', () => {
46
+ const fake = 'this.idleTimer = setTimeout(() => { this.stop() }, 20000)'
47
+ assert(unguarded(fake).length === 1, '扫描放过了一个裸定时器')
48
+ })
49
+
50
+ it('自检:兜底后的定时器不误报', () => {
51
+ const fake = "this.idleTimer = setTimeout(() => guard('idle-stop', () => { this.stop() }), 20000)"
52
+ assert(unguarded(fake).length === 0, '兜底写法被误判')
53
+ })
54
+
55
+ if (failures > 0) {
56
+ console.log(`=== ${failures} 项失败 ===`)
57
+ process.exitCode = 1
58
+ } else {
59
+ console.log('=== 全部通过 ===')
60
+ }