| 1 | /** |
| 2 | * douyinPublishSession - 抖音 H5 发布会话持久化 Store |
| 3 | * Stores Douyin publish polling sessions for 10-minute recovery. |
| 4 | */ |
| 5 | |
| 6 | import { createPersistStore } from '@/utils/storage/createPersistStore' |
| 7 | |
| 8 | export const DOUYIN_PUBLISH_SESSION_TTL = 10 * 60 * 1000 |
| 9 | |
| 10 | export type DouyinPublishSessionStatus = 'idle' | 'polling' | 'published' | 'failed' |
| 11 | export type DouyinPublishSubmitType = 'auto' | 'manual' |
| 12 | |
| 13 | export interface DouyinPublishSessionRecord { |
| 14 | publishRecordId: string |
| 15 | shortLink: string | null |
| 16 | permalink: string | null |
| 17 | status: DouyinPublishSessionStatus |
| 18 | workLink?: string |
| 19 | errorMsg?: string |
| 20 | createdAt: number |
| 21 | updatedAt: number |
| 22 | expiresAt: number |
| 23 | lastPolledAt?: number |
| 24 | autoSubmittedAt?: number |
| 25 | manualSubmittedAt?: number |
| 26 | } |
| 27 | |
| 28 | interface DouyinPublishSessionState { |
| 29 | records: Record<string, DouyinPublishSessionRecord> |
| 30 | } |
| 31 | |
| 32 | function isExpired(record?: DouyinPublishSessionRecord | null) { |
| 33 | return !record || record.expiresAt <= Date.now() |
| 34 | } |
| 35 | |
| 36 | export const useDouyinPublishSessionStore = createPersistStore<DouyinPublishSessionState, { |
| 37 | getSession: (sessionKey: string) => DouyinPublishSessionRecord | null |
| 38 | setSession: ( |
| 39 | sessionKey: string, |
| 40 | payload: Pick<DouyinPublishSessionRecord, 'publishRecordId'> & Partial<Omit<DouyinPublishSessionRecord, 'publishRecordId' | 'createdAt' | 'updatedAt' | 'expiresAt'>>, |
| 41 | ) => DouyinPublishSessionRecord |
| 42 | markPublished: (sessionKey: string, payload?: Partial<Pick<DouyinPublishSessionRecord, 'workLink' | 'errorMsg' | 'shortLink' | 'permalink'>>) => void |
| 43 | markFailed: (sessionKey: string, errorMsg?: string) => void |
| 44 | markPolling: (sessionKey: string, payload?: Partial<Pick<DouyinPublishSessionRecord, 'workLink' | 'errorMsg'>>) => void |
| 45 | markSubmitted: (sessionKey: string, type: DouyinPublishSubmitType) => void |
| 46 | clearSession: (sessionKey: string) => void |
| 47 | clearExpiredSessions: () => void |
| 48 | }>( |
| 49 | { records: {} }, |
| 50 | (set, get) => { |
| 51 | const methods = { |
| 52 | getSession(sessionKey: string) { |
| 53 | const record = get().records[sessionKey] |
| 54 | if (isExpired(record)) { |
| 55 | if (record) |
| 56 | methods.clearSession(sessionKey) |
| 57 | return null |
| 58 | } |
| 59 | return record |
| 60 | }, |
| 61 | setSession(sessionKey: string, payload: Pick<DouyinPublishSessionRecord, 'publishRecordId'> & Partial<Omit<DouyinPublishSessionRecord, 'publishRecordId' | 'createdAt' | 'updatedAt' | 'expiresAt'>>) { |
| 62 | const now = Date.now() |
| 63 | const current = get().records[sessionKey] |
| 64 | const next: DouyinPublishSessionRecord = { |
| 65 | publishRecordId: payload.publishRecordId, |
| 66 | shortLink: payload.shortLink ?? current?.shortLink ?? null, |
| 67 | permalink: payload.permalink ?? current?.permalink ?? null, |
| 68 | status: payload.status ?? current?.status ?? 'polling', |
| 69 | workLink: payload.workLink ?? current?.workLink, |
| 70 | errorMsg: payload.errorMsg ?? current?.errorMsg, |
| 71 | createdAt: current?.createdAt ?? now, |
| 72 | updatedAt: now, |
| 73 | expiresAt: now + DOUYIN_PUBLISH_SESSION_TTL, |
| 74 | lastPolledAt: payload.lastPolledAt ?? current?.lastPolledAt, |
| 75 | autoSubmittedAt: payload.autoSubmittedAt ?? current?.autoSubmittedAt, |
| 76 | manualSubmittedAt: payload.manualSubmittedAt ?? current?.manualSubmittedAt, |
| 77 | } |
| 78 | set(state => ({ |
| 79 | records: { |
| 80 | ...state.records, |
| 81 | [sessionKey]: next, |
| 82 | }, |
| 83 | })) |
| 84 | return next |
| 85 | }, |
| 86 | markPublished(sessionKey: string, payload?: Partial<Pick<DouyinPublishSessionRecord, 'workLink' | 'errorMsg' | 'shortLink' | 'permalink'>>) { |
| 87 | const current = methods.getSession(sessionKey) |
| 88 | if (!current) |
| 89 | return |
| 90 | methods.setSession(sessionKey, { |
| 91 | publishRecordId: current.publishRecordId, |
| 92 | status: 'published', |
| 93 | shortLink: payload?.shortLink ?? current.shortLink, |
| 94 | permalink: payload?.permalink ?? current.permalink, |
| 95 | workLink: payload?.workLink ?? current.workLink, |
| 96 | errorMsg: payload?.errorMsg, |
| 97 | }) |
| 98 | }, |
| 99 | markFailed(sessionKey: string, errorMsg?: string) { |
| 100 | const current = methods.getSession(sessionKey) |
| 101 | if (!current) |
| 102 | return |
| 103 | methods.setSession(sessionKey, { |
| 104 | publishRecordId: current.publishRecordId, |
| 105 | status: 'failed', |
| 106 | errorMsg, |
| 107 | }) |
| 108 | }, |
| 109 | markPolling(sessionKey: string, payload?: Partial<Pick<DouyinPublishSessionRecord, 'workLink' | 'errorMsg'>>) { |
| 110 | const current = methods.getSession(sessionKey) |
| 111 | if (!current) |
| 112 | return |
| 113 | methods.setSession(sessionKey, { |
| 114 | publishRecordId: current.publishRecordId, |
| 115 | status: 'polling', |
| 116 | workLink: payload?.workLink ?? current.workLink, |
| 117 | errorMsg: payload?.errorMsg, |
| 118 | }) |
| 119 | }, |
| 120 | markSubmitted(sessionKey: string, type: DouyinPublishSubmitType) { |
| 121 | const current = methods.getSession(sessionKey) |
| 122 | if (!current) |
| 123 | return |
| 124 | const now = Date.now() |
| 125 | methods.setSession(sessionKey, { |
| 126 | publishRecordId: current.publishRecordId, |
| 127 | autoSubmittedAt: type === 'auto' ? now : current.autoSubmittedAt, |
| 128 | manualSubmittedAt: type === 'manual' ? now : current.manualSubmittedAt, |
| 129 | }) |
| 130 | }, |
| 131 | clearSession(sessionKey: string) { |
| 132 | set((state) => { |
| 133 | const next = { ...state.records } |
| 134 | delete next[sessionKey] |
| 135 | return { records: next } |
| 136 | }) |
| 137 | }, |
| 138 | clearExpiredSessions() { |
| 139 | set((state) => { |
| 140 | const next = Object.fromEntries( |
| 141 | Object.entries(state.records).filter(([, record]) => !isExpired(record)), |
| 142 | ) |
| 143 | return { records: next } |
| 144 | }) |
| 145 | }, |
| 146 | } |
| 147 | |
| 148 | return methods |
| 149 | }, |
| 150 | { |
| 151 | name: 'douyin-publish-session', |
| 152 | version: 1, |
| 153 | }, |
| 154 | 'indexedDB', |
| 155 | ) |
| 156 |