Merge pull request #464 from xunchahaha/dev

增加一个导出的缓存
This commit is contained in:
xuncha
2026-03-16 20:16:32 +08:00
committed by GitHub
2 changed files with 353 additions and 16 deletions

View File

@@ -1596,6 +1596,7 @@ function ExportPage() {
const sessionMutualFriendsRunIdRef = useRef(0) const sessionMutualFriendsRunIdRef = useRef(0)
const sessionMutualFriendsWorkerRunningRef = useRef(false) const sessionMutualFriendsWorkerRunningRef = useRef(false)
const sessionMutualFriendsBackgroundFeedTimerRef = useRef<number | null>(null) const sessionMutualFriendsBackgroundFeedTimerRef = useRef<number | null>(null)
const sessionMutualFriendsPersistTimerRef = useRef<number | null>(null)
const sessionMutualFriendsVisibleRangeRef = useRef<{ startIndex: number; endIndex: number }>({ const sessionMutualFriendsVisibleRangeRef = useRef<{ startIndex: number; endIndex: number }>({
startIndex: 0, startIndex: 0,
endIndex: -1 endIndex: -1
@@ -2748,8 +2749,32 @@ function ExportPage() {
window.clearTimeout(sessionMutualFriendsBackgroundFeedTimerRef.current) window.clearTimeout(sessionMutualFriendsBackgroundFeedTimerRef.current)
sessionMutualFriendsBackgroundFeedTimerRef.current = null sessionMutualFriendsBackgroundFeedTimerRef.current = null
} }
if (sessionMutualFriendsPersistTimerRef.current) {
window.clearTimeout(sessionMutualFriendsPersistTimerRef.current)
sessionMutualFriendsPersistTimerRef.current = null
}
}, []) }, [])
const flushSessionMutualFriendsCache = useCallback(async () => {
try {
const scopeKey = await ensureExportCacheScope()
await configService.setExportSessionMutualFriendsCache(
scopeKey,
sessionMutualFriendsDirectMetricsRef.current
)
} catch (error) {
console.error('写入导出页共同好友缓存失败:', error)
}
}, [ensureExportCacheScope])
const scheduleFlushSessionMutualFriendsCache = useCallback(() => {
if (sessionMutualFriendsPersistTimerRef.current) return
sessionMutualFriendsPersistTimerRef.current = window.setTimeout(() => {
sessionMutualFriendsPersistTimerRef.current = null
void flushSessionMutualFriendsCache()
}, SESSION_MEDIA_METRIC_CACHE_FLUSH_DELAY_MS)
}, [flushSessionMutualFriendsCache])
const isSessionMutualFriendsReady = useCallback((sessionId: string): boolean => { const isSessionMutualFriendsReady = useCallback((sessionId: string): boolean => {
if (!sessionId) return true if (!sessionId) return true
if (sessionMutualFriendsReadySetRef.current.has(sessionId)) return true if (sessionMutualFriendsReadySetRef.current.has(sessionId)) return true
@@ -2879,10 +2904,35 @@ function ExportPage() {
} }
}, [getSessionMutualFriendProfile]) }, [getSessionMutualFriendProfile])
const rebuildSessionMutualFriendsStateFromDirectMetrics = useCallback((sessionIds?: string[]) => {
const targets = Array.isArray(sessionIds) && sessionIds.length > 0
? sessionIds
: Object.keys(sessionMutualFriendsDirectMetricsRef.current)
const nextMetrics: Record<string, SessionMutualFriendsMetric> = {}
const readyIds: string[] = []
for (const sessionIdRaw of targets) {
const sessionId = String(sessionIdRaw || '').trim()
if (!sessionId) continue
const rebuilt = rebuildSessionMutualFriendsMetric(sessionId)
if (!rebuilt) continue
nextMetrics[sessionId] = rebuilt
readyIds.push(sessionId)
}
sessionMutualFriendsMetricsRef.current = nextMetrics
setSessionMutualFriendsMetrics(nextMetrics)
if (readyIds.length > 0) {
for (const sessionId of readyIds) {
sessionMutualFriendsReadySetRef.current.add(sessionId)
}
patchSessionLoadTraceStage(readyIds, 'mutualFriends', 'done')
}
}, [patchSessionLoadTraceStage, rebuildSessionMutualFriendsMetric])
const applySessionMutualFriendsMetric = useCallback((sessionId: string, directMetric: SessionMutualFriendsMetric) => { const applySessionMutualFriendsMetric = useCallback((sessionId: string, directMetric: SessionMutualFriendsMetric) => {
const normalizedSessionId = String(sessionId || '').trim() const normalizedSessionId = String(sessionId || '').trim()
if (!normalizedSessionId) return if (!normalizedSessionId) return
sessionMutualFriendsDirectMetricsRef.current[normalizedSessionId] = directMetric sessionMutualFriendsDirectMetricsRef.current[normalizedSessionId] = directMetric
scheduleFlushSessionMutualFriendsCache()
const impactedSessionIds = new Set<string>([normalizedSessionId]) const impactedSessionIds = new Set<string>([normalizedSessionId])
const allSessionIds = sessionsRef.current const allSessionIds = sessionsRef.current
@@ -2912,7 +2962,7 @@ function ExportPage() {
} }
return changed ? next : prev return changed ? next : prev
}) })
}, [getSessionMutualFriendProfile, rebuildSessionMutualFriendsMetric]) }, [getSessionMutualFriendProfile, rebuildSessionMutualFriendsMetric, scheduleFlushSessionMutualFriendsCache])
const isSessionMediaMetricReady = useCallback((sessionId: string): boolean => { const isSessionMediaMetricReady = useCallback((sessionId: string): boolean => {
if (!sessionId) return true if (!sessionId) return true
@@ -3339,11 +3389,13 @@ function ExportPage() {
const [ const [
cachedContactsPayload, cachedContactsPayload,
cachedMessageCountsPayload, cachedMessageCountsPayload,
cachedContentMetricsPayload cachedContentMetricsPayload,
cachedMutualFriendsPayload
] = await Promise.all([ ] = await Promise.all([
loadContactsCaches(scopeKey), loadContactsCaches(scopeKey),
configService.getExportSessionMessageCountCache(scopeKey), configService.getExportSessionMessageCountCache(scopeKey),
configService.getExportSessionContentMetricCache(scopeKey) configService.getExportSessionContentMetricCache(scopeKey),
configService.getExportSessionMutualFriendsCache(scopeKey)
]) ])
if (isStale()) return if (isStale()) return
@@ -3411,6 +3463,15 @@ function ExportPage() {
if (cachedContentMetricReadySessionIds.length > 0) { if (cachedContentMetricReadySessionIds.length > 0) {
patchSessionLoadTraceStage(cachedContentMetricReadySessionIds, 'mediaMetrics', 'done') patchSessionLoadTraceStage(cachedContentMetricReadySessionIds, 'mediaMetrics', 'done')
} }
const cachedMutualFriendDirectMetrics = Object.entries(cachedMutualFriendsPayload?.metrics || {}).reduce<Record<string, SessionMutualFriendsMetric>>((acc, [sessionIdRaw, metricRaw]) => {
const sessionId = String(sessionIdRaw || '').trim()
if (!exportableSessionIdSet.has(sessionId) || !isSingleContactSession(sessionId)) return acc
const metric = metricRaw as SessionMutualFriendsMetric | undefined
if (!metric || !Array.isArray(metric.items) || !Number.isFinite(metric.count)) return acc
acc[sessionId] = metric
return acc
}, {})
const cachedMutualFriendSessionIds = Object.keys(cachedMutualFriendDirectMetrics)
if (isStale()) return if (isStale()) return
if (Object.keys(cachedMessageCounts).length > 0) { if (Object.keys(cachedMessageCounts).length > 0) {
@@ -3422,6 +3483,13 @@ function ExportPage() {
if (Object.keys(cachedContentMetrics).length > 0) { if (Object.keys(cachedContentMetrics).length > 0) {
mergeSessionContentMetrics(cachedContentMetrics) mergeSessionContentMetrics(cachedContentMetrics)
} }
if (cachedMutualFriendSessionIds.length > 0) {
sessionMutualFriendsDirectMetricsRef.current = cachedMutualFriendDirectMetrics
rebuildSessionMutualFriendsStateFromDirectMetrics(cachedMutualFriendSessionIds)
} else {
sessionMutualFriendsMetricsRef.current = {}
setSessionMutualFriendsMetrics({})
}
setSessions(baseSessions) setSessions(baseSessions)
sessionsHydratedAtRef.current = Date.now() sessionsHydratedAtRef.current = Date.now()
void (async () => { void (async () => {
@@ -3622,7 +3690,7 @@ function ExportPage() {
} finally { } finally {
if (!isStale()) setIsLoading(false) if (!isStale()) setIsLoading(false)
} }
}, [ensureExportCacheScope, loadContactsCaches, loadSessionMessageCounts, mergeSessionContentMetrics, patchSessionLoadTraceStage, resetSessionMediaMetricLoader, resetSessionMutualFriendsLoader, syncContactTypeCounts]) }, [ensureExportCacheScope, loadContactsCaches, loadSessionMessageCounts, mergeSessionContentMetrics, patchSessionLoadTraceStage, rebuildSessionMutualFriendsStateFromDirectMetrics, resetSessionMediaMetricLoader, resetSessionMutualFriendsLoader, syncContactTypeCounts])
useEffect(() => { useEffect(() => {
if (!isExportRoute) return if (!isExportRoute) return
@@ -3630,10 +3698,7 @@ function ExportPage() {
const hasFreshSessionSnapshot = hasBaseConfigReadyRef.current && const hasFreshSessionSnapshot = hasBaseConfigReadyRef.current &&
sessionsRef.current.length > 0 && sessionsRef.current.length > 0 &&
now - sessionsHydratedAtRef.current <= EXPORT_REENTER_SESSION_SOFT_REFRESH_MS now - sessionsHydratedAtRef.current <= EXPORT_REENTER_SESSION_SOFT_REFRESH_MS
const hasFreshSnsSnapshot = hasSeededSnsStatsRef.current && const baseConfigPromise = loadBaseConfig()
now - snsStatsHydratedAtRef.current <= EXPORT_REENTER_SNS_SOFT_REFRESH_MS
void loadBaseConfig()
void ensureSharedTabCountsLoaded() void ensureSharedTabCountsLoaded()
if (!hasFreshSessionSnapshot) { if (!hasFreshSessionSnapshot) {
void loadSessions() void loadSessions()
@@ -3641,9 +3706,14 @@ function ExportPage() {
// 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。 // 朋友圈统计延后一点加载,避免与首屏会话初始化抢占。
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
void (async () => {
await baseConfigPromise
const hasFreshSnsSnapshot = hasSeededSnsStatsRef.current &&
Date.now() - snsStatsHydratedAtRef.current <= EXPORT_REENTER_SNS_SOFT_REFRESH_MS
if (!hasFreshSnsSnapshot) { if (!hasFreshSnsSnapshot) {
void loadSnsStats({ full: true }) void loadSnsStats({ full: true })
} }
})()
}, 120) }, 120)
return () => window.clearTimeout(timer) return () => window.clearTimeout(timer)
@@ -4988,9 +5058,14 @@ function ExportPage() {
window.clearTimeout(sessionMutualFriendsBackgroundFeedTimerRef.current) window.clearTimeout(sessionMutualFriendsBackgroundFeedTimerRef.current)
sessionMutualFriendsBackgroundFeedTimerRef.current = null sessionMutualFriendsBackgroundFeedTimerRef.current = null
} }
void flushSessionMediaMetricCache() if (sessionMutualFriendsPersistTimerRef.current) {
window.clearTimeout(sessionMutualFriendsPersistTimerRef.current)
sessionMutualFriendsPersistTimerRef.current = null
} }
}, [flushSessionMediaMetricCache]) void flushSessionMediaMetricCache()
void flushSessionMutualFriendsCache()
}
}, [flushSessionMediaMetricCache, flushSessionMutualFriendsCache])
const contactByUsername = useMemo(() => { const contactByUsername = useMemo(() => {
const map = new Map<string, ContactInfo>() const map = new Map<string, ContactInfo>()
@@ -5254,6 +5329,23 @@ function ExportPage() {
console.error('导出页读取会话统计缓存失败:', error) console.error('导出页读取会话统计缓存失败:', error)
} }
try {
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId],
{ includeRelations: true, allowStaleCache: true, cacheOnly: true }
)
if (requestSeq !== detailRequestSeqRef.current) return
if (relationCacheResult.success && relationCacheResult.data) {
const relationMetric = relationCacheResult.data[normalizedSessionId] as SessionExportMetric | undefined
const relationCacheMeta = relationCacheResult.cache?.[normalizedSessionId] as SessionExportCacheMeta | undefined
if (relationMetric) {
applySessionDetailStats(normalizedSessionId, relationMetric, relationCacheMeta, true)
}
}
} catch (error) {
console.error('导出页读取会话关系缓存失败:', error)
}
const lastPreciseAt = sessionPreciseRefreshAtRef.current[preciseCacheKey] || 0 const lastPreciseAt = sessionPreciseRefreshAtRef.current[preciseCacheKey] || 0
const hasRecentPrecise = Date.now() - lastPreciseAt <= DETAIL_PRECISE_REFRESH_COOLDOWN_MS const hasRecentPrecise = Date.now() - lastPreciseAt <= DETAIL_PRECISE_REFRESH_COOLDOWN_MS
const shouldRunPreciseRefresh = !hasRecentPrecise && (!quickMetric || Boolean(quickCacheMeta?.stale)) const shouldRunPreciseRefresh = !hasRecentPrecise && (!quickMetric || Boolean(quickCacheMeta?.stale))
@@ -5302,16 +5394,36 @@ function ExportPage() {
} }
}, [applySessionDetailStats, contactByUsername, mergeSessionContentMetrics, sessionContentMetrics, sessionMessageCounts, sessionRowByUsername]) }, [applySessionDetailStats, contactByUsername, mergeSessionContentMetrics, sessionContentMetrics, sessionMessageCounts, sessionRowByUsername])
const loadSessionRelationStats = useCallback(async () => { const loadSessionRelationStats = useCallback(async (options?: { forceRefresh?: boolean }) => {
const normalizedSessionId = String(sessionDetail?.wxid || '').trim() const normalizedSessionId = String(sessionDetail?.wxid || '').trim()
if (!normalizedSessionId || isLoadingSessionRelationStats) return if (!normalizedSessionId || isLoadingSessionRelationStats) return
const requestSeq = detailRequestSeqRef.current const requestSeq = detailRequestSeqRef.current
const forceRefresh = options?.forceRefresh === true
setIsLoadingSessionRelationStats(true) setIsLoadingSessionRelationStats(true)
try { try {
if (!forceRefresh) {
const relationCacheResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId],
{ includeRelations: true, allowStaleCache: true, cacheOnly: true }
)
if (requestSeq !== detailRequestSeqRef.current) return
const relationMetric = relationCacheResult.success && relationCacheResult.data
? relationCacheResult.data[normalizedSessionId] as SessionExportMetric | undefined
: undefined
const relationCacheMeta = relationCacheResult.success
? relationCacheResult.cache?.[normalizedSessionId] as SessionExportCacheMeta | undefined
: undefined
if (relationMetric) {
applySessionDetailStats(normalizedSessionId, relationMetric, relationCacheMeta, true)
return
}
}
const relationResult = await window.electronAPI.chat.getExportSessionStats( const relationResult = await window.electronAPI.chat.getExportSessionStats(
[normalizedSessionId], [normalizedSessionId],
{ includeRelations: true, forceRefresh: true, preferAccurateSpecialTypes: true } { includeRelations: true, forceRefresh, preferAccurateSpecialTypes: true }
) )
if (requestSeq !== detailRequestSeqRef.current) return if (requestSeq !== detailRequestSeqRef.current) return
@@ -5333,6 +5445,60 @@ function ExportPage() {
} }
}, [applySessionDetailStats, isLoadingSessionRelationStats, sessionDetail?.wxid]) }, [applySessionDetailStats, isLoadingSessionRelationStats, sessionDetail?.wxid])
const handleRefreshTableData = useCallback(async () => {
const scopeKey = await ensureExportCacheScope()
resetSessionMutualFriendsLoader()
sessionMutualFriendsMetricsRef.current = {}
setSessionMutualFriendsMetrics({})
closeSessionMutualFriendsDialog()
try {
await configService.clearExportSessionMutualFriendsCache(scopeKey)
} catch (error) {
console.error('清理导出页共同好友缓存失败:', error)
}
if (isSessionCountStageReady) {
const visibleTargetIds = collectVisibleSessionMutualFriendsTargets(filteredContacts)
const visibleTargetSet = new Set(visibleTargetIds)
const remainingTargetIds = sessionsRef.current
.filter((session) => session.hasSession && isSingleContactSession(session.username) && !visibleTargetSet.has(session.username))
.map((session) => session.username)
if (visibleTargetIds.length > 0) {
enqueueSessionMutualFriendsRequests(visibleTargetIds, { front: true })
}
if (remainingTargetIds.length > 0) {
enqueueSessionMutualFriendsRequests(remainingTargetIds)
}
scheduleSessionMutualFriendsWorker()
}
await Promise.all([
loadContactsList({ scopeKey }),
loadSnsStats({ full: true }),
loadSnsUserPostCounts({ force: true })
])
if (String(sessionDetail?.wxid || '').trim()) {
void loadSessionRelationStats({ forceRefresh: true })
}
}, [
closeSessionMutualFriendsDialog,
collectVisibleSessionMutualFriendsTargets,
enqueueSessionMutualFriendsRequests,
ensureExportCacheScope,
filteredContacts,
isSessionCountStageReady,
loadContactsList,
loadSessionRelationStats,
loadSnsStats,
loadSnsUserPostCounts,
resetSessionMutualFriendsLoader,
scheduleSessionMutualFriendsWorker,
sessionDetail?.wxid
])
useEffect(() => { useEffect(() => {
if (!showSessionDetailPanel || !sessionDetailSupportsSnsTimeline) return if (!showSessionDetailPanel || !sessionDetailSupportsSnsTimeline) return
if (snsUserPostCountsStatus === 'idle') { if (snsUserPostCountsStatus === 'idle') {
@@ -6371,7 +6537,7 @@ function ExportPage() {
</button> </button>
)} )}
</div> </div>
<button className="secondary-btn" onClick={() => void loadContactsList()} disabled={isContactsListLoading}> <button className="secondary-btn" onClick={() => void handleRefreshTableData()} disabled={isContactsListLoading}>
<RefreshCw size={14} className={isContactsListLoading ? 'spin' : ''} /> <RefreshCw size={14} className={isContactsListLoading ? 'spin' : ''} />
</button> </button>
@@ -6468,7 +6634,7 @@ function ExportPage() {
<li>3 IPC </li> <li>3 IPC </li>
</ul> </ul>
<div className="issue-actions"> <div className="issue-actions">
<button className="issue-btn primary" onClick={() => void loadContactsList()}> <button className="issue-btn primary" onClick={() => void handleRefreshTableData()}>
<RefreshCw size={14} /> <RefreshCw size={14} />
<span></span> <span></span>
</button> </button>

View File

@@ -44,6 +44,7 @@ export const CONFIG_KEYS = {
EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP: 'exportSessionContentMetricCacheMap', EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP: 'exportSessionContentMetricCacheMap',
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap', EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP: 'exportSnsUserPostCountsCacheMap', EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP: 'exportSnsUserPostCountsCacheMap',
EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP: 'exportSessionMutualFriendsCacheMap',
SNS_PAGE_CACHE_MAP: 'snsPageCacheMap', SNS_PAGE_CACHE_MAP: 'snsPageCacheMap',
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs', CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap', CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap',
@@ -596,6 +597,34 @@ export interface ExportSnsUserPostCountsCacheItem {
counts: Record<string, number> counts: Record<string, number>
} }
export type ExportSessionMutualFriendDirection = 'incoming' | 'outgoing' | 'bidirectional'
export type ExportSessionMutualFriendBehavior = 'likes' | 'comments' | 'both'
export interface ExportSessionMutualFriendCacheItem {
name: string
incomingLikeCount: number
incomingCommentCount: number
outgoingLikeCount: number
outgoingCommentCount: number
totalCount: number
latestTime: number
direction: ExportSessionMutualFriendDirection
behavior: ExportSessionMutualFriendBehavior
}
export interface ExportSessionMutualFriendsCacheEntry {
count: number
items: ExportSessionMutualFriendCacheItem[]
loadedPosts: number
totalPosts: number | null
computedAt: number
}
export interface ExportSessionMutualFriendsCacheItem {
updatedAt: number
metrics: Record<string, ExportSessionMutualFriendsCacheEntry>
}
export interface SnsPageOverviewCache { export interface SnsPageOverviewCache {
totalPosts: number totalPosts: number
totalFriends: number totalFriends: number
@@ -855,6 +884,148 @@ export async function setExportSnsUserPostCountsCache(
await config.set(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP, map) await config.set(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP, map)
} }
const normalizeMutualFriendDirection = (value: unknown): ExportSessionMutualFriendDirection | null => {
if (value === 'incoming' || value === 'outgoing' || value === 'bidirectional') {
return value
}
return null
}
const normalizeMutualFriendBehavior = (value: unknown): ExportSessionMutualFriendBehavior | null => {
if (value === 'likes' || value === 'comments' || value === 'both') {
return value
}
return null
}
const normalizeExportSessionMutualFriendsCacheEntry = (raw: unknown): ExportSessionMutualFriendsCacheEntry | null => {
if (!raw || typeof raw !== 'object') return null
const source = raw as Record<string, unknown>
const count = Number(source.count)
const loadedPosts = Number(source.loadedPosts)
const computedAt = Number(source.computedAt)
const itemsRaw = Array.isArray(source.items) ? source.items : []
const totalPostsRaw = source.totalPosts
const totalPosts = totalPostsRaw === null || totalPostsRaw === undefined
? null
: Number(totalPostsRaw)
if (!Number.isFinite(count) || count < 0 || !Number.isFinite(loadedPosts) || loadedPosts < 0 || !Number.isFinite(computedAt) || computedAt < 0) {
return null
}
const items: ExportSessionMutualFriendCacheItem[] = []
for (const itemRaw of itemsRaw) {
if (!itemRaw || typeof itemRaw !== 'object') continue
const item = itemRaw as Record<string, unknown>
const name = String(item.name || '').trim()
const direction = normalizeMutualFriendDirection(item.direction)
const behavior = normalizeMutualFriendBehavior(item.behavior)
const incomingLikeCount = Number(item.incomingLikeCount)
const incomingCommentCount = Number(item.incomingCommentCount)
const outgoingLikeCount = Number(item.outgoingLikeCount)
const outgoingCommentCount = Number(item.outgoingCommentCount)
const totalCount = Number(item.totalCount)
const latestTime = Number(item.latestTime)
if (!name || !direction || !behavior) continue
if (
!Number.isFinite(incomingLikeCount) || incomingLikeCount < 0 ||
!Number.isFinite(incomingCommentCount) || incomingCommentCount < 0 ||
!Number.isFinite(outgoingLikeCount) || outgoingLikeCount < 0 ||
!Number.isFinite(outgoingCommentCount) || outgoingCommentCount < 0 ||
!Number.isFinite(totalCount) || totalCount < 0 ||
!Number.isFinite(latestTime) || latestTime < 0
) {
continue
}
items.push({
name,
incomingLikeCount: Math.floor(incomingLikeCount),
incomingCommentCount: Math.floor(incomingCommentCount),
outgoingLikeCount: Math.floor(outgoingLikeCount),
outgoingCommentCount: Math.floor(outgoingCommentCount),
totalCount: Math.floor(totalCount),
latestTime: Math.floor(latestTime),
direction,
behavior
})
}
return {
count: Math.floor(count),
items,
loadedPosts: Math.floor(loadedPosts),
totalPosts: totalPosts === null
? null
: (Number.isFinite(totalPosts) && totalPosts >= 0 ? Math.floor(totalPosts) : null),
computedAt: Math.floor(computedAt)
}
}
export async function getExportSessionMutualFriendsCache(scopeKey: string): Promise<ExportSessionMutualFriendsCacheItem | null> {
if (!scopeKey) return null
const value = await config.get(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP)
if (!value || typeof value !== 'object') return null
const rawMap = value as Record<string, unknown>
const rawItem = rawMap[scopeKey]
if (!rawItem || typeof rawItem !== 'object') return null
const rawUpdatedAt = (rawItem as Record<string, unknown>).updatedAt
const rawMetrics = (rawItem as Record<string, unknown>).metrics
if (!rawMetrics || typeof rawMetrics !== 'object') return null
const metrics: Record<string, ExportSessionMutualFriendsCacheEntry> = {}
for (const [sessionIdRaw, metricRaw] of Object.entries(rawMetrics as Record<string, unknown>)) {
const sessionId = String(sessionIdRaw || '').trim()
if (!sessionId) continue
const metric = normalizeExportSessionMutualFriendsCacheEntry(metricRaw)
if (!metric) continue
metrics[sessionId] = metric
}
return {
updatedAt: typeof rawUpdatedAt === 'number' && Number.isFinite(rawUpdatedAt) ? rawUpdatedAt : 0,
metrics
}
}
export async function setExportSessionMutualFriendsCache(
scopeKey: string,
metrics: Record<string, ExportSessionMutualFriendsCacheEntry>
): Promise<void> {
if (!scopeKey) return
const current = await config.get(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP)
const map = current && typeof current === 'object'
? { ...(current as Record<string, unknown>) }
: {}
const normalized: Record<string, ExportSessionMutualFriendsCacheEntry> = {}
for (const [sessionIdRaw, metricRaw] of Object.entries(metrics || {})) {
const sessionId = String(sessionIdRaw || '').trim()
if (!sessionId) continue
const metric = normalizeExportSessionMutualFriendsCacheEntry(metricRaw)
if (!metric) continue
normalized[sessionId] = metric
}
map[scopeKey] = {
updatedAt: Date.now(),
metrics: normalized
}
await config.set(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP, map)
}
export async function clearExportSessionMutualFriendsCache(scopeKey: string): Promise<void> {
if (!scopeKey) return
const current = await config.get(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP)
if (!current || typeof current !== 'object') return
const map = { ...(current as Record<string, unknown>) }
if (!(scopeKey in map)) return
delete map[scopeKey]
await config.set(CONFIG_KEYS.EXPORT_SESSION_MUTUAL_FRIENDS_CACHE_MAP, map)
}
export async function getSnsPageCache(scopeKey: string): Promise<SnsPageCacheItem | null> { export async function getSnsPageCache(scopeKey: string): Promise<SnsPageCacheItem | null> {
if (!scopeKey) return null if (!scopeKey) return null
const value = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP) const value = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP)