@miot-rn/common-component 1.0.3 → 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 +10 -1
  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 +3 -0
  34. package/dist/store/useGlobalSpecManager.js +395 -54
  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
@@ -132,7 +132,7 @@ function getIidsGetter() {
132
132
  }
133
133
 
134
134
  // 获取目标模块
135
- const targetModule = findModuleByKeyAndDescription(normalizedMKey, normalizedMDescription);
135
+ const targetModule = findModule(normalizedMKey, normalizedMDescription);
136
136
  // console.log('targetModule---', targetModule);
137
137
 
138
138
  if (!targetModule && mkey) {
@@ -226,8 +226,10 @@ function getIidsGetter() {
226
226
  // 添加模块信息
227
227
  if (mkey && targetModule) {
228
228
  spec.miid = targetModule?.iid;
229
- spec.mkey = mkey || parseSpecType(targetModule?.type).key;
230
- spec.mDescription = normalizedMDescription;
229
+ spec.mkey = parseSpecType(targetModule?.type).key || mkey;
230
+ if (normalizedMDescription) {
231
+ spec.mDescription = normalizedMDescription;
232
+ }
231
233
  }
232
234
 
233
235
  // 将最新版本的结果添加到数组开头
@@ -557,25 +559,25 @@ function getTargetModule(miid) {
557
559
  }
558
560
 
559
561
  /**
560
- * 根据mkey和mDescription查找模块
562
+ * 查找匹配的模块。
563
+ * - mkey 通过 module.type 解析(与 service/property 保持一致口径)
564
+ * - mDescription 仅在提供时用于二次消歧(同 name 存在多个 module 的场景)
561
565
  * @param {string} mkey - 模块键
562
566
  * @param {string} mDescription - 模块描述(可选)
563
567
  * @returns {Object|null} - 返回目标模块对象或null
564
568
  */
565
- function findModuleByKeyAndDescription(mkey, mDescription) {
569
+ function findModule(mkey, mDescription) {
566
570
  const normalizedMKey = safeTrim(mkey);
567
571
  const normalizedMDescription = safeTrim(mDescription);
568
572
  // 如果是版本3
569
573
  if (CachedSpecVersion === 3) {
570
574
  return (CachedInstance.modules || []).find(module => {
571
- const moduleDescription = safeTrim(module?.description);
572
- const moduleName = safeTrim(module?.name);
573
- // 如果提供了mDescription,同时匹配mkey和mDescription
575
+ if (parseSpecType(module?.type).key !== normalizedMKey) return false;
576
+ // 提供了 mDescription 时同时匹配 module.description
574
577
  if (normalizedMDescription) {
575
- return moduleName === normalizedMKey && moduleDescription === normalizedMDescription;
578
+ return safeTrim(module?.description) === normalizedMDescription;
576
579
  }
577
- // 只匹配mkey
578
- return moduleName === normalizedMKey;
580
+ return true;
579
581
  });
580
582
  }
581
583
  // 如果是版本2,返回整个实例
@@ -4,10 +4,13 @@ export { toKey };
4
4
  declare const useGlobalSpecManager: import("zustand").UseBoundStore<import("zustand").StoreApi<{
5
5
  specsMetaInfo: {};
6
6
  specsState: {};
7
+ pendingKeys: Set<unknown>;
7
8
  eventListeners: {};
8
9
  subscribedSpecs: Set<unknown>;
9
10
  specUpdateListeners: never[];
10
11
  addSpecUpdateListener: (listener: any) => () => void;
12
+ _deferRemovePendingKey: (key: string) => void;
13
+ _rollbackOptimisticSpec: (key: string, failedValue: any) => boolean;
11
14
  fetchSpecs: (specs: any, datasource?: number, batchUpdate?: boolean) => Promise<({
12
15
  code: number;
13
16
  message: string;
@@ -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,6 +131,155 @@ 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
@@ -151,6 +322,14 @@ const useGlobalSpecManager = create((set, get) => ({
151
322
  // 非批量模式:立即更新 state
152
323
  const specDataSource = results[0]?.specDataSource;
153
324
  if (!batchUpdate) {
325
+ _confirmedState.set(key, {
326
+ value: results[0].value,
327
+ metaInfo: {
328
+ ...(get().specsMetaInfo[key] || {}),
329
+ updateTime,
330
+ specDataSource
331
+ }
332
+ });
154
333
  set(state => ({
155
334
  specsState: {
156
335
  ...state.specsState,
@@ -207,12 +386,16 @@ const useGlobalSpecManager = create((set, get) => ({
207
386
  key: toKey(spec)
208
387
  };
209
388
  } catch (error) {
210
- 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}`);
211
394
  return {
212
395
  code: GlobalSpecManagerErrorCode.FETCH_REQUEST_FAILED,
213
396
  message: 'getProps出现异常',
214
397
  rawErrorCode: error?.code,
215
- rawErrorMessage: error?.message
398
+ rawErrorMessage: errMsg
216
399
  };
217
400
  }
218
401
  });
@@ -222,6 +405,16 @@ const useGlobalSpecManager = create((set, get) => ({
222
405
  if (batchUpdate) {
223
406
  const successResults = results.filter(r => r.code === 0);
224
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
+ });
225
418
  set(state => ({
226
419
  specsState: successResults.reduce((acc, res) => {
227
420
  acc[res.key] = res.value;
@@ -285,44 +478,74 @@ const useGlobalSpecManager = create((set, get) => ({
285
478
  message: '无效的属性设置对象'
286
479
  };
287
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
+ });
288
518
  try {
289
- const key = toKey(spec);
290
- const value = spec.value;
291
519
  const results = await setProps([spec]);
292
520
  if (results[0]?.code === 0) {
293
521
  const updateTime = Math.floor(Date.now() / 1000);
294
522
  const specDataSource = results[0]?.specDataSource;
295
-
296
- // 非批量模式:立即更新 state
297
- if (!batchUpdate) {
298
- set(state => ({
299
- specsState: {
300
- ...state.specsState,
301
- [key]: value
302
- },
303
- specsMetaInfo: {
304
- ...state.specsMetaInfo,
305
- [key]: {
306
- ...(state.specsMetaInfo[key] || {}),
307
- updateTime,
308
- specDataSource
309
- }
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
310
544
  }
311
- }));
312
-
313
- // 触发监听器
314
- get().specUpdateListeners.forEach(listener => {
315
- listener({
316
- type: 'prop',
317
- key,
318
- data: {
319
- value,
320
- updateTime,
321
- specDataSource
322
- },
323
- triggerType: 'set'
324
- });
325
- });
545
+ }
546
+ }));
547
+ get()._deferRemovePendingKey(key);
548
+ if (!batchUpdate) {
326
549
  return {
327
550
  code: GlobalSpecManagerErrorCode.SUCCESS,
328
551
  message: '设置成功',
@@ -333,8 +556,6 @@ const useGlobalSpecManager = create((set, get) => ({
333
556
  }
334
557
  };
335
558
  }
336
-
337
- // 批量模式:仅收集结果不更新 state
338
559
  return {
339
560
  code: GlobalSpecManagerErrorCode.SUCCESS,
340
561
  key,
@@ -345,6 +566,15 @@ const useGlobalSpecManager = create((set, get) => ({
345
566
  };
346
567
  }
347
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
+ }
348
578
  return {
349
579
  code: GlobalSpecManagerErrorCode.SET_REQUEST_FAILED,
350
580
  message: 'setProps失败',
@@ -353,6 +583,15 @@ const useGlobalSpecManager = create((set, get) => ({
353
583
  };
354
584
  } catch (error) {
355
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
+ }
356
595
  return {
357
596
  code: GlobalSpecManagerErrorCode.SET_REQUEST_FAILED,
358
597
  message: 'setProps出现异常',
@@ -367,6 +606,16 @@ const useGlobalSpecManager = create((set, get) => ({
367
606
  if (batchUpdate) {
368
607
  const successResults = results.filter(r => r.code === 0);
369
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
+ });
370
619
  set(state => ({
371
620
  specsState: successResults.reduce((acc, res) => {
372
621
  acc[res.key] = res.value;
@@ -377,7 +626,8 @@ const useGlobalSpecManager = create((set, get) => ({
377
626
  specsMetaInfo: successResults.reduce((acc, res) => {
378
627
  acc[res.key] = {
379
628
  ...(state.specsMetaInfo[res.key] || {}),
380
- updateTime: res.updateTime
629
+ updateTime: res.updateTime,
630
+ specDataSource: res.specDataSource
381
631
  };
382
632
  return acc;
383
633
  }, {
@@ -420,23 +670,73 @@ const useGlobalSpecManager = create((set, get) => ({
420
670
  message: '入参必须为有效的spec对象'
421
671
  };
422
672
  }
423
- const throttleKey = `${specToSet.miid}:${specToSet.siid}:${specToSet.piid}`;
424
- const now = Date.now();
425
- const lastExec = _setSpecLastExecTime[throttleKey] || 0;
426
- const elapsed = now - lastExec;
427
- if (elapsed >= 500) {
428
- // 冷却期已过,立即执行
429
- console.log(`[STD-LOCK====][${TAG}][setSpec][throttle][${new Date().toISOString()}] 立即执行 key=${throttleKey}, value=${JSON.stringify(specToSet.value)}, elapsed=${elapsed}ms`);
430
- _setSpecLastExecTime[throttleKey] = now;
431
- const results = await get().setSpecs([specToSet], false);
432
- 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
+ }
433
702
  }
434
-
435
- // 冷却期内,直接丢弃
436
- 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
+ });
437
734
  return {
438
- code: 0,
439
- message: 'throttled: dropped'
735
+ code: GlobalSpecManagerErrorCode.SUCCESS,
736
+ message: '已缓存,等待防抖发送',
737
+ data: {
738
+ value: specToSet.value
739
+ }
440
740
  };
441
741
  },
442
742
  /**
@@ -556,6 +856,18 @@ const useGlobalSpecManager = create((set, get) => ({
556
856
  });
557
857
  });
558
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
+
559
871
  // 属性:根据更新时间和值变化决定是否更新状态
560
872
  const localUpdateTime = latestState.specsMetaInfo[key]?.updateTime || 0; // 初始状态默认旧时间为0
561
873
  // 这里对于updateTime相等的情况,需要处理,因为设备时间戳是秒级
@@ -572,6 +884,14 @@ const useGlobalSpecManager = create((set, get) => ({
572
884
  if (isValueChanged) {
573
885
  // 值变化:更新值和时间
574
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
+ });
575
895
  set(state => ({
576
896
  specsState: {
577
897
  ...state.specsState,
@@ -602,6 +922,14 @@ const useGlobalSpecManager = create((set, get) => ({
602
922
  } else {
603
923
  // 值未变化:仅更新时间
604
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
+ });
605
933
  set(state => ({
606
934
  specsMetaInfo: {
607
935
  ...state.specsMetaInfo,
@@ -633,6 +961,16 @@ const useGlobalSpecManager = create((set, get) => ({
633
961
  }, {
634
962
  publishImmediately: true
635
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
+
636
974
  // 记录新订阅的 spec key
637
975
  newSpecs.forEach(spec => {
638
976
  const key = toKey(spec);
@@ -647,6 +985,7 @@ const useGlobalSpecManager = create((set, get) => ({
647
985
 
648
986
  // 返回取消订阅函数(清除订阅记录)
649
987
  return () => {
988
+ reportSubLog(`[SPEC-SUB][${TAG}][subscribeSpecs][unsubscribe][${new Date().toISOString()}] 取消订阅, specCount=${newSpecs.length}, keys=${JSON.stringify(newSpecs.map(toKey))}`);
650
989
  subscription.unsubscribe();
651
990
  newSpecs.forEach(spec => {
652
991
  const key = toKey(spec);
@@ -658,9 +997,11 @@ const useGlobalSpecManager = create((set, get) => ({
658
997
  };
659
998
  });
660
999
  });
1000
+ reportSubLog(`[SPEC-SUB][${TAG}][subscribeSpecs][unsubscribe-done][${new Date().toISOString()}] 取消订阅完成, 剩余已订阅=${get().subscribedSpecs.size}`);
661
1001
  };
662
1002
  } catch (error) {
663
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');
664
1005
  return () => {};
665
1006
  }
666
1007
  },
@@ -0,0 +1 @@
1
+ export {};