Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
246 lines
5.8 KiB
JavaScript
246 lines
5.8 KiB
JavaScript
// Service Worker for offline support and performance optimization
|
|
|
|
const CACHE_NAME = 'marketing-agent-v1'
|
|
const STATIC_CACHE_NAME = 'marketing-agent-static-v1'
|
|
const DYNAMIC_CACHE_NAME = 'marketing-agent-dynamic-v1'
|
|
const API_CACHE_NAME = 'marketing-agent-api-v1'
|
|
|
|
// Files to cache immediately
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/index.html',
|
|
'/manifest.json',
|
|
'/images/logo.png',
|
|
'/images/placeholder.png'
|
|
]
|
|
|
|
// API endpoints to cache
|
|
const CACHEABLE_API_PATTERNS = [
|
|
/\/api\/v1\/users\/profile$/,
|
|
/\/api\/v1\/campaigns\/templates$/,
|
|
/\/api\/v1\/settings$/
|
|
]
|
|
|
|
// Install event - cache static assets
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
|
return cache.addAll(STATIC_ASSETS)
|
|
})
|
|
)
|
|
self.skipWaiting()
|
|
})
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter((cacheName) => {
|
|
return cacheName.startsWith('marketing-agent-') &&
|
|
cacheName !== CACHE_NAME &&
|
|
cacheName !== STATIC_CACHE_NAME &&
|
|
cacheName !== DYNAMIC_CACHE_NAME &&
|
|
cacheName !== API_CACHE_NAME
|
|
})
|
|
.map((cacheName) => caches.delete(cacheName))
|
|
)
|
|
})
|
|
)
|
|
self.clients.claim()
|
|
})
|
|
|
|
// Fetch event - implement caching strategies
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event
|
|
const url = new URL(request.url)
|
|
|
|
// Skip non-GET requests
|
|
if (request.method !== 'GET') {
|
|
return
|
|
}
|
|
|
|
// Handle API requests
|
|
if (url.pathname.startsWith('/api/')) {
|
|
event.respondWith(handleApiRequest(request))
|
|
return
|
|
}
|
|
|
|
// Handle static assets
|
|
if (isStaticAsset(url.pathname)) {
|
|
event.respondWith(handleStaticAsset(request))
|
|
return
|
|
}
|
|
|
|
// Handle dynamic content
|
|
event.respondWith(handleDynamicContent(request))
|
|
})
|
|
|
|
// Cache-first strategy for static assets
|
|
async function handleStaticAsset(request) {
|
|
const cache = await caches.open(STATIC_CACHE_NAME)
|
|
const cached = await cache.match(request)
|
|
|
|
if (cached) {
|
|
return cached
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(request)
|
|
if (response.ok) {
|
|
cache.put(request, response.clone())
|
|
}
|
|
return response
|
|
} catch (error) {
|
|
return new Response('Offline - Asset not available', {
|
|
status: 503,
|
|
statusText: 'Service Unavailable'
|
|
})
|
|
}
|
|
}
|
|
|
|
// Network-first strategy for API requests with fallback
|
|
async function handleApiRequest(request) {
|
|
const cache = await caches.open(API_CACHE_NAME)
|
|
|
|
try {
|
|
const response = await fetch(request)
|
|
|
|
// Cache successful responses for cacheable endpoints
|
|
if (response.ok && isCacheableApi(request.url)) {
|
|
cache.put(request, response.clone())
|
|
}
|
|
|
|
return response
|
|
} catch (error) {
|
|
// Try cache on network failure
|
|
const cached = await cache.match(request)
|
|
if (cached) {
|
|
// Add header to indicate cached response
|
|
const headers = new Headers(cached.headers)
|
|
headers.set('X-From-Cache', 'true')
|
|
|
|
return new Response(cached.body, {
|
|
status: cached.status,
|
|
statusText: cached.statusText,
|
|
headers
|
|
})
|
|
}
|
|
|
|
// Return offline response
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Network unavailable',
|
|
offline: true
|
|
}),
|
|
{
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
// Stale-while-revalidate strategy for dynamic content
|
|
async function handleDynamicContent(request) {
|
|
const cache = await caches.open(DYNAMIC_CACHE_NAME)
|
|
const cached = await cache.match(request)
|
|
|
|
const fetchPromise = fetch(request).then((response) => {
|
|
if (response.ok) {
|
|
cache.put(request, response.clone())
|
|
}
|
|
return response
|
|
})
|
|
|
|
return cached || fetchPromise
|
|
}
|
|
|
|
// Helper functions
|
|
function isStaticAsset(pathname) {
|
|
return /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/.test(pathname)
|
|
}
|
|
|
|
function isCacheableApi(url) {
|
|
return CACHEABLE_API_PATTERNS.some(pattern => pattern.test(url))
|
|
}
|
|
|
|
// Background sync for offline actions
|
|
self.addEventListener('sync', (event) => {
|
|
if (event.tag === 'sync-campaigns') {
|
|
event.waitUntil(syncCampaigns())
|
|
}
|
|
})
|
|
|
|
async function syncCampaigns() {
|
|
const cache = await caches.open('offline-campaigns')
|
|
const requests = await cache.keys()
|
|
|
|
for (const request of requests) {
|
|
try {
|
|
const response = await fetch(request)
|
|
if (response.ok) {
|
|
await cache.delete(request)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to sync:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Push notifications
|
|
self.addEventListener('push', (event) => {
|
|
const options = {
|
|
body: event.data ? event.data.text() : 'New notification',
|
|
icon: '/images/logo.png',
|
|
badge: '/images/badge.png',
|
|
vibrate: [100, 50, 100],
|
|
data: {
|
|
dateOfArrival: Date.now(),
|
|
primaryKey: 1
|
|
},
|
|
actions: [
|
|
{
|
|
action: 'view',
|
|
title: 'View',
|
|
icon: '/images/checkmark.png'
|
|
},
|
|
{
|
|
action: 'close',
|
|
title: 'Close',
|
|
icon: '/images/xmark.png'
|
|
}
|
|
]
|
|
}
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification('Marketing Agent', options)
|
|
)
|
|
})
|
|
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close()
|
|
|
|
if (event.action === 'view') {
|
|
event.waitUntil(
|
|
clients.openWindow('/')
|
|
)
|
|
}
|
|
})
|
|
|
|
// Message handling for cache control
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting()
|
|
} else if (event.data.type === 'CLEAR_CACHE') {
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames.map((cacheName) => caches.delete(cacheName))
|
|
)
|
|
})
|
|
)
|
|
}
|
|
}) |