fix: 缓存击穿/play竞态/onUnload泄漏/清除数据UI/统计NaN
- leaderboard 云函数: _fetchLatestByOpenid 加 in-flight promise,并发请求复用同一次扫描,防缓存击穿 - voice: play 加序列号,丢弃过期播放,避免旧语音截断新语音 - timer: onUnload 补取消 _completionCountUp,系统返回手势不再泄漏动画定时器 - settings: 清除数据 setData 补 voiceGender/vibrateIntensity 重置,避免 UI 与存储不一致 - storage: getTotalStats 对 r.duration 防御(Number||0),避免脏数据产生 NaN 传染 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ const TZ_OFFSET_MS = 8 * 60 * 60 * 1000
|
|||||||
// across instances, which is fine - the board needn't be real-time.
|
// across instances, which is fine - the board needn't be real-time.
|
||||||
const CACHE_TTL = 60 * 1000
|
const CACHE_TTL = 60 * 1000
|
||||||
let _docsCache = null // { latestByOpenid: Map, ts }
|
let _docsCache = null // { latestByOpenid: Map, ts }
|
||||||
|
let _docsCachePromise = null // in-flight 扫描 promise,防缓存击穿
|
||||||
|
|
||||||
const tsOf = (d) => {
|
const tsOf = (d) => {
|
||||||
if (!d.updatedAt) return 0
|
if (!d.updatedAt) return 0
|
||||||
@@ -40,6 +41,9 @@ const _fetchLatestByOpenid = async () => {
|
|||||||
if (_docsCache && (Date.now() - _docsCache.ts) < CACHE_TTL) {
|
if (_docsCache && (Date.now() - _docsCache.ts) < CACHE_TTL) {
|
||||||
return _docsCache.latestByOpenid
|
return _docsCache.latestByOpenid
|
||||||
}
|
}
|
||||||
|
// 缓存过期:复用 in-flight promise,避免并发请求各自全表扫描(缓存击穿)
|
||||||
|
if (_docsCachePromise) return _docsCachePromise
|
||||||
|
_docsCachePromise = (async () => {
|
||||||
const latestByOpenid = new Map()
|
const latestByOpenid = new Map()
|
||||||
// Exclude accounts that haven't synced in 90+ days to bound the scan.
|
// Exclude accounts that haven't synced in 90+ days to bound the scan.
|
||||||
// Docs without an updatedAt field (pre-fix legacy) are included as well.
|
// Docs without an updatedAt field (pre-fix legacy) are included as well.
|
||||||
@@ -66,6 +70,8 @@ const _fetchLatestByOpenid = async () => {
|
|||||||
}
|
}
|
||||||
_docsCache = { latestByOpenid, ts: Date.now() }
|
_docsCache = { latestByOpenid, ts: Date.now() }
|
||||||
return latestByOpenid
|
return latestByOpenid
|
||||||
|
})()
|
||||||
|
try { return await _docsCachePromise } finally { _docsCachePromise = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.main = async (event) => {
|
exports.main = async (event) => {
|
||||||
|
|||||||
@@ -371,6 +371,8 @@ Page({
|
|||||||
currentPlanId: 'beginner',
|
currentPlanId: 'beginner',
|
||||||
voiceGuide: true,
|
voiceGuide: true,
|
||||||
vibrate: true,
|
vibrate: true,
|
||||||
|
voiceGender: 'female',
|
||||||
|
vibrateIntensity: 'heavy',
|
||||||
currentThemeId: 'orange',
|
currentThemeId: 'orange',
|
||||||
nickName: '',
|
nickName: '',
|
||||||
avatarUrl: ''
|
avatarUrl: ''
|
||||||
|
|||||||
@@ -292,6 +292,10 @@ Page({
|
|||||||
this._countUpTask.cancel()
|
this._countUpTask.cancel()
|
||||||
this._countUpTask = null
|
this._countUpTask = null
|
||||||
}
|
}
|
||||||
|
if (this._completionCountUp) {
|
||||||
|
this._completionCountUp.cancel()
|
||||||
|
this._completionCountUp = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onStart() {
|
onStart() {
|
||||||
|
|||||||
+3
-2
@@ -156,9 +156,10 @@ const getTotalStats = () => {
|
|||||||
let maxDuration = 0
|
let maxDuration = 0
|
||||||
Object.keys(records).forEach((key) => {
|
Object.keys(records).forEach((key) => {
|
||||||
records[key].forEach((r) => {
|
records[key].forEach((r) => {
|
||||||
totalDuration += r.duration
|
const d = Number(r && r.duration) || 0
|
||||||
|
totalDuration += d
|
||||||
totalSessions++
|
totalSessions++
|
||||||
if (r.duration > maxDuration) maxDuration = r.duration
|
if (d > maxDuration) maxDuration = d
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return { totalDuration, totalSessions, maxDuration }
|
return { totalDuration, totalSessions, maxDuration }
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const FILEID_CACHE_KEY = 'tts_fileid_cache'
|
|||||||
const LOCAL_CACHE_KEY = 'tts_local_cache' // { cacheKey: savedFilePath } - 持久本地文件,跨重启
|
const LOCAL_CACHE_KEY = 'tts_local_cache' // { cacheKey: savedFilePath } - 持久本地文件,跨重启
|
||||||
|
|
||||||
let _ctx = null
|
let _ctx = null
|
||||||
|
let _playSeq = 0 // play 序列号,丢弃过期播放避免乱序
|
||||||
let _localCache = null // { cacheKey: savedFilePath } - persisted, lazy-loaded
|
let _localCache = null // { cacheKey: savedFilePath } - persisted, lazy-loaded
|
||||||
const _urlCache = {} // { promptKey: audioUrl } — per-session, volatile
|
const _urlCache = {} // { promptKey: audioUrl } — per-session, volatile
|
||||||
let _fileIDCache = null // { promptKey: fileID } — persisted, lazy-loaded
|
let _fileIDCache = null // { promptKey: fileID } — persisted, lazy-loaded
|
||||||
@@ -204,8 +205,11 @@ const preload = (keys) => {
|
|||||||
*/
|
*/
|
||||||
const play = async (promptKey) => {
|
const play = async (promptKey) => {
|
||||||
if (!PROMPTS[promptKey]) return
|
if (!PROMPTS[promptKey]) return
|
||||||
|
const seq = ++_playSeq
|
||||||
const src = await _resolveSrc(promptKey)
|
const src = await _resolveSrc(promptKey)
|
||||||
if (!src) return
|
if (!src) return
|
||||||
|
// 期间若有新的 play 调用,放弃本次,避免旧语音截断新语音
|
||||||
|
if (seq !== _playSeq) return
|
||||||
const ctx = _getCtx()
|
const ctx = _getCtx()
|
||||||
try {
|
try {
|
||||||
ctx.stop()
|
ctx.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user