@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.
- package/dist/service/specs/index.js +63 -32
- package/dist/service/specs/spec.js +6 -0
- package/dist/service/specs/std-spec.js +10 -1
- 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 +3 -0
- package/dist/store/useGlobalSpecManager.js +395 -54
- 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
|
@@ -132,7 +132,7 @@ function getIidsGetter() {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// 获取目标模块
|
|
135
|
-
const targetModule =
|
|
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 =
|
|
230
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
// 如果提供了mDescription,同时匹配mkey和mDescription
|
|
575
|
+
if (parseSpecType(module?.type).key !== normalizedMKey) return false;
|
|
576
|
+
// 提供了 mDescription 时同时匹配 module.description
|
|
574
577
|
if (normalizedMDescription) {
|
|
575
|
-
return
|
|
578
|
+
return safeTrim(module?.description) === normalizedMDescription;
|
|
576
579
|
}
|
|
577
|
-
|
|
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
|
-
//
|
|
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,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
|
-
|
|
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:
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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:
|
|
439
|
-
message: '
|
|
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 {};
|