返回 AiToEarn
douyinPublishSession.ts
根目录 / project / aitoearn-web / src / store / douyinPublishSession.ts
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
156 lines TYPESCRIPT