useOfflineBookingCachePolling.js
11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/**
* @description: 轮询离线预约缓存
*/
import Taro from '@tarojs/taro'
import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache'
import { get_network_type, is_usable_network } from '@/utils/network'
/**
* @description: 轮询状态
* @typedef {Object} PollingState
* @property {Number} timer_id 轮询定时器id
* @property {Boolean} running 是否正在轮询
* @property {Boolean} in_flight 是否正在刷新
* @property {Number} ref_count 引用计数
* @property {Boolean} app_enabled 是否启用应用
* @property {Object} last_options 最后一次选项
* @property {Boolean} network_usable 网络可用性
* @property {Boolean} has_network_listener 是否已注册网络监听器
* @property {Function} network_listener 网络监听器
* @property {Promise} network_listener_promise 网络监听器Promise
*/
/** @type {PollingState} */
const polling_state = {
timer_id: null, // 轮询定时器id
running: false, // 是否正在轮询
in_flight: false, // 是否正在刷新
ref_count: 0, // 引用计数
app_enabled: false, // 是否启用应用
last_options: null, // 最后一次选项
network_usable: null, // 网络可用性
has_network_listener: false, // 是否已注册网络监听器
network_listener: null, // 网络监听器
network_listener_promise: null, // 网络监听器Promise
}
/**
* @description: 缓存最后一次 options(用于网络恢复时重启轮询)
* @param {Object} options 选项
* @return {Object} 最后一次选项
*/
const normalize_options = (options) => {
polling_state.last_options = options || polling_state.last_options || {}
return polling_state.last_options
}
/**
* 这是异步编程中典型的飞行状态锁(In-Flight Lock) 模式,是异步防重的核心思维落地方式;
* 核心逻辑:执行前 “上锁” 标记 → 执行异步操作 → 无论成败都 “解锁” 重置标记,从根源避免重复执行;
* finally 块是关键保障:防止异步操作报错导致 “永久上锁”,确保后续调用能正常执行。
*/
/**
* @description: 刷新离线预约缓存一次
* @param {Object} options 选项
* @param {Boolean} options.force 是否强制刷新
*/
const run_refresh_once = async (options) => {
if (polling_state.in_flight) return // 核心防重复——如果正在刷新,直接返回
// 标记为“正在刷新”
polling_state.in_flight = true
try {
await refresh_offline_booking_cache({ force: !!options?.force })
} finally {
// 刷新完成后,标记为“刷新完成”
polling_state.in_flight = false
}
}
/**
* @description: 更新网络可用性
*/
const update_network_usable = async () => {
const type = await get_network_type()
polling_state.network_usable = is_usable_network(type)
}
/**
* @description: 判断是否需要运行轮询
* @return {Boolean} 是否需要运行轮询
*/
const should_run_polling = () => {
if (polling_state.ref_count <= 0) return false
if (polling_state.network_usable === false) return false
if (polling_state.network_usable === null) return false
return true
}
/**
* @description: 确保网络监听器已注册
* @return {Promise} 网络监听器Promise
*/
const ensure_network_listener = async () => {
/**
* 代码优先通过两个条件判断避免重复执行监听器逻辑
* 1. 有已注册的监听器直接返回
* 2. 有未完成的注册 Promise 则直接返回
*/
if (polling_state.has_network_listener) {
await update_network_usable()
return
}
if (polling_state.network_listener_promise) {
await polling_state.network_listener_promise
return
}
// 立即执行异步的监听器注册流程(标记状态→更新网络可用性→定义回调→注册监听)
polling_state.network_listener_promise = (async () => {
// 标记已注册网络监听器
polling_state.has_network_listener = true
// 初始化时更新网络可用性
await update_network_usable()
// 网络状态变化监听器, 网络状态变化时的处理逻辑,此时只是定义,不会立即执行
polling_state.network_listener = (res) => {
const is_connected = res?.isConnected !== false
const type = res?.networkType || 'unknown'
polling_state.network_usable = is_connected && is_usable_network(type)
// 网络不可用时,停止轮询
if (!polling_state.network_usable) {
stop_offline_booking_cache_polling()
return
}
if (should_run_polling()) {
start_offline_booking_cache_polling(polling_state.last_options || {})
}
}
try {
// 注册网络状态变化监听器
Taro.onNetworkStatusChange(polling_state.network_listener)
} catch (e) {
polling_state.has_network_listener = false
polling_state.network_listener = null
polling_state.network_usable = null
console.error('注册网络监听失败:', e)
}
})()
try {
// 等待网络监听器初始化完成, Taro.onNetworkStatusChange
await polling_state.network_listener_promise
} finally {
// 等待注册流程完成后,强制清空 Promise 缓存(finally 块),保证下次执行逻辑时状态干净。
polling_state.network_listener_promise = null
}
}
/**
* @description: 注销网络监听器
* 涉及字段:
* - has_network_listener:是否有注册网络监听器
* - ref_count:引用计数
* - network_listener:网络状态变化监听器
* - network_usable:网络可用性状态
*/
const teardown_network_listener = () => {
// 1. 前置校验:避免无效执行
// 如果没有注册网络监听器,直接返回
if (!polling_state.has_network_listener) return
// 如果有引用计数,说明有其他地方在使用轮询,不能注销监听器
if (polling_state.ref_count > 0) return
// 标记监听器已注销(核心状态更新)
polling_state.has_network_listener = false
// 解绑框架层面的监听器
if (polling_state.network_listener && typeof Taro.offNetworkStatusChange === 'function') {
try {
Taro.offNetworkStatusChange(polling_state.network_listener)
} catch (e) {
// 捕获解绑失败的异常(比如监听器已被手动解绑)
console.warn('注销网络监听器失败:', e)
}
}
// 手动清空本地引用(关键!无论解绑成功/失败都要做)
// 注销后,清空网络监听器引用,确保后续调用能正常工作
polling_state.network_listener = null
/**
* 核心目的:清空 network_usable = null 是为了让状态和监听器的生命周期完全同步 —— 监听器注销后,其产生的网络状态也必须失效,避免 “无监听器却有状态” 的矛盾;
* 关键作用:通过让 should_run_polling() 直接返回 false,杜绝基于过期状态启动轮询的可能;
* 设计思维:体现了 “状态闭环” 的工程化思想 —— 任何状态都要有明确的产生、更新、销毁逻辑,不残留 “脏数据” 干扰后续流程。
*/
// 清空网络可用性状态,确保后续判断逻辑能正常工作
// 清空衍生状态,避免脏数据
polling_state.network_usable = null
}
/**
* @description: 启动离线预约缓存轮询
* @param {Object} options 选项
* @param {Number} options.interval_ms 轮询间隔,单位毫秒
* @param {Boolean} options.immediate 是否立即刷新一次
* @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache)
*/
const start_offline_booking_cache_polling = (options) => {
normalize_options(options)
if (!should_run_polling()) return // 不满足轮询条件直接返回
const interval_ms = Number(options?.interval_ms || 60000)
if (polling_state.running) return // 核心防重复——如果已经在轮询,直接返回
polling_state.running = true // 标记为“正在轮询”
// 立即刷新一次,确保轮询开始时数据是最新的
if (options?.immediate !== false) {
run_refresh_once(options)
}
// 启动轮询定时器,按照指定间隔执行刷新操作
polling_state.timer_id = setInterval(() => {
run_refresh_once(options)
}, interval_ms)
}
/**
* @description: 停止离线预约缓存轮询
*/
const stop_offline_booking_cache_polling = () => {
if (polling_state.timer_id) {
clearInterval(polling_state.timer_id)
polling_state.timer_id = null
}
polling_state.running = false
}
/**
* 引用计数的核心作用
* 这两个函数实现了轮询功能的 “引用计数式资源管理”,本质是追踪有多少 “使用者 / 场景” 依赖这个轮询功能,从而决定是否启动 / 维持 / 停止轮询、注册 / 注销网络监听器,
* 核心目的是:
* - 避免轮询被重复启动、错误停止
* - 防止无使用者时仍占用资源(定时器、网络监听器)
* - 保证多场景共用轮询时的逻辑一致性
*/
/**
* @description: 增加轮询引用计数
* 核心动作:将全局的 ref_count 加 1,代表 “又多了一个场景需要使用轮询功能”。
* @param {Object} options 选项
*/
const acquire_polling_ref = (options) => {
normalize_options(options)
polling_state.ref_count += 1
// 增加引用计数后,确保网络监听器已注册并初始化完成,然后启动轮询
ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {}))
}
/**
* @description: 减少轮询引用计数
* 核心动作:将 ref_count 减 1(且保证不会为负数),代表 “有一个场景不再需要轮询功能”。
*/
const release_polling_ref = () => {
polling_state.ref_count = Math.max(0, polling_state.ref_count - 1)
if (polling_state.ref_count === 0) {
// 引用计数降为0时,停止轮询并注销网络监听器
stop_offline_booking_cache_polling()
teardown_network_listener()
}
}
/**
* @description: 启用离线预约缓存轮询
* @param {Object} options 选项
* @param {Number} options.interval_ms 轮询间隔,单位毫秒
* @param {Boolean} options.immediate 是否立即刷新一次
* @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache)
*/
export const enable_offline_booking_cache_polling = (options) => {
normalize_options(options)
/**
* 核心目的:对 app_enabled=true 的场景做兜底,确保轮询在 “已启用但异常停止” 时能被主动恢复,而非被动等待网络变化;
* 执行逻辑:先保证网络监听器(轮询的依赖)就绪,再尝试启动轮询,且利用 start_offline_booking_cache_polling 的幂等性避免重复;
* 设计思维:体现了 “主动调用需即时生效” 的用户体验考量,以及 “依赖前置检查” 的工程化思维 —— 先保证依赖(监听器)就绪,再执行核心操作(启动轮询)。
*/
if (polling_state.app_enabled) {
ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {}))
return
}
polling_state.app_enabled = true
acquire_polling_ref(polling_state.last_options || {})
}
/**
* @description: 禁用离线预约缓存轮询
*/
export const disable_offline_booking_cache_polling = () => {
if (!polling_state.app_enabled) return
polling_state.app_enabled = false
release_polling_ref()
}