@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
|
@@ -9,11 +9,20 @@ import { useEffect, useRef } from 'react';
|
|
|
9
9
|
import { getProps, setProps, doAction as _doAction, subscribe } from "../service/specs/index.js";
|
|
10
10
|
import { encodeProp } from "../utils/spec.js";
|
|
11
11
|
import { deepEqual } from "../utils/objects.js";
|
|
12
|
+
/* 刻意不带 .js 后缀:本文件其余 '../xxx.js' 导入在单测里都被 jest.mock(..., { virtual: true }) 拦掉了,
|
|
13
|
+
而 subscribe-log 没有被 mock,带 .js 会导致 jest 解析不到真实的 .ts 文件。 */
|
|
14
|
+
import { reportSubLog } from "../utils/subscribe-log";
|
|
12
15
|
|
|
13
16
|
/* eslint-disable no-console */
|
|
14
17
|
|
|
15
18
|
const toKey = encodeProp;
|
|
16
19
|
export { toKey };
|
|
20
|
+
|
|
21
|
+
// 同一个 spec 操作完成后,延迟处理订阅推送,避免固件旧值覆盖乐观更新值
|
|
22
|
+
const DELAY_TIME = 2000;
|
|
23
|
+
|
|
24
|
+
// 同一个 spec 快速连续设置时,首次立即发送,后续合并为最后一次值延迟发送
|
|
25
|
+
const DEBOUNCE_SET_INTERVAL = 1000;
|
|
17
26
|
const TAG = 'GlobalSpecManager';
|
|
18
27
|
const GlobalSpecManagerErrorCode = {
|
|
19
28
|
SUCCESS: 0,
|
|
@@ -49,8 +58,16 @@ const LogFilter = (key, value) => {
|
|
|
49
58
|
return ['service', 'prop', 'event', 'arguments'].includes(key) ? undefined : value;
|
|
50
59
|
};
|
|
51
60
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
61
|
+
// 最近一次真实确认的 spec 值,仅用于乐观更新失败回滚,不放入 zustand state
|
|
62
|
+
const _confirmedState = new Map();
|
|
63
|
+
// 每个 pending key 对应的延迟释放 timer,不放入 zustand state
|
|
64
|
+
const _pendingTimers = new Map();
|
|
65
|
+
// pending 期间暂存的最新 push 值,timer 到期后按策略应用
|
|
66
|
+
const _pendingPushCache = new Map();
|
|
67
|
+
// 每个 key 当前正在进行中的 set 请求计数,用于避免连续 set 时提前释放 pending
|
|
68
|
+
const _pendingSetCounts = new Map();
|
|
69
|
+
// setSpec 防抖状态:{ [key]: { timer, lastSpec } }
|
|
70
|
+
const _debounceState = new Map();
|
|
54
71
|
|
|
55
72
|
/*
|
|
56
73
|
负责管理设备Spec的全局Manager
|
|
@@ -73,6 +90,11 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
73
90
|
*/
|
|
74
91
|
specsState: {},
|
|
75
92
|
/*
|
|
93
|
+
当前处于乐观更新待确认的 key 集合。
|
|
94
|
+
订阅推送在 pending 期间先缓存,不直接覆盖本地乐观值。
|
|
95
|
+
*/
|
|
96
|
+
pendingKeys: new Set(),
|
|
97
|
+
/*
|
|
76
98
|
储存每个spec event 监听器
|
|
77
99
|
*/
|
|
78
100
|
eventListeners: {},
|
|
@@ -109,18 +131,167 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
109
131
|
}));
|
|
110
132
|
};
|
|
111
133
|
},
|
|
134
|
+
/**
|
|
135
|
+
* 延迟从 pendingKeys 移除指定 key。
|
|
136
|
+
* 同一 key 多次调用会重置 timer,以最后一次 set 成功或操作为准。
|
|
137
|
+
*/
|
|
138
|
+
_deferRemovePendingKey: key => {
|
|
139
|
+
const timer = _pendingTimers.get(key);
|
|
140
|
+
if (timer) {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
_pendingTimers.set(key, setTimeout(() => {
|
|
144
|
+
const pendingSetCount = _pendingSetCounts.get(key) || 0;
|
|
145
|
+
if (pendingSetCount > 0) {
|
|
146
|
+
_pendingTimers.delete(key);
|
|
147
|
+
get()._deferRemovePendingKey(key);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const hasCached = _pendingPushCache.has(key);
|
|
151
|
+
set(state => {
|
|
152
|
+
const newPending = new Set(state.pendingKeys);
|
|
153
|
+
newPending.delete(key);
|
|
154
|
+
return {
|
|
155
|
+
pendingKeys: newPending
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
_pendingTimers.delete(key);
|
|
159
|
+
if (hasCached) {
|
|
160
|
+
const cachedSpec = _pendingPushCache.get(key);
|
|
161
|
+
_pendingPushCache.delete(key);
|
|
162
|
+
const latestState = get();
|
|
163
|
+
const isValueChanged = !deepEqual(latestState.specsState[key], cachedSpec.value);
|
|
164
|
+
if (cachedSpec.value != null) {
|
|
165
|
+
const metaInfo = {
|
|
166
|
+
...(latestState.specsMetaInfo[key] || {}),
|
|
167
|
+
updateTime: cachedSpec.updateTime,
|
|
168
|
+
specDataSource: cachedSpec.specDataSource
|
|
169
|
+
};
|
|
170
|
+
_confirmedState.set(key, {
|
|
171
|
+
value: cachedSpec.value,
|
|
172
|
+
metaInfo
|
|
173
|
+
});
|
|
174
|
+
if (isValueChanged) {
|
|
175
|
+
set(state => ({
|
|
176
|
+
specsState: {
|
|
177
|
+
...state.specsState,
|
|
178
|
+
[key]: cachedSpec.value
|
|
179
|
+
},
|
|
180
|
+
specsMetaInfo: {
|
|
181
|
+
...state.specsMetaInfo,
|
|
182
|
+
[key]: metaInfo
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
} else {
|
|
186
|
+
set(state => ({
|
|
187
|
+
specsMetaInfo: {
|
|
188
|
+
...state.specsMetaInfo,
|
|
189
|
+
[key]: metaInfo
|
|
190
|
+
}
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
get().specUpdateListeners.forEach(listener => {
|
|
194
|
+
listener({
|
|
195
|
+
type: 'prop',
|
|
196
|
+
key,
|
|
197
|
+
data: {
|
|
198
|
+
value: cachedSpec.value,
|
|
199
|
+
updateTime: cachedSpec.updateTime,
|
|
200
|
+
specDataSource: cachedSpec.specDataSource
|
|
201
|
+
},
|
|
202
|
+
triggerType: isValueChanged ? 'prop_changed' : 'prop_changed_time_only'
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}, DELAY_TIME));
|
|
208
|
+
},
|
|
209
|
+
_rollbackOptimisticSpec: (key, failedValue) => {
|
|
210
|
+
const latestState = get();
|
|
211
|
+
if (!deepEqual(latestState.specsState[key], failedValue)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const timer = _pendingTimers.get(key);
|
|
215
|
+
if (timer) {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
_pendingTimers.delete(key);
|
|
218
|
+
}
|
|
219
|
+
const hasCachedPush = _pendingPushCache.has(key);
|
|
220
|
+
const confirmed = _confirmedState.get(key);
|
|
221
|
+
let shouldKeepValue = !!confirmed;
|
|
222
|
+
let rollbackValue = confirmed?.value;
|
|
223
|
+
let rollbackMetaInfo = confirmed?.metaInfo;
|
|
224
|
+
if (hasCachedPush) {
|
|
225
|
+
const cachedSpec = _pendingPushCache.get(key);
|
|
226
|
+
shouldKeepValue = true;
|
|
227
|
+
rollbackValue = cachedSpec.value;
|
|
228
|
+
rollbackMetaInfo = {
|
|
229
|
+
...(rollbackMetaInfo || {}),
|
|
230
|
+
updateTime: cachedSpec.updateTime,
|
|
231
|
+
specDataSource: cachedSpec.specDataSource
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
_pendingPushCache.delete(key);
|
|
235
|
+
const nextMetaInfo = shouldKeepValue ? {
|
|
236
|
+
...(rollbackMetaInfo || {}),
|
|
237
|
+
specDataSource: rollbackMetaInfo?.specDataSource || 'rollback'
|
|
238
|
+
} : undefined;
|
|
239
|
+
if (shouldKeepValue) {
|
|
240
|
+
_confirmedState.set(key, {
|
|
241
|
+
value: rollbackValue,
|
|
242
|
+
metaInfo: nextMetaInfo
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
_confirmedState.delete(key);
|
|
246
|
+
}
|
|
247
|
+
set(state => {
|
|
248
|
+
const newPending = new Set(state.pendingKeys);
|
|
249
|
+
const newSpecsState = {
|
|
250
|
+
...state.specsState
|
|
251
|
+
};
|
|
252
|
+
const newMetaInfo = {
|
|
253
|
+
...state.specsMetaInfo
|
|
254
|
+
};
|
|
255
|
+
newPending.delete(key);
|
|
256
|
+
if (shouldKeepValue) {
|
|
257
|
+
newSpecsState[key] = rollbackValue;
|
|
258
|
+
newMetaInfo[key] = nextMetaInfo;
|
|
259
|
+
} else {
|
|
260
|
+
delete newSpecsState[key];
|
|
261
|
+
delete newMetaInfo[key];
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
pendingKeys: newPending,
|
|
265
|
+
specsState: newSpecsState,
|
|
266
|
+
specsMetaInfo: newMetaInfo
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
get().specUpdateListeners.forEach(listener => {
|
|
270
|
+
listener({
|
|
271
|
+
type: 'prop',
|
|
272
|
+
key,
|
|
273
|
+
data: {
|
|
274
|
+
value: rollbackValue,
|
|
275
|
+
updateTime: nextMetaInfo?.updateTime,
|
|
276
|
+
specDataSource: nextMetaInfo?.specDataSource || 'rollback'
|
|
277
|
+
},
|
|
278
|
+
triggerType: 'set'
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
return true;
|
|
282
|
+
},
|
|
112
283
|
/**
|
|
113
284
|
* 主动读取设备Spec属性
|
|
114
285
|
* @async
|
|
115
286
|
* @function fetchSpecs
|
|
116
287
|
* @memberof useGlobalSpecManager
|
|
117
288
|
* @param {Array<object>} specs - Spec对象数组,每个对象必须包含 siid 属性
|
|
118
|
-
* @param {
|
|
289
|
+
* @param {number} [datasource=1] - 数据源:1=优先服务器缓存 2=直接下发rpc(最新值) 3=直接读缓存
|
|
119
290
|
* @param {boolean} [batchUpdate=false] - 是否批量更新状态
|
|
120
291
|
* @returns {Promise<Array<object>>} 返回包含每个Spec读取结果的数组
|
|
121
292
|
* @throws {Error} 当入参无效或读取过程中发生异常时抛出错误
|
|
122
293
|
*/
|
|
123
|
-
fetchSpecs: async (specs,
|
|
294
|
+
fetchSpecs: async (specs, datasource = 1, batchUpdate = false) => {
|
|
124
295
|
// 边界验证:specs 必须是数组且非空
|
|
125
296
|
if (!Array.isArray(specs) || specs.length === 0) {
|
|
126
297
|
console.log(`[STD-LOCK][${TAG}][fetchSpecs][${new Date().toISOString()}] 入参 specs 必须为非空数组`);
|
|
@@ -142,10 +313,8 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
142
313
|
const key = toKey(spec);
|
|
143
314
|
const results = await getProps([{
|
|
144
315
|
...spec,
|
|
145
|
-
|
|
146
|
-
}]
|
|
147
|
-
fromSourceIfNeed: fromSource
|
|
148
|
-
});
|
|
316
|
+
dataSource: datasource
|
|
317
|
+
}]);
|
|
149
318
|
// data manager返回的结果code是0,但value是null,需要两者都判断
|
|
150
319
|
if (results[0]?.code === 0 && results[0]?.value != null) {
|
|
151
320
|
const updateTime = Math.floor(Date.now() / 1000);
|
|
@@ -153,6 +322,14 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
153
322
|
// 非批量模式:立即更新 state
|
|
154
323
|
const specDataSource = results[0]?.specDataSource;
|
|
155
324
|
if (!batchUpdate) {
|
|
325
|
+
_confirmedState.set(key, {
|
|
326
|
+
value: results[0].value,
|
|
327
|
+
metaInfo: {
|
|
328
|
+
...(get().specsMetaInfo[key] || {}),
|
|
329
|
+
updateTime,
|
|
330
|
+
specDataSource
|
|
331
|
+
}
|
|
332
|
+
});
|
|
156
333
|
set(state => ({
|
|
157
334
|
specsState: {
|
|
158
335
|
...state.specsState,
|
|
@@ -209,12 +386,16 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
209
386
|
key: toKey(spec)
|
|
210
387
|
};
|
|
211
388
|
} catch (error) {
|
|
212
|
-
|
|
389
|
+
// Error 对象的属性非枚举,JSON.stringify 会得到 {},改成显式取 message/stack,
|
|
390
|
+
// 同时把 spec 关键字段一并打印,方便定位是哪个 piid 崩掉。
|
|
391
|
+
const errMsg = error?.message || (typeof error === 'string' ? error : String(error));
|
|
392
|
+
const errStack = error?.stack ? `\n${error.stack}` : '';
|
|
393
|
+
console.warn(`[STD-LOCK][${TAG}][fetchSpecs][${new Date().toISOString()}] 读取 spec 异常 ` + `spec={siid:${spec.siid},piid:${spec.piid},pkey:${spec.pkey || '-'}}: ${errMsg}${errStack}`);
|
|
213
394
|
return {
|
|
214
395
|
code: GlobalSpecManagerErrorCode.FETCH_REQUEST_FAILED,
|
|
215
396
|
message: 'getProps出现异常',
|
|
216
397
|
rawErrorCode: error?.code,
|
|
217
|
-
rawErrorMessage:
|
|
398
|
+
rawErrorMessage: errMsg
|
|
218
399
|
};
|
|
219
400
|
}
|
|
220
401
|
});
|
|
@@ -224,6 +405,16 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
224
405
|
if (batchUpdate) {
|
|
225
406
|
const successResults = results.filter(r => r.code === 0);
|
|
226
407
|
if (successResults.length > 0) {
|
|
408
|
+
successResults.forEach(res => {
|
|
409
|
+
_confirmedState.set(res.key, {
|
|
410
|
+
value: res.value,
|
|
411
|
+
metaInfo: {
|
|
412
|
+
...(get().specsMetaInfo[res.key] || {}),
|
|
413
|
+
updateTime: res.updateTime,
|
|
414
|
+
specDataSource: res.specDataSource
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
});
|
|
227
418
|
set(state => ({
|
|
228
419
|
specsState: successResults.reduce((acc, res) => {
|
|
229
420
|
acc[res.key] = res.value;
|
|
@@ -287,44 +478,74 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
287
478
|
message: '无效的属性设置对象'
|
|
288
479
|
};
|
|
289
480
|
}
|
|
481
|
+
const key = toKey(spec);
|
|
482
|
+
const value = spec.value;
|
|
483
|
+
const optimisticTime = Math.floor(Date.now() / 1000);
|
|
484
|
+
set(state => {
|
|
485
|
+
const newPending = new Set(state.pendingKeys);
|
|
486
|
+
newPending.add(key);
|
|
487
|
+
return {
|
|
488
|
+
pendingKeys: newPending,
|
|
489
|
+
specsState: {
|
|
490
|
+
...state.specsState,
|
|
491
|
+
[key]: value
|
|
492
|
+
},
|
|
493
|
+
specsMetaInfo: {
|
|
494
|
+
...state.specsMetaInfo,
|
|
495
|
+
[key]: {
|
|
496
|
+
...(state.specsMetaInfo[key] || {}),
|
|
497
|
+
updateTime: optimisticTime,
|
|
498
|
+
specDataSource: 'optimistic'
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
_pendingPushCache.delete(key);
|
|
504
|
+
_pendingSetCounts.set(key, (_pendingSetCounts.get(key) || 0) + 1);
|
|
505
|
+
get()._deferRemovePendingKey(key);
|
|
506
|
+
get().specUpdateListeners.forEach(listener => {
|
|
507
|
+
listener({
|
|
508
|
+
type: 'prop',
|
|
509
|
+
key,
|
|
510
|
+
data: {
|
|
511
|
+
value,
|
|
512
|
+
updateTime: optimisticTime,
|
|
513
|
+
specDataSource: 'optimistic'
|
|
514
|
+
},
|
|
515
|
+
triggerType: 'set'
|
|
516
|
+
});
|
|
517
|
+
});
|
|
290
518
|
try {
|
|
291
|
-
const key = toKey(spec);
|
|
292
|
-
const value = spec.value;
|
|
293
519
|
const results = await setProps([spec]);
|
|
294
520
|
if (results[0]?.code === 0) {
|
|
295
521
|
const updateTime = Math.floor(Date.now() / 1000);
|
|
296
522
|
const specDataSource = results[0]?.specDataSource;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
523
|
+
const nextCount = Math.max((_pendingSetCounts.get(key) || 1) - 1, 0);
|
|
524
|
+
if (nextCount > 0) {
|
|
525
|
+
_pendingSetCounts.set(key, nextCount);
|
|
526
|
+
} else {
|
|
527
|
+
_pendingSetCounts.delete(key);
|
|
528
|
+
}
|
|
529
|
+
_confirmedState.set(key, {
|
|
530
|
+
value,
|
|
531
|
+
metaInfo: {
|
|
532
|
+
...(get().specsMetaInfo[key] || {}),
|
|
533
|
+
updateTime,
|
|
534
|
+
specDataSource
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
set(state => ({
|
|
538
|
+
specsMetaInfo: {
|
|
539
|
+
...state.specsMetaInfo,
|
|
540
|
+
[key]: {
|
|
541
|
+
...(state.specsMetaInfo[key] || {}),
|
|
542
|
+
updateTime,
|
|
543
|
+
specDataSource
|
|
312
544
|
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
listener({
|
|
318
|
-
type: 'prop',
|
|
319
|
-
key,
|
|
320
|
-
data: {
|
|
321
|
-
value,
|
|
322
|
-
updateTime,
|
|
323
|
-
specDataSource
|
|
324
|
-
},
|
|
325
|
-
triggerType: 'set'
|
|
326
|
-
});
|
|
327
|
-
});
|
|
545
|
+
}
|
|
546
|
+
}));
|
|
547
|
+
get()._deferRemovePendingKey(key);
|
|
548
|
+
if (!batchUpdate) {
|
|
328
549
|
return {
|
|
329
550
|
code: GlobalSpecManagerErrorCode.SUCCESS,
|
|
330
551
|
message: '设置成功',
|
|
@@ -335,8 +556,6 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
335
556
|
}
|
|
336
557
|
};
|
|
337
558
|
}
|
|
338
|
-
|
|
339
|
-
// 批量模式:仅收集结果不更新 state
|
|
340
559
|
return {
|
|
341
560
|
code: GlobalSpecManagerErrorCode.SUCCESS,
|
|
342
561
|
key,
|
|
@@ -347,6 +566,15 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
347
566
|
};
|
|
348
567
|
}
|
|
349
568
|
console.log(`[STD-LOCK][${TAG}][setSpecs][${new Date().toISOString()}] 设置 spec 失败: ${JSON.stringify(results[0], LogFilter, 2)}`);
|
|
569
|
+
const nextCount = Math.max((_pendingSetCounts.get(key) || 1) - 1, 0);
|
|
570
|
+
if (nextCount > 0) {
|
|
571
|
+
_pendingSetCounts.set(key, nextCount);
|
|
572
|
+
} else {
|
|
573
|
+
_pendingSetCounts.delete(key);
|
|
574
|
+
if (!get()._rollbackOptimisticSpec(key, value)) {
|
|
575
|
+
get()._deferRemovePendingKey(key);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
350
578
|
return {
|
|
351
579
|
code: GlobalSpecManagerErrorCode.SET_REQUEST_FAILED,
|
|
352
580
|
message: 'setProps失败',
|
|
@@ -355,6 +583,15 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
355
583
|
};
|
|
356
584
|
} catch (error) {
|
|
357
585
|
console.error(`[STD-LOCK][${TAG}][setSpecs][${new Date().toISOString()}] 设置 spec 异常: ${JSON.stringify(error, LogFilter, 2)}`);
|
|
586
|
+
const nextCount = Math.max((_pendingSetCounts.get(key) || 1) - 1, 0);
|
|
587
|
+
if (nextCount > 0) {
|
|
588
|
+
_pendingSetCounts.set(key, nextCount);
|
|
589
|
+
} else {
|
|
590
|
+
_pendingSetCounts.delete(key);
|
|
591
|
+
if (!get()._rollbackOptimisticSpec(key, value)) {
|
|
592
|
+
get()._deferRemovePendingKey(key);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
358
595
|
return {
|
|
359
596
|
code: GlobalSpecManagerErrorCode.SET_REQUEST_FAILED,
|
|
360
597
|
message: 'setProps出现异常',
|
|
@@ -369,6 +606,16 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
369
606
|
if (batchUpdate) {
|
|
370
607
|
const successResults = results.filter(r => r.code === 0);
|
|
371
608
|
if (successResults.length > 0) {
|
|
609
|
+
successResults.forEach(res => {
|
|
610
|
+
_confirmedState.set(res.key, {
|
|
611
|
+
value: res.value,
|
|
612
|
+
metaInfo: {
|
|
613
|
+
...(get().specsMetaInfo[res.key] || {}),
|
|
614
|
+
updateTime: res.updateTime,
|
|
615
|
+
specDataSource: res.specDataSource
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
});
|
|
372
619
|
set(state => ({
|
|
373
620
|
specsState: successResults.reduce((acc, res) => {
|
|
374
621
|
acc[res.key] = res.value;
|
|
@@ -379,7 +626,8 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
379
626
|
specsMetaInfo: successResults.reduce((acc, res) => {
|
|
380
627
|
acc[res.key] = {
|
|
381
628
|
...(state.specsMetaInfo[res.key] || {}),
|
|
382
|
-
updateTime: res.updateTime
|
|
629
|
+
updateTime: res.updateTime,
|
|
630
|
+
specDataSource: res.specDataSource
|
|
383
631
|
};
|
|
384
632
|
return acc;
|
|
385
633
|
}, {
|
|
@@ -422,23 +670,73 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
422
670
|
message: '入参必须为有效的spec对象'
|
|
423
671
|
};
|
|
424
672
|
}
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
673
|
+
const key = toKey(specToSet);
|
|
674
|
+
const existing = _debounceState.get(key);
|
|
675
|
+
if (!existing) {
|
|
676
|
+
console.log(`[STD-LOCK][${TAG}][setSpec][${new Date().toISOString()}] 防抖首次立即发送 key=${key}, value=${JSON.stringify(specToSet.value)}`);
|
|
677
|
+
const timer = setTimeout(() => {
|
|
678
|
+
const state = _debounceState.get(key);
|
|
679
|
+
if (state && state.lastSpec) {
|
|
680
|
+
const trailingSpec = state.lastSpec;
|
|
681
|
+
_debounceState.delete(key);
|
|
682
|
+
get().setSpecs([trailingSpec], false);
|
|
683
|
+
} else {
|
|
684
|
+
_debounceState.delete(key);
|
|
685
|
+
}
|
|
686
|
+
}, DEBOUNCE_SET_INTERVAL);
|
|
687
|
+
_debounceState.set(key, {
|
|
688
|
+
timer,
|
|
689
|
+
lastSpec: null
|
|
690
|
+
});
|
|
691
|
+
try {
|
|
692
|
+
const results = await get().setSpecs([specToSet], false);
|
|
693
|
+
return results[0];
|
|
694
|
+
} catch (error) {
|
|
695
|
+
return {
|
|
696
|
+
code: GlobalSpecManagerErrorCode.SET_REQUEST_FAILED,
|
|
697
|
+
message: 'setSpec leading 异常',
|
|
698
|
+
rawErrorCode: error?.code,
|
|
699
|
+
rawErrorMessage: error?.message
|
|
700
|
+
};
|
|
701
|
+
}
|
|
435
702
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
703
|
+
console.log(`[STD-LOCK][${TAG}][setSpec][${new Date().toISOString()}] 防抖缓存更新 key=${key}, value=${JSON.stringify(specToSet.value)}`);
|
|
704
|
+
_pendingPushCache.delete(key);
|
|
705
|
+
get()._deferRemovePendingKey(key);
|
|
706
|
+
existing.lastSpec = specToSet;
|
|
707
|
+
clearTimeout(existing.timer);
|
|
708
|
+
existing.timer = setTimeout(() => {
|
|
709
|
+
const state = _debounceState.get(key);
|
|
710
|
+
if (state && state.lastSpec) {
|
|
711
|
+
const trailingSpec = state.lastSpec;
|
|
712
|
+
_debounceState.delete(key);
|
|
713
|
+
get().setSpecs([trailingSpec], false);
|
|
714
|
+
} else {
|
|
715
|
+
_debounceState.delete(key);
|
|
716
|
+
}
|
|
717
|
+
}, DEBOUNCE_SET_INTERVAL);
|
|
718
|
+
set(state => ({
|
|
719
|
+
specsState: {
|
|
720
|
+
...state.specsState,
|
|
721
|
+
[key]: specToSet.value
|
|
722
|
+
}
|
|
723
|
+
}));
|
|
724
|
+
get().specUpdateListeners.forEach(listener => {
|
|
725
|
+
listener({
|
|
726
|
+
type: 'prop',
|
|
727
|
+
key,
|
|
728
|
+
data: {
|
|
729
|
+
value: specToSet.value
|
|
730
|
+
},
|
|
731
|
+
triggerType: 'set'
|
|
732
|
+
});
|
|
733
|
+
});
|
|
439
734
|
return {
|
|
440
|
-
code:
|
|
441
|
-
message: '
|
|
735
|
+
code: GlobalSpecManagerErrorCode.SUCCESS,
|
|
736
|
+
message: '已缓存,等待防抖发送',
|
|
737
|
+
data: {
|
|
738
|
+
value: specToSet.value
|
|
739
|
+
}
|
|
442
740
|
};
|
|
443
741
|
},
|
|
444
742
|
/**
|
|
@@ -558,6 +856,18 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
558
856
|
});
|
|
559
857
|
});
|
|
560
858
|
} else {
|
|
859
|
+
// 同一个 spec 正在乐观更新待确认时,先缓存推送值,延迟到 pending 释放后再判断是否应用。
|
|
860
|
+
if (latestState.pendingKeys.has(key)) {
|
|
861
|
+
if (updatedSpec.value != null) {
|
|
862
|
+
_pendingPushCache.set(key, {
|
|
863
|
+
value: updatedSpec.value,
|
|
864
|
+
updateTime,
|
|
865
|
+
specDataSource: updatedSpec.specDataSource
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
561
871
|
// 属性:根据更新时间和值变化决定是否更新状态
|
|
562
872
|
const localUpdateTime = latestState.specsMetaInfo[key]?.updateTime || 0; // 初始状态默认旧时间为0
|
|
563
873
|
// 这里对于updateTime相等的情况,需要处理,因为设备时间戳是秒级
|
|
@@ -574,6 +884,14 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
574
884
|
if (isValueChanged) {
|
|
575
885
|
// 值变化:更新值和时间
|
|
576
886
|
console.log(`[STD-LOCK][${TAG}][subscribeSpecs][${new Date().toISOString()}] 订阅到的 spec 推送::属性变更 ${JSON.stringify(updatedSpec, LogFilter, 2)}`);
|
|
887
|
+
_confirmedState.set(key, {
|
|
888
|
+
value: updatedSpec.value,
|
|
889
|
+
metaInfo: {
|
|
890
|
+
...(get().specsMetaInfo[key] || {}),
|
|
891
|
+
updateTime,
|
|
892
|
+
specDataSource
|
|
893
|
+
}
|
|
894
|
+
});
|
|
577
895
|
set(state => ({
|
|
578
896
|
specsState: {
|
|
579
897
|
...state.specsState,
|
|
@@ -604,6 +922,14 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
604
922
|
} else {
|
|
605
923
|
// 值未变化:仅更新时间
|
|
606
924
|
console.log(`[STD-LOCK][${TAG}][subscribeSpecs][${new Date().toISOString()}] 订阅到的 spec 推送::仅更新时间 ${JSON.stringify(updatedSpec, LogFilter, 2)}`);
|
|
925
|
+
_confirmedState.set(key, {
|
|
926
|
+
value: updatedSpec.value,
|
|
927
|
+
metaInfo: {
|
|
928
|
+
...(get().specsMetaInfo[key] || {}),
|
|
929
|
+
updateTime,
|
|
930
|
+
specDataSource
|
|
931
|
+
}
|
|
932
|
+
});
|
|
607
933
|
set(state => ({
|
|
608
934
|
specsMetaInfo: {
|
|
609
935
|
...state.specsMetaInfo,
|
|
@@ -635,6 +961,16 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
635
961
|
}, {
|
|
636
962
|
publishImmediately: true
|
|
637
963
|
});
|
|
964
|
+
// 底层 std-spec 的 listenMessages 失败时,mapError 会让 Promise 变成 resolve,
|
|
965
|
+
// 且 resolve 出来的是 prop 数组而非 { props, unsubscribe },导致此处拿到的 props 为 undefined。
|
|
966
|
+
// 这种"假成功"过去完全静默,这里显式识别并打日志。
|
|
967
|
+
const isSubscribeSuspicious = !subscription || typeof subscription.unsubscribe !== 'function' || !subscription.props;
|
|
968
|
+
if (isSubscribeSuspicious) {
|
|
969
|
+
reportSubLog(`[SPEC-SUB][${TAG}][subscribeSpecs][fail][${new Date().toISOString()}] 订阅未真正生效(底层返回结构异常,推送将收不到), specCount=${newSpecs.length}, result=${JSON.stringify(subscription, LogFilter, 2)}`, 'error');
|
|
970
|
+
} else {
|
|
971
|
+
reportSubLog(`[SPEC-SUB][${TAG}][subscribeSpecs][success][${new Date().toISOString()}] 订阅成功, specCount=${newSpecs.length}, subscribedPropCount=${subscription.props?.length}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
638
974
|
// 记录新订阅的 spec key
|
|
639
975
|
newSpecs.forEach(spec => {
|
|
640
976
|
const key = toKey(spec);
|
|
@@ -649,6 +985,7 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
649
985
|
|
|
650
986
|
// 返回取消订阅函数(清除订阅记录)
|
|
651
987
|
return () => {
|
|
988
|
+
reportSubLog(`[SPEC-SUB][${TAG}][subscribeSpecs][unsubscribe][${new Date().toISOString()}] 取消订阅, specCount=${newSpecs.length}, keys=${JSON.stringify(newSpecs.map(toKey))}`);
|
|
652
989
|
subscription.unsubscribe();
|
|
653
990
|
newSpecs.forEach(spec => {
|
|
654
991
|
const key = toKey(spec);
|
|
@@ -660,9 +997,11 @@ const useGlobalSpecManager = create((set, get) => ({
|
|
|
660
997
|
};
|
|
661
998
|
});
|
|
662
999
|
});
|
|
1000
|
+
reportSubLog(`[SPEC-SUB][${TAG}][subscribeSpecs][unsubscribe-done][${new Date().toISOString()}] 取消订阅完成, 剩余已订阅=${get().subscribedSpecs.size}`);
|
|
663
1001
|
};
|
|
664
1002
|
} catch (error) {
|
|
665
1003
|
console.error(`[STD-LOCK][${TAG}][subscribeSpecs][${new Date().toISOString()}] 订阅异常: ${JSON.stringify(error, null, 2)}`);
|
|
1004
|
+
reportSubLog(`[SPEC-SUB][${TAG}][subscribeSpecs][fail][${new Date().toISOString()}] 订阅抛异常, specCount=${specs.length}, code=${error?.code}, message=${error?.message}`, 'error');
|
|
666
1005
|
return () => {};
|
|
667
1006
|
}
|
|
668
1007
|
},
|
|
@@ -799,11 +1138,11 @@ export const addSpecUpdateListener = listener => {
|
|
|
799
1138
|
/**
|
|
800
1139
|
* 读取设备 spec
|
|
801
1140
|
* @param specs Spec 对象数组
|
|
802
|
-
* @param
|
|
1141
|
+
* @param datasource 数据源:1=优先服务器缓存 2=直接下发rpc(最新值) 3=直接读缓存
|
|
803
1142
|
* @param batchUpdate 是否批量更新状态
|
|
804
1143
|
*/
|
|
805
|
-
export const fetchSpecs = async (specs,
|
|
806
|
-
return useGlobalSpecManager.getState().fetchSpecs(specs,
|
|
1144
|
+
export const fetchSpecs = async (specs, datasource = 1, batchUpdate = false) => {
|
|
1145
|
+
return useGlobalSpecManager.getState().fetchSpecs(specs, datasource, batchUpdate);
|
|
807
1146
|
};
|
|
808
1147
|
|
|
809
1148
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|