@miot-rn/common-component 1.0.2 → 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.
- package/dist/service/specs/index.js +63 -32
- package/dist/service/specs/spec.js +6 -0
- package/dist/service/specs/std-spec.js +31 -22
- package/dist/service/specs/subscribe-logging.test.d.ts +17 -0
- package/dist/service/specs/subscribe-logging.test.js +217 -0
- package/dist/service/track/TrackFlatList.d.ts +3 -0
- package/dist/service/track/TrackFlatList.js +19 -0
- package/dist/service/track/TrackScrollContext.d.ts +10 -0
- package/dist/service/track/TrackScrollContext.js +2 -0
- package/dist/service/track/TrackScrollView.d.ts +3 -0
- package/dist/service/track/TrackScrollView.js +20 -0
- package/dist/service/track/TrackService.d.ts +10 -0
- package/dist/service/track/TrackService.js +104 -0
- package/dist/service/track/index.d.ts +12 -0
- package/dist/service/track/index.js +9 -0
- package/dist/service/track/intersect.d.ts +8 -0
- package/dist/service/track/intersect.js +15 -0
- package/dist/service/track/intersect.test.d.ts +1 -0
- package/dist/service/track/intersect.test.js +189 -0
- package/dist/service/track/types.d.ts +16 -0
- package/dist/service/track/types.js +1 -0
- package/dist/service/track/useCardTrack.d.ts +14 -0
- package/dist/service/track/useCardTrack.js +61 -0
- package/dist/service/track/useExposeOnVisible.d.ts +11 -0
- package/dist/service/track/useExposeOnVisible.js +74 -0
- package/dist/service/track/usePageTrack.d.ts +7 -0
- package/dist/service/track/usePageTrack.js +101 -0
- package/dist/service/track/useTrack.d.ts +7 -0
- package/dist/service/track/useTrack.js +23 -0
- package/dist/service/track/useTrackScrollProvider.d.ts +24 -0
- package/dist/service/track/useTrackScrollProvider.js +81 -0
- package/dist/specs/instance-parser.js +13 -11
- package/dist/store/useGlobalSpecManager.d.ts +5 -2
- package/dist/store/useGlobalSpecManager.js +402 -63
- package/dist/store/useGlobalSpecManager.test.d.ts +1 -0
- package/dist/store/useGlobalSpecManager.test.js +247 -0
- package/dist/utils/subscribe-log.d.ts +1 -0
- package/dist/utils/subscribe-log.js +37 -0
- package/package.json +2 -2
|
@@ -19,10 +19,11 @@ import { Device } from 'miot';
|
|
|
19
19
|
import Subject from "./subject";
|
|
20
20
|
import { getEventGenerator } from "./utils";
|
|
21
21
|
import { encodeProp } from "../../utils/spec";
|
|
22
|
-
import { OK, PLUGIN_NULL_ERROR } from "./error-code";
|
|
22
|
+
import { OK, PLUGIN_NULL_ERROR, PROP_NOT_FOUND } from "./error-code";
|
|
23
23
|
import * as spec from "./spec";
|
|
24
24
|
import specCache, { CACHE_SOURCE } from "./specCache";
|
|
25
25
|
import { copy } from "../../utils/objects";
|
|
26
|
+
import { reportSubLog } from "../../utils/subscribe-log";
|
|
26
27
|
// 事件主题 management
|
|
27
28
|
const specSubject = new Subject(getEventGenerator('spec_subject', encodeProp));
|
|
28
29
|
export { CACHE_SOURCE };
|
|
@@ -38,6 +39,29 @@ export function isCacheEnabled() {
|
|
|
38
39
|
|
|
39
40
|
// Type definitions
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 对齐 native 层返回的结果数组。
|
|
44
|
+
*
|
|
45
|
+
* native 层在遇到未识别的 siid/piid 时可能整体丢弃返回项,导致返回数组比入参数组短,
|
|
46
|
+
* 或对应位置为 undefined。这里按 props 长度做对齐,未命中位置补 PROP_NOT_FOUND 占位,
|
|
47
|
+
* 避免上层 `results.forEach(r => r.code)` 抛 TypeError。
|
|
48
|
+
*/
|
|
49
|
+
function alignResultsToProps(props, rawResults) {
|
|
50
|
+
const arr = Array.isArray(rawResults) ? rawResults : [];
|
|
51
|
+
return props.map((p, i) => {
|
|
52
|
+
const r = arr[i];
|
|
53
|
+
if (r && typeof r === 'object') {
|
|
54
|
+
return r;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
siid: p.siid,
|
|
58
|
+
piid: p.piid,
|
|
59
|
+
code: PROP_NOT_FOUND,
|
|
60
|
+
message: 'native returned no result'
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
/**
|
|
42
66
|
* 读取属性值
|
|
43
67
|
*
|
|
@@ -108,43 +132,31 @@ export function getProps(props, options = {}) {
|
|
|
108
132
|
|
|
109
133
|
// 从源获取数据并更新缓存
|
|
110
134
|
const fetchFromSourceAndUpdateCache = cachedData => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
return results.map(r => ({
|
|
123
|
-
...r,
|
|
124
|
-
specDataSource: r.specDataSource || CACHE_SOURCE.GET
|
|
125
|
-
}));
|
|
135
|
+
const runFetch = () => spec.getProps(props).then(rawResults => {
|
|
136
|
+
// 与入参对齐,缺失位置补 PROP_NOT_FOUND 占位,避免 undefined 传导到上层
|
|
137
|
+
const results = alignResultsToProps(props, rawResults);
|
|
138
|
+
return ensureCache().then(() => {
|
|
139
|
+
results.forEach(result => {
|
|
140
|
+
if (result && result.code === OK && result.value !== undefined) {
|
|
141
|
+
const key = specCache.getCacheKey(result);
|
|
142
|
+
specCache.put(key, result.value, result.updateTime || Math.floor(Date.now() / 1000), CACHE_SOURCE.GET);
|
|
143
|
+
}
|
|
126
144
|
});
|
|
145
|
+
return results.map(r => ({
|
|
146
|
+
...r,
|
|
147
|
+
specDataSource: r.specDataSource || CACHE_SOURCE.GET
|
|
148
|
+
}));
|
|
127
149
|
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 如果强制刷新或缓存未命中,从源获取
|
|
153
|
+
if (forceRefresh || !cachedData || !useCache) {
|
|
154
|
+
return runFetch();
|
|
128
155
|
}
|
|
129
156
|
|
|
130
157
|
// 如果缓存中有数据,且fromSourceIfNeed为true,仍然从源获取并更新
|
|
131
158
|
if (fromSourceIfNeed && cachedData) {
|
|
132
|
-
return
|
|
133
|
-
// 更新缓存
|
|
134
|
-
return ensureCache().then(() => {
|
|
135
|
-
results.forEach(result => {
|
|
136
|
-
if (result.code === OK && result.value !== undefined) {
|
|
137
|
-
const key = specCache.getCacheKey(result);
|
|
138
|
-
specCache.put(key, result.value, result.updateTime || Math.floor(Date.now() / 1000), CACHE_SOURCE.GET);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
// 返回设备最新数据,缓存仅作为提前通知使用
|
|
142
|
-
return results.map(r => ({
|
|
143
|
-
...r,
|
|
144
|
-
specDataSource: r.specDataSource || CACHE_SOURCE.GET
|
|
145
|
-
}));
|
|
146
|
-
});
|
|
147
|
-
});
|
|
159
|
+
return runFetch();
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
// 直接返回缓存数据
|
|
@@ -338,6 +350,10 @@ export function subscribe(props, listener, options = {}) {
|
|
|
338
350
|
// 受全局开关控制
|
|
339
351
|
updateCache = cacheEnabled // 受全局开关控制
|
|
340
352
|
} = options;
|
|
353
|
+
const subT0 = Date.now();
|
|
354
|
+
const subStage = (name, extra = '') => {
|
|
355
|
+
reportSubLog(`[SPEC-SUB][specs.subscribe][${name}][${new Date().toISOString()}] propCount=${props.length}, cost=${Date.now() - subT0}ms, publishImmediately=${publishImmediately}${extra}`);
|
|
356
|
+
};
|
|
341
357
|
|
|
342
358
|
// 确保缓存已初始化
|
|
343
359
|
const ensureCache = () => {
|
|
@@ -347,9 +363,12 @@ export function subscribe(props, listener, options = {}) {
|
|
|
347
363
|
// 如果需要立即发布当前值,从缓存读取并通知
|
|
348
364
|
const publishFromCache = () => {
|
|
349
365
|
if (!publishImmediately) {
|
|
366
|
+
subStage('cache-skip');
|
|
350
367
|
return Promise.resolve();
|
|
351
368
|
}
|
|
369
|
+
subStage('cache-init-enter');
|
|
352
370
|
return ensureCache().then(() => {
|
|
371
|
+
subStage('cache-init-done');
|
|
353
372
|
// 从缓存读取并通知
|
|
354
373
|
props.forEach(prop => {
|
|
355
374
|
const key = specCache.getCacheKey(prop);
|
|
@@ -391,21 +410,33 @@ export function subscribe(props, listener, options = {}) {
|
|
|
391
410
|
|
|
392
411
|
// 执行订阅
|
|
393
412
|
const executeSubscribe = () => {
|
|
413
|
+
subStage('exec-subscribe-enter');
|
|
394
414
|
return spec.subscribe(props, wrappedListener);
|
|
395
415
|
};
|
|
396
416
|
|
|
417
|
+
// 看门狗:整条链路若长时间不 settle(例如 specCache.init / Host.storage.get 一直 pending),
|
|
418
|
+
// 成功和失败两个分支都不会执行,日志表现为彻底静默。这里用超时把"卡住"这种状态显式打出来。
|
|
419
|
+
const stuckTimer = setTimeout(() => {
|
|
420
|
+
subStage('STUCK', ', 订阅流程超过 8s 未完成,疑似 specCache.init/Host.storage.get 未 settle');
|
|
421
|
+
}, 8000);
|
|
422
|
+
|
|
397
423
|
// 执行流程
|
|
398
424
|
return publishFromCache().then(() => executeSubscribe()).then(result => {
|
|
425
|
+
clearTimeout(stuckTimer);
|
|
426
|
+
subStage('success', `, subscribedPropCount=${result?.props?.length}`);
|
|
399
427
|
// 返回订阅对象,包含取消订阅方法
|
|
400
428
|
return {
|
|
401
429
|
props: result.props,
|
|
402
430
|
unsubscribe: () => {
|
|
431
|
+
reportSubLog(`[SPEC-SUB][specs.unsubscribe][${new Date().toISOString()}] 取消订阅, propCount=${props.length}`);
|
|
403
432
|
if (result && result.unsubscribe) {
|
|
404
433
|
result.unsubscribe();
|
|
405
434
|
}
|
|
406
435
|
}
|
|
407
436
|
};
|
|
408
437
|
}).catch(e => {
|
|
438
|
+
clearTimeout(stuckTimer);
|
|
439
|
+
subStage('fail', `, code=${e?.code}, message=${e?.message}, raw=${JSON.stringify(e)}`);
|
|
409
440
|
console.log('subscribe error:', props, e);
|
|
410
441
|
return Promise.reject(e);
|
|
411
442
|
});
|
|
@@ -18,6 +18,7 @@ import { reduceResults } from "./utils";
|
|
|
18
18
|
import { isSuccess } from "./error-code";
|
|
19
19
|
// 引入std-spec
|
|
20
20
|
import * as stdSpec from "./std-spec";
|
|
21
|
+
import { reportSubLog } from "../../utils/subscribe-log";
|
|
21
22
|
export const ServiceSourceType = {
|
|
22
23
|
WIFI: 1,
|
|
23
24
|
BLE: 2,
|
|
@@ -91,9 +92,14 @@ export function subscribe(props, listener) {
|
|
|
91
92
|
const specSubscribeProps = current(props);
|
|
92
93
|
const ps = specs.map((spec, index) => spec.subscribe(specSubscribeProps[index], listener));
|
|
93
94
|
return Promise.all(ps).then(arr => {
|
|
95
|
+
// arr[0].props 为 undefined 说明底层订阅走了失败分支(被 mapError 转成了 resolve)
|
|
96
|
+
if (!arr[0] || !arr[0].props) {
|
|
97
|
+
reportSubLog(`[SPEC-SUB][spec.subscribe][fail][${new Date().toISOString()}] 底层返回结构异常, propCount=${props.length}, raw=${JSON.stringify(arr[0])}`);
|
|
98
|
+
}
|
|
94
99
|
const result = {
|
|
95
100
|
props: arr[0].props,
|
|
96
101
|
unsubscribe: () => {
|
|
102
|
+
reportSubLog(`[SPEC-SUB][spec.unsubscribe][${new Date().toISOString()}] 取消订阅, subCount=${arr.length}, propCount=${props.length}`);
|
|
97
103
|
subscriptions.delete(result);
|
|
98
104
|
arr.forEach(sub => {
|
|
99
105
|
if (sub && sub.unsubscribe) {
|
|
@@ -3,9 +3,30 @@ import Subject from "./subject";
|
|
|
3
3
|
import { genError, getEventGenerator, mapError } from "./utils";
|
|
4
4
|
import { isTypeOf } from "../../utils/types";
|
|
5
5
|
import { encodeProp, decodeProp, specsToSubscribeKeys, specAccess } from "../../utils/spec";
|
|
6
|
+
import { reportSubLog } from "../../utils/subscribe-log";
|
|
6
7
|
import { OK } from "./error-code";
|
|
7
8
|
const specSubject = new Subject(getEventGenerator('std_spec_subject', encodeProp));
|
|
8
9
|
let receivedMessagesSubscription = null;
|
|
10
|
+
function fillInfo(props) {
|
|
11
|
+
const propsNew = props.filter(element => {
|
|
12
|
+
return element !== null;
|
|
13
|
+
});
|
|
14
|
+
return propsNew.map(prop => {
|
|
15
|
+
const {
|
|
16
|
+
did,
|
|
17
|
+
miid,
|
|
18
|
+
...rest
|
|
19
|
+
} = prop;
|
|
20
|
+
const t = {
|
|
21
|
+
...rest,
|
|
22
|
+
did: did || Device.deviceID
|
|
23
|
+
};
|
|
24
|
+
if (miid) {
|
|
25
|
+
t.miid = miid;
|
|
26
|
+
}
|
|
27
|
+
return t;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
9
30
|
export function getProps(props, chunkFn) {
|
|
10
31
|
// console.log('getProps---props', props);
|
|
11
32
|
// 处理固件不上报的情况,此时须将dataSource 设置为2,强行从固件读取
|
|
@@ -16,7 +37,7 @@ export function getProps(props, chunkFn) {
|
|
|
16
37
|
siid,
|
|
17
38
|
piid,
|
|
18
39
|
did = Device.deviceID,
|
|
19
|
-
dataSource =
|
|
40
|
+
dataSource = 1
|
|
20
41
|
} = prop;
|
|
21
42
|
const p = {
|
|
22
43
|
siid,
|
|
@@ -183,32 +204,14 @@ function subscribeReceivedMessage() {
|
|
|
183
204
|
});
|
|
184
205
|
});
|
|
185
206
|
}
|
|
186
|
-
function fillInfo(props) {
|
|
187
|
-
const propsNew = props.filter(element => {
|
|
188
|
-
return element !== null;
|
|
189
|
-
});
|
|
190
|
-
return propsNew.map(prop => {
|
|
191
|
-
const {
|
|
192
|
-
did,
|
|
193
|
-
miid,
|
|
194
|
-
...rest
|
|
195
|
-
} = prop;
|
|
196
|
-
const t = {
|
|
197
|
-
...rest,
|
|
198
|
-
did: did || Device.deviceID
|
|
199
|
-
};
|
|
200
|
-
if (miid) {
|
|
201
|
-
t.miid = miid;
|
|
202
|
-
}
|
|
203
|
-
return t;
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
207
|
export function subscribe(props, listener) {
|
|
207
208
|
if (!receivedMessagesSubscription) {
|
|
208
209
|
receivedMessagesSubscription = subscribeReceivedMessage();
|
|
209
210
|
}
|
|
210
211
|
props = fillInfo(props);
|
|
211
212
|
const keys = specsToSubscribeKeys(props);
|
|
213
|
+
const t0 = Date.now();
|
|
214
|
+
reportSubLog(`[SPEC-SUB][std-spec.subscribe][enter][${new Date().toISOString()}] 即将调用原生 listenMessages, keyCount=${keys.length}, keys=${JSON.stringify(keys)}`);
|
|
212
215
|
// return Device.getDeviceWifi().subscribeMessages(...keys).then(() => {
|
|
213
216
|
return Device.getDeviceWifi().listenMessages(...keys).then(() => {
|
|
214
217
|
const subscription = specSubject.subscribe(props, listener);
|
|
@@ -218,11 +221,17 @@ export function subscribe(props, listener) {
|
|
|
218
221
|
code: OK
|
|
219
222
|
};
|
|
220
223
|
});
|
|
224
|
+
reportSubLog(`[SPEC-SUB][std-spec.subscribe][success][${new Date().toISOString()}] 原生 listenMessages 成功, cost=${Date.now() - t0}ms, keyCount=${keys.length}`);
|
|
221
225
|
return {
|
|
222
226
|
props: subscribed,
|
|
223
227
|
unsubscribe: () => {
|
|
228
|
+
reportSubLog(`[SPEC-SUB][std-spec.unsubscribe][${new Date().toISOString()}] 取消本地监听, keyCount=${keys.length}, keys=${JSON.stringify(keys)}`);
|
|
224
229
|
subscription && subscription.unsubscribe && subscription.unsubscribe();
|
|
225
230
|
}
|
|
226
231
|
};
|
|
227
|
-
}).catch(
|
|
232
|
+
}).catch(e => {
|
|
233
|
+
// 注意:这里沿用 mapError 的既有行为(返回值而非 rethrow,Promise 仍会 resolve),仅补充失败日志
|
|
234
|
+
reportSubLog(`[SPEC-SUB][std-spec.subscribe][fail][${new Date().toISOString()}] 原生 listenMessages 失败, cost=${Date.now() - t0}ms, keyCount=${keys.length}, code=${e?.code}, message=${e?.message}, raw=${JSON.stringify(e)}`);
|
|
235
|
+
return mapError(props)(e);
|
|
236
|
+
});
|
|
228
237
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare let mockListenMessagesImpl: (...keys: string[]) => Promise<any>;
|
|
2
|
+
declare let mockStorageGetImpl: (key: string) => Promise<any>;
|
|
3
|
+
declare const mockListenMessagesCalls: string[][];
|
|
4
|
+
declare const mockReportLogCalls: Array<{
|
|
5
|
+
model: string;
|
|
6
|
+
line: string;
|
|
7
|
+
}>;
|
|
8
|
+
declare const SPECS: {
|
|
9
|
+
siid: number;
|
|
10
|
+
piid: number;
|
|
11
|
+
access: string[];
|
|
12
|
+
}[];
|
|
13
|
+
declare let logs: string[];
|
|
14
|
+
declare const collect: (...args: unknown[]) => void;
|
|
15
|
+
declare const specSubLogs: (stage: string) => string[];
|
|
16
|
+
declare const isDoubleWritten: (line: string) => boolean;
|
|
17
|
+
declare const expectAllSpecSubReported: () => void;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 复现 spec 订阅的两类静默失败,并验证 [SPEC-SUB] 日志能把它们打出来
|
|
5
|
+
*
|
|
6
|
+
* 1. listenMessages 失败:被 std-spec 的 mapError 转成 resolve,上层看到的是"假成功"
|
|
7
|
+
* 2. Host.storage.get 不 settle:publishFromCache 永不完成,listenMessages 根本不会被调用
|
|
8
|
+
* (对应线上 6 次进插件里 2 次原生零 manageSub 的现象)
|
|
9
|
+
*
|
|
10
|
+
* 注:变量必须带 mock 前缀,否则 jest.mock 的 factory 不允许引用。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* 每个用例都要 resetModules 后拿到全新的模块实例(模块级 cacheEnabled / receivedMessagesSubscription
|
|
14
|
+
都是单例状态),因此必须用惰性 require 而非顶层 import。 */
|
|
15
|
+
/* eslint-disable global-require, @typescript-eslint/no-var-requires */
|
|
16
|
+
|
|
17
|
+
let mockListenMessagesImpl = () => Promise.resolve();
|
|
18
|
+
let mockStorageGetImpl = () => Promise.resolve({});
|
|
19
|
+
const mockListenMessagesCalls = [];
|
|
20
|
+
/* 反馈日志上报的入参记录:console 只有连着调试工具才看得见,
|
|
21
|
+
订阅链路必须同时落到 reportLog 才能被测试同学上传回来。 */
|
|
22
|
+
const mockReportLogCalls = [];
|
|
23
|
+
jest.mock('miot', () => ({
|
|
24
|
+
Device: {
|
|
25
|
+
deviceID: 'test-did',
|
|
26
|
+
model: 'micar.car.kunlun',
|
|
27
|
+
getDeviceWifi: () => ({
|
|
28
|
+
listenMessages: (...keys) => {
|
|
29
|
+
mockListenMessagesCalls.push(keys);
|
|
30
|
+
return mockListenMessagesImpl(...keys);
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
DeviceEvent: {
|
|
35
|
+
deviceReceivedMessages: {
|
|
36
|
+
addListener: () => ({
|
|
37
|
+
remove: () => {}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
// specCache 在模块加载期就读 Service.account.ID 拼 STORAGE_KEY,必须提供
|
|
42
|
+
Service: {
|
|
43
|
+
account: {
|
|
44
|
+
ID: 'test-account'
|
|
45
|
+
},
|
|
46
|
+
smarthome: {
|
|
47
|
+
reportLog: (model, line) => {
|
|
48
|
+
mockReportLogCalls.push({
|
|
49
|
+
model,
|
|
50
|
+
line
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
Host: {
|
|
56
|
+
storage: {
|
|
57
|
+
get: key => mockStorageGetImpl(key),
|
|
58
|
+
set: () => Promise.resolve()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}), {
|
|
62
|
+
virtual: true
|
|
63
|
+
});
|
|
64
|
+
const SPECS = [{
|
|
65
|
+
siid: 6,
|
|
66
|
+
piid: 1,
|
|
67
|
+
access: ['read', 'notify']
|
|
68
|
+
}, {
|
|
69
|
+
siid: 6,
|
|
70
|
+
piid: 8,
|
|
71
|
+
access: ['read', 'notify']
|
|
72
|
+
}];
|
|
73
|
+
let logs = [];
|
|
74
|
+
const collect = (...args) => {
|
|
75
|
+
logs.push(args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' '));
|
|
76
|
+
};
|
|
77
|
+
const specSubLogs = stage => logs.filter(l => l.includes('[SPEC-SUB]') && l.includes(stage));
|
|
78
|
+
|
|
79
|
+
/* reportSubLog 自身的降级提示是 console-only 的,不参与双写校验 */
|
|
80
|
+
const isDoubleWritten = line => line.includes('[SPEC-SUB]') && !line.includes('[reportSubLog]');
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 断言订阅链路日志确实双写到了反馈日志。
|
|
84
|
+
*
|
|
85
|
+
* console 只有连着调试工具才看得见,测试同学复现后捞不到;只有 reportLog 才会落到
|
|
86
|
+
* 可上传的反馈日志里。这里不逐个枚举阶段名,而是校验"凡是打到 console 的
|
|
87
|
+
* [SPEC-SUB] 都必须同时出现在 reportLog 中"——将来新增日志点若漏接 reportSubLog,
|
|
88
|
+
* 会直接被这条断言拦住。
|
|
89
|
+
*/
|
|
90
|
+
const expectAllSpecSubReported = () => {
|
|
91
|
+
const consoleLines = logs.filter(isDoubleWritten);
|
|
92
|
+
const reportedLines = mockReportLogCalls.map(c => c.line);
|
|
93
|
+
expect(consoleLines.length).toBeGreaterThan(0);
|
|
94
|
+
consoleLines.forEach(line => expect(reportedLines).toContain(line));
|
|
95
|
+
// 反馈日志里的 model 必须是设备 model,否则日志归不到设备上
|
|
96
|
+
mockReportLogCalls.forEach(c => expect(c.model).toBe('micar.car.kunlun'));
|
|
97
|
+
};
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
jest.resetModules();
|
|
100
|
+
logs = [];
|
|
101
|
+
mockListenMessagesCalls.length = 0;
|
|
102
|
+
mockReportLogCalls.length = 0;
|
|
103
|
+
mockListenMessagesImpl = () => Promise.resolve();
|
|
104
|
+
mockStorageGetImpl = () => Promise.resolve({});
|
|
105
|
+
jest.spyOn(console, 'log').mockImplementation(collect);
|
|
106
|
+
jest.spyOn(console, 'error').mockImplementation(collect);
|
|
107
|
+
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
108
|
+
});
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
jest.restoreAllMocks();
|
|
111
|
+
jest.useRealTimers();
|
|
112
|
+
});
|
|
113
|
+
describe('订阅成功路径', () => {
|
|
114
|
+
it('打出 cache-init / listenMessages / success 全链路日志,取消订阅也有日志', async () => {
|
|
115
|
+
const {
|
|
116
|
+
subscribe
|
|
117
|
+
} = require("./index");
|
|
118
|
+
const result = await subscribe(SPECS, () => {}, {
|
|
119
|
+
publishImmediately: true
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 原生确实被调到,且两个 notify 属性都进了订阅 key
|
|
123
|
+
expect(mockListenMessagesCalls).toHaveLength(1);
|
|
124
|
+
expect(mockListenMessagesCalls[0]).toHaveLength(2);
|
|
125
|
+
expect(mockListenMessagesCalls[0].every(k => k.startsWith('prop.'))).toBe(true);
|
|
126
|
+
expect(specSubLogs('[cache-init-enter]')).toHaveLength(1);
|
|
127
|
+
expect(specSubLogs('[cache-init-done]')).toHaveLength(1);
|
|
128
|
+
expect(specSubLogs('[exec-subscribe-enter]')).toHaveLength(1);
|
|
129
|
+
expect(specSubLogs('[std-spec.subscribe][enter]')).toHaveLength(1);
|
|
130
|
+
expect(specSubLogs('[std-spec.subscribe][success]')).toHaveLength(1);
|
|
131
|
+
expect(specSubLogs('[specs.subscribe][success]')).toHaveLength(1);
|
|
132
|
+
// 成功路径不应出现任何 fail
|
|
133
|
+
expect(specSubLogs('[fail]')).toHaveLength(0);
|
|
134
|
+
|
|
135
|
+
// 订阅链路的日志必须同时进反馈日志,否则测试同学复现后捞不到
|
|
136
|
+
expectAllSpecSubReported();
|
|
137
|
+
|
|
138
|
+
// 取消订阅:三层各一条
|
|
139
|
+
logs = [];
|
|
140
|
+
result.unsubscribe();
|
|
141
|
+
expect(specSubLogs('[specs.unsubscribe]')).toHaveLength(1);
|
|
142
|
+
expect(specSubLogs('[spec.unsubscribe]')).toHaveLength(1);
|
|
143
|
+
expect(specSubLogs('[std-spec.unsubscribe]')).toHaveLength(1);
|
|
144
|
+
|
|
145
|
+
// 取消订阅这三条同样要能被上传回来
|
|
146
|
+
expectAllSpecSubReported();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('失败形态 1:listenMessages 失败被 mapError 吞掉', () => {
|
|
150
|
+
it('Promise 仍 resolve 成"假成功",但 fail 日志已能定位', async () => {
|
|
151
|
+
mockListenMessagesImpl = () => Promise.reject(Object.assign(new Error('listen timeout'), {
|
|
152
|
+
code: -70012
|
|
153
|
+
}));
|
|
154
|
+
const {
|
|
155
|
+
subscribe
|
|
156
|
+
} = require("./index");
|
|
157
|
+
const result = await subscribe(SPECS, () => {}, {
|
|
158
|
+
publishImmediately: true
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 复现核心:整条链路没有 reject,调用方拿到的是"成功"
|
|
162
|
+
expect(result).toBeDefined();
|
|
163
|
+
expect(result.props).toBeUndefined(); // 结构已损坏——真正的证据
|
|
164
|
+
|
|
165
|
+
// 新日志把失败点钉在 std-spec,并在 spec 层标出结构异常
|
|
166
|
+
const stdFail = specSubLogs('[std-spec.subscribe][fail]');
|
|
167
|
+
expect(stdFail).toHaveLength(1);
|
|
168
|
+
expect(stdFail[0]).toContain('code=-70012');
|
|
169
|
+
expect(stdFail[0]).toContain('listen timeout');
|
|
170
|
+
expect(specSubLogs('[spec.subscribe][fail]')).toHaveLength(1);
|
|
171
|
+
|
|
172
|
+
// index 层仍误报 success(既有行为,日志里 subscribedPropCount=undefined 可辨识)
|
|
173
|
+
const idxSuccess = specSubLogs('[specs.subscribe][success]');
|
|
174
|
+
expect(idxSuccess).toHaveLength(1);
|
|
175
|
+
expect(idxSuccess[0]).toContain('subscribedPropCount=undefined');
|
|
176
|
+
|
|
177
|
+
// 失败分支尤其要能上传回来——这是定位「偶现收不到推送」的唯一现场
|
|
178
|
+
expectAllSpecSubReported();
|
|
179
|
+
expect(mockReportLogCalls.map(c => c.line).join('\n')).toContain('[std-spec.subscribe][fail]');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('失败形态 2:Host.storage.get 不 settle', () => {
|
|
183
|
+
it('listenMessages 永不调用,STUCK 看门狗把静默卡死打出来', async () => {
|
|
184
|
+
jest.useFakeTimers();
|
|
185
|
+
mockStorageGetImpl = () => new Promise(() => {}); // 永不 settle
|
|
186
|
+
|
|
187
|
+
const {
|
|
188
|
+
subscribe
|
|
189
|
+
} = require("./index");
|
|
190
|
+
let settled = false;
|
|
191
|
+
subscribe(SPECS, () => {}, {
|
|
192
|
+
publishImmediately: true
|
|
193
|
+
}).then(() => {
|
|
194
|
+
settled = true;
|
|
195
|
+
}, () => {
|
|
196
|
+
settled = true;
|
|
197
|
+
});
|
|
198
|
+
await Promise.resolve();
|
|
199
|
+
jest.advanceTimersByTime(8000);
|
|
200
|
+
|
|
201
|
+
// 复现线上现象:原生零调用、Promise 不 settle、成功和失败分支都不执行
|
|
202
|
+
expect(mockListenMessagesCalls).toHaveLength(0);
|
|
203
|
+
expect(settled).toBe(false);
|
|
204
|
+
expect(specSubLogs('[specs.subscribe][success]')).toHaveLength(0);
|
|
205
|
+
expect(specSubLogs('[specs.subscribe][fail]')).toHaveLength(0);
|
|
206
|
+
|
|
207
|
+
// 唯一能说明问题的就是看门狗
|
|
208
|
+
const stuck = specSubLogs('[STUCK]');
|
|
209
|
+
expect(stuck).toHaveLength(1);
|
|
210
|
+
expect(stuck[0]).toContain('specCache.init/Host.storage.get');
|
|
211
|
+
|
|
212
|
+
// 卡在 cache-init,没有进到 exec-subscribe
|
|
213
|
+
expect(specSubLogs('[cache-init-enter]')).toHaveLength(1);
|
|
214
|
+
expect(specSubLogs('[cache-init-done]')).toHaveLength(0);
|
|
215
|
+
expect(specSubLogs('[exec-subscribe-enter]')).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { FlatList } from 'react-native';
|
|
4
|
+
import { useTrackScrollProvider } from "./useTrackScrollProvider";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RN FlatList 的 ergonomic wrapper:内部建立 TrackScrollContext。
|
|
8
|
+
*
|
|
9
|
+
* 后续可改用官方 onViewableItemsChanged + viewabilityConfig,绕过 measureInWindow,
|
|
10
|
+
* 性能更好;本期先走统一路径降低复杂度。
|
|
11
|
+
*/
|
|
12
|
+
export function TrackFlatList(props) {
|
|
13
|
+
const tracker = useTrackScrollProvider({
|
|
14
|
+
onScroll: props.onScroll,
|
|
15
|
+
onLayout: props.onLayout,
|
|
16
|
+
onMomentumScrollEnd: props.onMomentumScrollEnd
|
|
17
|
+
});
|
|
18
|
+
return /*#__PURE__*/React.createElement(tracker.Provider, null, /*#__PURE__*/React.createElement(FlatList, _extends({}, props, tracker.scrollProps)));
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export type ScrollListener = () => void;
|
|
3
|
+
export interface MeasurableNode {
|
|
4
|
+
measureInWindow: (cb: (x: number, y: number, width: number, height: number) => void) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface TrackScrollContextValue {
|
|
7
|
+
containerRef: RefObject<MeasurableNode | null>;
|
|
8
|
+
subscribe: (cb: ScrollListener) => () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const TrackScrollContext: import("react").Context<TrackScrollContextValue | null>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ScrollView } from 'react-native';
|
|
4
|
+
import { useTrackScrollProvider } from "./useTrackScrollProvider";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 直接消费 RN ScrollView 的 ergonomic wrapper:内部建立 TrackScrollContext,
|
|
8
|
+
* 业务用法与原生 <ScrollView> 完全一致。
|
|
9
|
+
*
|
|
10
|
+
* 包装层 / SDK 容器(SubpageLayout / PageLayout)不要用本组件——
|
|
11
|
+
* 改用 useTrackScrollProvider 取 scrollProps,把 ref/onScroll 透传到容器暴露的接缝。
|
|
12
|
+
*/
|
|
13
|
+
export function TrackScrollView(props) {
|
|
14
|
+
const tracker = useTrackScrollProvider({
|
|
15
|
+
onScroll: props.onScroll,
|
|
16
|
+
onLayout: props.onLayout,
|
|
17
|
+
onMomentumScrollEnd: props.onMomentumScrollEnd
|
|
18
|
+
});
|
|
19
|
+
return /*#__PURE__*/React.createElement(tracker.Provider, null, /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, tracker.scrollProps)));
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TrackEventType, TrackParams, TrackServiceConfig } from './types';
|
|
2
|
+
export declare function initTrackService(config: TrackServiceConfig): void;
|
|
3
|
+
export declare function getCommonParams(): Record<string, unknown>;
|
|
4
|
+
export declare function setPageRef(ref: string, subRef?: string): void;
|
|
5
|
+
export declare function reportTrack(eventTypes: TrackEventType[], params?: TrackParams): void;
|
|
6
|
+
export declare function getTrackLog(): {
|
|
7
|
+
time: number;
|
|
8
|
+
event: string;
|
|
9
|
+
params: Record<string, unknown>;
|
|
10
|
+
}[];
|