// Service Worker สำหรับระบบจัดการนัดหมาย
// Version 1.0.0
const CACHE_NAME = 'appointment-chat-v1.0.0';
const STATIC_CACHE = 'appointment-static-v1.0.0';
const DYNAMIC_CACHE = 'appointment-dynamic-v1.0.0';
// ไฟล์ที่ต้องการ cache ไว้สำหรับการทำงานแบบออฟไลน์
const STATIC_FILES = [
'/',
'/index.html',
'/manifest.json',
// Font Awesome
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css',
// Google Fonts
'https://fonts.googleapis.com/css2?family=Prompt:wght@400;500;600;700&display=swap',
'https://fonts.gstatic.com/s/prompt/v10/jizfREFUsnUct9P6cDfd4OmnLD0Z4zM.woff2',
'https://fonts.gstatic.com/s/prompt/v10/jizfREFUsnUct9P6cDfd4OmnLD0Z5DM.woff2',
// เพิ่มไฟล์อื่นๆ ตามต้องการ
];
// URLs ที่ไม่ต้องการ cache
const EXCLUDED_URLS = [
'/api.php',
'chrome-extension://',
'moz-extension://',
];
// ✅ Install Event - Cache static files
self.addEventListener('install', (event) => {
console.log('🔧 Service Worker: Installing...');
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('📦 Service Worker: Caching static files');
return cache.addAll(STATIC_FILES);
})
.catch((error) => {
console.error('❌ Service Worker: Cache installation failed', error);
})
);
// บังคับให้ service worker ใหม่เข้ามาแทนที่ทันที
self.skipWaiting();
});
// ✅ Activate Event - Clean up old caches
self.addEventListener('activate', (event) => {
console.log('🚀 Service Worker: Activating...');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// ลบ cache เก่าที่ไม่ใช้แล้ว
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
console.log('🗑️ Service Worker: Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('✅ Service Worker: Cleanup complete');
// เข้าควบคุม client ทั้งหมดทันที
return self.clients.claim();
})
);
});
// ✅ Fetch Event - Network First with Cache Fallback
self.addEventListener('fetch', (event) => {
const {request} = event;
const url = request.url;
// ข้าม URLs ที่ไม่ต้องการ cache
if (EXCLUDED_URLS.some(excludedUrl => url.includes(excludedUrl))) {
return;
}
// ข้าม non-GET requests
if (request.method !== 'GET') {
return;
}
event.respondWith(
networkFirstWithCacheFallback(request)
);
});
// 🌐 Network First Strategy with Cache Fallback
async function networkFirstWithCacheFallback(request) {
const url = request.url;
try {
// พยายามเรียกจาก network ก่อน
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// หาก response ดี ให้ cache ไว้ใน dynamic cache
const cache = await caches.open(DYNAMIC_CACHE);
// Cache เฉพาะ static files และ font files
if (isStaticResource(url) || isFontResource(url)) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
// หาก network response ไม่ดี ให้ fallback ไป cache
return getCachedResponse(request);
} catch (error) {
// หาก network fail ให้ fallback ไป cache
console.log('🔌 Service Worker: Network failed, trying cache for:', url);
return getCachedResponse(request);
}
}
// 📦 Get response from cache
async function getCachedResponse(request) {
const url = request.url;
// ลองหาใน static cache ก่อน
let cachedResponse = await caches.match(request, {cacheName: STATIC_CACHE});
if (cachedResponse) {
console.log('📦 Service Worker: Serving from static cache:', url);
return cachedResponse;
}
// ถ้าไม่มีใน static cache ให้ลองหาใน dynamic cache
cachedResponse = await caches.match(request, {cacheName: DYNAMIC_CACHE});
if (cachedResponse) {
console.log('📦 Service Worker: Serving from dynamic cache:', url);
return cachedResponse;
}
// หาก request เป็น navigation (เช่น หน้าเว็บ) ให้ return index.html
if (request.mode === 'navigate') {
console.log('🏠 Service Worker: Serving index.html for navigation');
return caches.match('/index.html');
}
// ถ้าไม่มีใน cache เลย ให้ return offline page หรือ error response
console.log('❌ Service Worker: No cache found for:', url);
return new Response('Offline - Content not available', {
status: 503,
statusText: 'Service Unavailable',
headers: {'Content-Type': 'text/plain'}
});
}
// 🔍 Helper Functions
function isStaticResource(url) {
return url.includes('.css') ||
url.includes('.js') ||
url.includes('.png') ||
url.includes('.jpg') ||
url.includes('.jpeg') ||
url.includes('.gif') ||
url.includes('.svg') ||
url.includes('.ico') ||
url.includes('.webp');
}
function isFontResource(url) {
return url.includes('.woff') ||
url.includes('.woff2') ||
url.includes('.ttf') ||
url.includes('.eot') ||
url.includes('fonts.googleapis.com') ||
url.includes('fonts.gstatic.com');
}
// 🔄 Background Sync for offline data
self.addEventListener('sync', (event) => {
console.log('🔄 Service Worker: Background sync triggered');
if (event.tag === 'appointment-sync') {
event.waitUntil(syncAppointments());
}
});
// 📤 Sync appointments when back online
async function syncAppointments() {
console.log('📤 Service Worker: Syncing appointments...');
try {
// ส่งข้อความไปยัง client ให้ sync ข้อมูล
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage({
type: 'SYNC_APPOINTMENTS'
});
});
console.log('✅ Service Worker: Sync message sent to clients');
} catch (error) {
console.error('❌ Service Worker: Sync failed', error);
}
}
// 🔔 Push Notification Handler
self.addEventListener('push', (event) => {
console.log('🔔 Service Worker: Push notification received');
const options = {
body: 'คุณมีนัดหมายใกล้เข้ามาแล้ว',
icon: '/icon-192.png',
badge: '/icon-192.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'view',
title: 'ดูนัดหมาย',
icon: '/icon-192.png'
},
{
action: 'close',
title: 'ปิด'
}
]
};
event.waitUntil(
self.registration.showNotification('เตือนนัดหมาย', options)
);
});
// 🔔 Notification Click Handler
self.addEventListener('notificationclick', (event) => {
console.log('🔔 Service Worker: Notification clicked');
event.notification.close();
if (event.action === 'view') {
// เปิดแอปหรือ focus ไปที่แอป
event.waitUntil(
clients.matchAll().then((clientList) => {
for (const client of clientList) {
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
}
});
// 📨 Message Handler - รับข้อความจาก client
self.addEventListener('message', (event) => {
console.log('📨 Service Worker: Message received:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'GET_VERSION') {
event.ports[0].postMessage({version: CACHE_NAME});
}
});
// 🧹 Periodic cleanup of old dynamic cache entries
setInterval(() => {
cleanupDynamicCache();
}, 24 * 60 * 60 * 1000); // ทุก 24 ชั่วโมง
async function cleanupDynamicCache() {
try {
const cache = await caches.open(DYNAMIC_CACHE);
const requests = await cache.keys();
// ลบ cache entries ที่เก่าเกิน 7 วัน
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
for (const request of requests) {
const response = await cache.match(request);
const dateHeader = response.headers.get('date');
if (dateHeader) {
const responseDate = new Date(dateHeader).getTime();
if (responseDate < oneWeekAgo) {
await cache.delete(request);
console.log('🧹 Service Worker: Cleaned old cache entry:', request.url);
}
}
}
} catch (error) {
console.error('❌ Service Worker: Cache cleanup failed', error);
}
}
console.log('🎉 Service Worker: Script loaded successfully');