import type { OwnPost } from "../domain/types"; import { KEYS } from "../data/mock/keys"; import { readJson, writeJson } from "./storage"; /** 本月成效:距離上次真 API sync 不足此間隔則不重打(5 分鐘) */ export const OWN_POSTS_CURRENT_TTL_MS = 5 * 60 * 1000; type AccountSyncMeta = { /** 上次成功呼叫 sync API 的時間(ms) */ lastSyncMs: number; /** 是否已為「過去月份缺資料」補抓過一次 */ pastOnceDone: boolean; }; type MetaStore = Record; /** accountId → in-flight sync promise(singleflight) */ const inflight = new Map>(); function loadStore(): MetaStore { return readJson(KEYS.ownPostsSyncMeta, {} as MetaStore); } function saveStore(store: MetaStore): void { writeJson(KEYS.ownPostsSyncMeta, store); } export function getOwnPostsSyncMeta(accountId: string): AccountSyncMeta { const row = loadStore()[accountId]; return { lastSyncMs: row?.lastSyncMs ?? 0, pastOnceDone: Boolean(row?.pastOnceDone), }; } function patchMeta(accountId: string, patch: Partial): void { const store = loadStore(); const prev = store[accountId] || { lastSyncMs: 0, pastOnceDone: false }; store[accountId] = { ...prev, ...patch }; saveStore(store); } export function isCurrentSyncFresh(accountId: string, now = Date.now()): boolean { const { lastSyncMs } = getOwnPostsSyncMeta(accountId); return lastSyncMs > 0 && now - lastSyncMs < OWN_POSTS_CURRENT_TTL_MS; } export function isPastOnceDone(accountId: string): boolean { return getOwnPostsSyncMeta(accountId).pastOnceDone; } export type OwnPostsSyncReason = /** 使用者手動同步:一定打 API */ | "manual" /** 過去月份缺資料:整帳只補抓一次 */ | "past-once" /** 本月定期刷新(5 分鐘) */ | "current-ttl"; export type OwnPostsSyncResult = { posts: OwnPost[]; /** 是否真的打了 sync API(false = 走 list 或共用 in-flight 已完成後的讀取) */ fetched: boolean; /** 因 TTL/past-once 已完成而跳過 */ skipped: boolean; reason: OwnPostsSyncReason; }; type Ops = { list: (accountId: string) => Promise; sync: (accountId: string) => Promise; }; /** * 以 accountId 為 key 的 singleflight sync。 * - manual:一定呼叫 sync(並發共用同一個 promise) * - past-once:若已 pastOnceDone 則只 list * - current-ttl:若 5 分鐘內已 sync 則只 list */ export async function syncOwnPostsGated( accountId: string, ops: Ops, reason: OwnPostsSyncReason, ): Promise { if (!accountId) { return { posts: [], fetched: false, skipped: true, reason }; } const existing = inflight.get(accountId); if (existing) { const posts = await existing; return { posts, fetched: true, skipped: false, reason }; } if (reason === "past-once" && isPastOnceDone(accountId)) { const posts = await ops.list(accountId); return { posts, fetched: false, skipped: true, reason }; } if (reason === "current-ttl" && isCurrentSyncFresh(accountId)) { const posts = await ops.list(accountId); return { posts, fetched: false, skipped: true, reason }; } const promise = (async () => { try { const posts = await ops.sync(accountId); const prev = getOwnPostsSyncMeta(accountId); patchMeta(accountId, { lastSyncMs: Date.now(), // 手動或過去月份補抓 → 標記 past-once 完成;TTL 刷新不改旗標 pastOnceDone: prev.pastOnceDone || reason === "past-once" || reason === "manual", }); return posts; } finally { inflight.delete(accountId); } })(); inflight.set(accountId, promise); const posts = await promise; return { posts, fetched: true, skipped: false, reason }; } /** 過去月份(不含本月)是否仍全空——用來決定是否觸發 past-once */ export function needsPastMonthBackfill( months: Array<{ key: string; posts: number }>, currentMonthKey: string, ): boolean { return months.some((m) => m.key !== currentMonthKey && m.posts <= 0); }