@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.
Files changed (39) hide show
  1. package/dist/service/specs/index.js +63 -32
  2. package/dist/service/specs/spec.js +6 -0
  3. package/dist/service/specs/std-spec.js +31 -22
  4. package/dist/service/specs/subscribe-logging.test.d.ts +17 -0
  5. package/dist/service/specs/subscribe-logging.test.js +217 -0
  6. package/dist/service/track/TrackFlatList.d.ts +3 -0
  7. package/dist/service/track/TrackFlatList.js +19 -0
  8. package/dist/service/track/TrackScrollContext.d.ts +10 -0
  9. package/dist/service/track/TrackScrollContext.js +2 -0
  10. package/dist/service/track/TrackScrollView.d.ts +3 -0
  11. package/dist/service/track/TrackScrollView.js +20 -0
  12. package/dist/service/track/TrackService.d.ts +10 -0
  13. package/dist/service/track/TrackService.js +104 -0
  14. package/dist/service/track/index.d.ts +12 -0
  15. package/dist/service/track/index.js +9 -0
  16. package/dist/service/track/intersect.d.ts +8 -0
  17. package/dist/service/track/intersect.js +15 -0
  18. package/dist/service/track/intersect.test.d.ts +1 -0
  19. package/dist/service/track/intersect.test.js +189 -0
  20. package/dist/service/track/types.d.ts +16 -0
  21. package/dist/service/track/types.js +1 -0
  22. package/dist/service/track/useCardTrack.d.ts +14 -0
  23. package/dist/service/track/useCardTrack.js +61 -0
  24. package/dist/service/track/useExposeOnVisible.d.ts +11 -0
  25. package/dist/service/track/useExposeOnVisible.js +74 -0
  26. package/dist/service/track/usePageTrack.d.ts +7 -0
  27. package/dist/service/track/usePageTrack.js +101 -0
  28. package/dist/service/track/useTrack.d.ts +7 -0
  29. package/dist/service/track/useTrack.js +23 -0
  30. package/dist/service/track/useTrackScrollProvider.d.ts +24 -0
  31. package/dist/service/track/useTrackScrollProvider.js +81 -0
  32. package/dist/specs/instance-parser.js +13 -11
  33. package/dist/store/useGlobalSpecManager.d.ts +5 -2
  34. package/dist/store/useGlobalSpecManager.js +402 -63
  35. package/dist/store/useGlobalSpecManager.test.d.ts +1 -0
  36. package/dist/store/useGlobalSpecManager.test.js +247 -0
  37. package/dist/utils/subscribe-log.d.ts +1 -0
  38. package/dist/utils/subscribe-log.js +37 -0
  39. 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
- // setSpec 节流:以 siid:piid key,500ms 内第一次立即执行,冷却期内丢弃
53
- const _setSpecLastExecTime = {};
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 {boolean} [fromSource=true] - 是否强制从源获取数据
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, fromSource = true, batchUpdate = false) => {
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
- fromSource
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
- console.error(`[STD-LOCK][${TAG}][fetchSpecs][${new Date().toISOString()}] 读取 spec 异常: ${JSON.stringify(error, LogFilter, 2)}`);
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: error?.message
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
- // 非批量模式:立即更新 state
299
- if (!batchUpdate) {
300
- set(state => ({
301
- specsState: {
302
- ...state.specsState,
303
- [key]: value
304
- },
305
- specsMetaInfo: {
306
- ...state.specsMetaInfo,
307
- [key]: {
308
- ...(state.specsMetaInfo[key] || {}),
309
- updateTime,
310
- specDataSource
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
- get().specUpdateListeners.forEach(listener => {
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 throttleKey = `${specToSet.miid}:${specToSet.siid}:${specToSet.piid}`;
426
- const now = Date.now();
427
- const lastExec = _setSpecLastExecTime[throttleKey] || 0;
428
- const elapsed = now - lastExec;
429
- if (elapsed >= 500) {
430
- // 冷却期已过,立即执行
431
- console.log(`[STD-LOCK====][${TAG}][setSpec][throttle][${new Date().toISOString()}] 立即执行 key=${throttleKey}, value=${JSON.stringify(specToSet.value)}, elapsed=${elapsed}ms`);
432
- _setSpecLastExecTime[throttleKey] = now;
433
- const results = await get().setSpecs([specToSet], false);
434
- return results[0];
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
- console.log(`[STD-LOCK====][${TAG}][setSpec][throttle][${new Date().toISOString()}] 被节流丢弃 key=${throttleKey}, value=${JSON.stringify(specToSet.value)}, elapsed=${elapsed}ms, remaining=${500 - elapsed}ms`);
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: 0,
441
- message: 'throttled: dropped'
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 fromSource 是否强制从设备/服务端获取
1141
+ * @param datasource 数据源:1=优先服务器缓存 2=直接下发rpc(最新值) 3=直接读缓存
803
1142
  * @param batchUpdate 是否批量更新状态
804
1143
  */
805
- export const fetchSpecs = async (specs, fromSource = true, batchUpdate = false) => {
806
- return useGlobalSpecManager.getState().fetchSpecs(specs, fromSource, batchUpdate);
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 {};