2026-01-08 21:49:57 +01:00

227 lines
5.6 KiB
TypeScript

/**
* Service Worker client - communication bridge between main thread and Service Worker
*/
interface SWMessage {
type: string
data?: unknown
}
interface SWResponse {
type: string
data?: unknown
}
class ServiceWorkerClient {
private registration: ServiceWorkerRegistration | null = null
private messageHandlers: Map<string, Array<(data: unknown) => void>> = new Map()
private readyPromise: Promise<ServiceWorkerRegistration> | null = null
/**
* Initialize and register the Service Worker
*/
async register(): Promise<ServiceWorkerRegistration> {
if (this.readyPromise) {
return this.readyPromise
}
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
throw new Error('Service Workers are not supported in this browser')
}
this.readyPromise = navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.then((registration) => {
this.registration = registration
console.warn('[SWClient] Service Worker registered:', registration.scope)
// Listen for messages from Service Worker
navigator.serviceWorker.addEventListener('message', (event) => {
this.handleMessage(event.data)
})
// Handle updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
console.warn('[SWClient] New Service Worker available, reloading...')
window.location.reload()
}
})
}
})
return registration
})
.catch((error) => {
console.error('[SWClient] Service Worker registration failed:', error)
throw error
})
return this.readyPromise
}
/**
* Unregister the Service Worker
*/
async unregister(): Promise<boolean> {
if (!this.registration) {
return false
}
const result = await this.registration.unregister()
this.registration = null
this.readyPromise = null
return result
}
/**
* Send message to Service Worker
*/
async sendMessage(message: SWMessage): Promise<void> {
if (!this.registration?.active) {
await this.register()
}
if (!this.registration?.active) {
throw new Error('Service Worker is not active')
}
this.registration.active.postMessage(message)
}
/**
* Send message and wait for response
*/
async sendMessageWithResponse(message: SWMessage, timeout: number = 5000): Promise<SWResponse> {
if (!this.registration?.active) {
await this.register()
}
if (!this.registration?.active) {
throw new Error('Service Worker is not active')
}
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel()
const timeoutId = setTimeout(() => {
reject(new Error('Service Worker response timeout'))
}, timeout)
messageChannel.port1.onmessage = (event) => {
clearTimeout(timeoutId)
resolve(event.data)
}
if (this.registration?.active) {
this.registration.active.postMessage(message, [messageChannel.port2])
}
})
}
/**
* Handle messages from Service Worker
*/
private handleMessage(message: SWResponse): void {
const handlers = this.messageHandlers.get(message.type)
if (handlers) {
handlers.forEach((handler) => {
try {
handler(message.data)
} catch (error) {
console.error(`[SWClient] Error handling message ${message.type}:`, error)
}
})
}
}
/**
* Register a message handler
*/
onMessage(type: string, handler: (data: unknown) => void): () => void {
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, [])
}
const handlers = this.messageHandlers.get(type)
if (handlers) {
handlers.push(handler)
}
// Return unsubscribe function
return () => {
const typeHandlers = this.messageHandlers.get(type)
if (typeHandlers) {
const index = typeHandlers.indexOf(handler)
if (index > -1) {
typeHandlers.splice(index, 1)
}
}
}
}
/**
* Start platform sync
*/
async startPlatformSync(): Promise<void> {
await this.sendMessage({ type: 'START_PLATFORM_SYNC' })
}
/**
* Stop platform sync
*/
async stopPlatformSync(): Promise<void> {
await this.sendMessage({ type: 'STOP_PLATFORM_SYNC' })
}
/**
* Start user content sync
*/
async startUserSync(userPubkey: string): Promise<void> {
await this.sendMessage({ type: 'START_USER_SYNC', data: { userPubkey } })
}
/**
* Start publish worker
*/
async startPublishWorker(): Promise<void> {
await this.sendMessage({ type: 'START_PUBLISH_WORKER' })
}
/**
* Stop publish worker
*/
async stopPublishWorker(): Promise<void> {
await this.sendMessage({ type: 'STOP_PUBLISH_WORKER' })
}
/**
* Start notification detector
*/
async startNotificationDetector(userPubkey: string): Promise<void> {
await this.sendMessage({ type: 'START_NOTIFICATION_DETECTOR', data: { userPubkey } })
}
/**
* Stop notification detector
*/
async stopNotificationDetector(): Promise<void> {
await this.sendMessage({ type: 'STOP_NOTIFICATION_DETECTOR' })
}
/**
* Check if Service Worker is ready
*/
async isReady(): Promise<boolean> {
try {
await this.register()
return this.registration?.active !== null
} catch {
return false
}
}
}
export const swClient = new ServiceWorkerClient()