- Image lazy loading: IntersectionObserver + blur-up placeholder - Request deduplication: in-flight Promise sharing by URL+body key - Prefetch on hover: 5-minute cache for pre-fetched detail data - Memory leak audit: useCleanup() tracks intervals/listeners/observers - Background sync queue: retry failed IDB saves on visibility change - Bundle splitting: composable architecture enables tree-shaking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
91 lines
1.9 KiB
TypeScript
91 lines
1.9 KiB
TypeScript
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
const STORAGE_KEY = 'aiui-sync-queue'
|
|
|
|
interface QueuedOperation {
|
|
id: string
|
|
type: string
|
|
data: unknown
|
|
createdAt: number
|
|
}
|
|
|
|
const queue = ref<QueuedOperation[]>([])
|
|
const hasQueuedItems = ref(false)
|
|
|
|
function loadQueue() {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored) queue.value = JSON.parse(stored)
|
|
hasQueuedItems.value = queue.value.length > 0
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
function saveQueue() {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(queue.value))
|
|
hasQueuedItems.value = queue.value.length > 0
|
|
}
|
|
|
|
loadQueue()
|
|
|
|
export function useSyncQueue() {
|
|
function enqueue(type: string, data: unknown) {
|
|
queue.value.push({
|
|
id: crypto.randomUUID(),
|
|
type,
|
|
data,
|
|
createdAt: Date.now(),
|
|
})
|
|
saveQueue()
|
|
}
|
|
|
|
async function processQueue(handler: (op: QueuedOperation) => Promise<boolean>) {
|
|
const remaining: QueuedOperation[] = []
|
|
for (const op of queue.value) {
|
|
try {
|
|
const success = await handler(op)
|
|
if (!success) remaining.push(op)
|
|
} catch {
|
|
remaining.push(op)
|
|
}
|
|
}
|
|
queue.value = remaining
|
|
saveQueue()
|
|
}
|
|
|
|
function clearQueue() {
|
|
queue.value = []
|
|
saveQueue()
|
|
}
|
|
|
|
// Auto-retry on visibility change
|
|
let retryHandler: (() => void) | null = null
|
|
|
|
function startAutoRetry(handler: (op: QueuedOperation) => Promise<boolean>) {
|
|
retryHandler = () => {
|
|
if (document.visibilityState === 'visible' && queue.value.length > 0) {
|
|
processQueue(handler)
|
|
}
|
|
}
|
|
document.addEventListener('visibilitychange', retryHandler)
|
|
}
|
|
|
|
onMounted(() => {
|
|
// Already loaded
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (retryHandler) {
|
|
document.removeEventListener('visibilitychange', retryHandler)
|
|
}
|
|
})
|
|
|
|
return {
|
|
queue,
|
|
hasQueuedItems,
|
|
enqueue,
|
|
processQueue,
|
|
clearQueue,
|
|
startAutoRetry,
|
|
}
|
|
}
|