mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
Merge pull request #367 from jxxghp/cursor/fix-sync-queue-data-corruption-issues-ee60
Cursor/fix sync queue data corruption issues ee60
This commit is contained in:
+57
-58
@@ -1,28 +1,8 @@
|
|||||||
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
||||||
|
|
||||||
// Service Worker 类型声明
|
// Service Worker 类型声明
|
||||||
declare let self: ServiceWorkerGlobalScope
|
declare let self: ServiceWorkerGlobalScope & {
|
||||||
|
__WB_MANIFEST: Array<{ url: string; revision?: string }>
|
||||||
// 扩展ServiceWorkerRegistration类型以支持sync
|
|
||||||
interface SyncManager {
|
|
||||||
register(tag: string): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServiceWorkerRegistration {
|
|
||||||
readonly sync: SyncManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扩展ExtendableEvent以支持sync事件
|
|
||||||
interface SyncEvent extends ExtendableEvent {
|
|
||||||
readonly tag: string
|
|
||||||
readonly lastChance: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扩展ServiceWorkerGlobalScope事件映射
|
|
||||||
declare global {
|
|
||||||
interface ServiceWorkerGlobalScopeEventMap {
|
|
||||||
'sync': SyncEvent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存版本控制
|
// 缓存版本控制
|
||||||
@@ -80,40 +60,67 @@ async function setStoredUnreadCount(count: number): Promise<void> {
|
|||||||
|
|
||||||
// 简单的IndexedDB包装器
|
// 简单的IndexedDB包装器
|
||||||
async function openDB(): Promise<IDBDatabase> {
|
async function openDB(): Promise<IDBDatabase> {
|
||||||
|
// Bump the version to add the new "sync" store while keeping existing data intact
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open('mp_badge_db', 1)
|
const request = indexedDB.open('mp_badge_db', 2)
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
request.onsuccess = () => resolve(request.result)
|
request.onsuccess = () => resolve(request.result)
|
||||||
|
|
||||||
request.onupgradeneeded = event => {
|
request.onupgradeneeded = event => {
|
||||||
const db = (event.target as IDBOpenDBRequest).result
|
const db = (event.target as IDBOpenDBRequest).result
|
||||||
|
|
||||||
|
// Badge store (existing)
|
||||||
if (!db.objectStoreNames.contains('badge')) {
|
if (!db.objectStoreNames.contains('badge')) {
|
||||||
db.createObjectStore('badge')
|
db.createObjectStore('badge')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dedicated store for offline-sync items
|
||||||
|
if (!db.objectStoreNames.contains('sync')) {
|
||||||
|
db.createObjectStore('sync')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取IndexedDB中的数据
|
// 获取IndexedDB中的数据
|
||||||
async function get(key: string): Promise<any> {
|
async function get(key: string, storeName: string = 'badge'): Promise<any> {
|
||||||
const db = await openDB()
|
const db = await openDB()
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(['badge'], 'readonly')
|
const tx = db.transaction([storeName], 'readonly')
|
||||||
const store = transaction.objectStore('badge')
|
const store = tx.objectStore(storeName)
|
||||||
const request = store.get(key)
|
const request = store.get(key)
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
request.onsuccess = () => resolve(request.result)
|
request.onsuccess = () => resolve(request.result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存数据到IndexedDB
|
// 保存数据到IndexedDB
|
||||||
async function set(key: string, value: any): Promise<void> {
|
async function set(key: string, value: any, storeName: string = 'badge'): Promise<void> {
|
||||||
const db = await openDB()
|
const db = await openDB()
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction(['badge'], 'readwrite')
|
const tx = db.transaction([storeName], 'readwrite')
|
||||||
const store = transaction.objectStore('badge')
|
const store = tx.objectStore(storeName)
|
||||||
const request = store.put(value, key)
|
|
||||||
request.onerror = () => reject(request.error)
|
store.put(value, key)
|
||||||
request.onsuccess = () => resolve()
|
|
||||||
|
tx.oncomplete = () => resolve()
|
||||||
|
tx.onerror = () => reject(tx.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除IndexedDB中的数据(确保事务完成)
|
||||||
|
async function del(key: string, storeName: string = 'badge'): Promise<void> {
|
||||||
|
const db = await openDB()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([storeName], 'readwrite')
|
||||||
|
const store = tx.objectStore(storeName)
|
||||||
|
|
||||||
|
store.delete(key)
|
||||||
|
|
||||||
|
tx.oncomplete = () => resolve()
|
||||||
|
tx.onerror = () => reject(tx.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +370,7 @@ const syncQueue: Array<{
|
|||||||
|
|
||||||
// 添加请求到同步队列
|
// 添加请求到同步队列
|
||||||
async function addToSyncQueue(request: Request) {
|
async function addToSyncQueue(request: Request) {
|
||||||
const id = `sync-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||||
const url = request.url
|
const url = request.url
|
||||||
const method = request.method
|
const method = request.method
|
||||||
|
|
||||||
@@ -384,8 +391,8 @@ async function addToSyncQueue(request: Request) {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到IndexedDB
|
// 保存到IndexedDB (使用专用的 "sync" store)
|
||||||
await set(`sync-${id}`, syncItem)
|
await set(id, syncItem, 'sync')
|
||||||
syncQueue.push(syncItem)
|
syncQueue.push(syncItem)
|
||||||
|
|
||||||
// 注册后台同步
|
// 注册后台同步
|
||||||
@@ -397,23 +404,20 @@ async function addToSyncQueue(request: Request) {
|
|||||||
// 执行同步队列中的请求
|
// 执行同步队列中的请求
|
||||||
async function processSyncQueue() {
|
async function processSyncQueue() {
|
||||||
const db = await openDB()
|
const db = await openDB()
|
||||||
const transaction = db.transaction(['badge'], 'readonly')
|
|
||||||
const store = transaction.objectStore('badge')
|
|
||||||
const request = store.getAllKeys()
|
|
||||||
|
|
||||||
const keys = await new Promise<IDBValidKey[]>((resolve, reject) => {
|
// 使用专用的 "sync" store,并开启 readwrite 事务(便于后续删除)
|
||||||
request.onsuccess = () => resolve(request.result)
|
const tx = db.transaction(['sync'], 'readwrite')
|
||||||
request.onerror = () => reject(request.error)
|
const store = tx.objectStore('sync')
|
||||||
|
|
||||||
|
const items: Array<any> = await new Promise((resolve, reject) => {
|
||||||
|
const req = store.getAll()
|
||||||
|
req.onsuccess = () => resolve(req.result)
|
||||||
|
req.onerror = () => reject(req.error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 过滤出同步项
|
for (const syncItem of items) {
|
||||||
const syncKeys = keys.filter(key => String(key).startsWith('sync-'))
|
const key = syncItem.id
|
||||||
|
|
||||||
for (const key of syncKeys) {
|
|
||||||
try {
|
try {
|
||||||
const syncItem = await get(String(key))
|
|
||||||
if (!syncItem) continue
|
|
||||||
|
|
||||||
// 构建请求
|
// 构建请求
|
||||||
const init: RequestInit = {
|
const init: RequestInit = {
|
||||||
method: syncItem.method,
|
method: syncItem.method,
|
||||||
@@ -430,10 +434,8 @@ async function processSyncQueue() {
|
|||||||
const response = await fetch(syncItem.url, init)
|
const response = await fetch(syncItem.url, init)
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// 成功后删除同步项
|
// 成功后删除同步项,并等待事务完成
|
||||||
const deleteTransaction = db.transaction(['badge'], 'readwrite')
|
await del(key, 'sync')
|
||||||
const deleteStore = deleteTransaction.objectStore('badge')
|
|
||||||
await deleteStore.delete(key)
|
|
||||||
|
|
||||||
// 通知客户端同步成功
|
// 通知客户端同步成功
|
||||||
const clients = await self.clients.matchAll()
|
const clients = await self.clients.matchAll()
|
||||||
@@ -450,12 +452,9 @@ async function processSyncQueue() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Sync failed for item:', key, error)
|
console.error('Sync failed for item:', key, error)
|
||||||
|
|
||||||
// 检查是否超过24小时,如果是则删除
|
// 如果该同步项已存在超过 24 小时,则将其丢弃
|
||||||
const syncItem = await get(String(key))
|
if (Date.now() - syncItem.timestamp > 24 * 60 * 60 * 1000) {
|
||||||
if (syncItem && Date.now() - syncItem.timestamp > 24 * 60 * 60 * 1000) {
|
await del(key, 'sync')
|
||||||
const deleteTransaction = db.transaction(['badge'], 'readwrite')
|
|
||||||
const deleteStore = deleteTransaction.objectStore('badge')
|
|
||||||
await deleteStore.delete(key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+38
@@ -0,0 +1,38 @@
|
|||||||
|
export {}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
/**
|
||||||
|
* Background SyncManager interface as per the Web Background Sync API.
|
||||||
|
*/
|
||||||
|
interface SyncManager {
|
||||||
|
/**
|
||||||
|
* Registers a one-off sync event with the provided tag.
|
||||||
|
*/
|
||||||
|
register(tag: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension of ServiceWorkerRegistration to include the SyncManager.
|
||||||
|
*/
|
||||||
|
interface ServiceWorkerRegistration {
|
||||||
|
/**
|
||||||
|
* The SyncManager for background sync operations.
|
||||||
|
*/
|
||||||
|
readonly sync: SyncManager
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event fired when a background sync is triggered.
|
||||||
|
*/
|
||||||
|
interface SyncEvent extends ExtendableEvent {
|
||||||
|
readonly tag: string
|
||||||
|
readonly lastChance: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend ServiceWorkerGlobalScope event map to include the sync event type.
|
||||||
|
*/
|
||||||
|
interface ServiceWorkerGlobalScopeEventMap {
|
||||||
|
'sync': SyncEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
// Type definitions for workbox-precaching runtime use in service worker
|
||||||
|
// Simplified subset needed by this project
|
||||||
|
|
||||||
|
declare module 'workbox-precaching' {
|
||||||
|
/**
|
||||||
|
* A manifest entry generated by Workbox build tools.
|
||||||
|
*/
|
||||||
|
export interface ManifestEntry {
|
||||||
|
url: string
|
||||||
|
revision?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes outdated precaches created by older versions of Workbox.
|
||||||
|
*/
|
||||||
|
export function cleanupOutdatedCaches(): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the supplied manifest entries to the precache list and sets up the
|
||||||
|
* appropriate route so that they are served from the cache.
|
||||||
|
*/
|
||||||
|
export function precacheAndRoute(entries: ManifestEntry[]): void
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user