410 lines
10 KiB
TypeScript
410 lines
10 KiB
TypeScript
import { logger } from './logging.js';
|
|
|
|
/**
|
|
* Event types for service worker internal communication
|
|
*/
|
|
export enum ServiceWorkerEvent {
|
|
// Cache events
|
|
CACHE_HIT = 'cache:hit',
|
|
CACHE_MISS = 'cache:miss',
|
|
CACHE_ERROR = 'cache:error',
|
|
CACHE_INVALIDATE = 'cache:invalidate',
|
|
CACHE_INVALIDATE_ALL = 'cache:invalidate_all',
|
|
CACHE_REVALIDATE = 'cache:revalidate',
|
|
|
|
// Update events
|
|
UPDATE_CHECK_START = 'update:check_start',
|
|
UPDATE_CHECK_COMPLETE = 'update:check_complete',
|
|
UPDATE_AVAILABLE = 'update:available',
|
|
UPDATE_APPLIED = 'update:applied',
|
|
UPDATE_ERROR = 'update:error',
|
|
|
|
// Network events
|
|
NETWORK_REQUEST_START = 'network:request_start',
|
|
NETWORK_REQUEST_COMPLETE = 'network:request_complete',
|
|
NETWORK_REQUEST_ERROR = 'network:request_error',
|
|
NETWORK_ONLINE = 'network:online',
|
|
NETWORK_OFFLINE = 'network:offline',
|
|
|
|
// Connection events
|
|
CLIENT_CONNECTED = 'connection:client_connected',
|
|
CLIENT_DISCONNECTED = 'connection:client_disconnected',
|
|
|
|
// Lifecycle events
|
|
INSTALL = 'lifecycle:install',
|
|
ACTIVATE = 'lifecycle:activate',
|
|
READY = 'lifecycle:ready',
|
|
}
|
|
|
|
/**
|
|
* Event payload interfaces
|
|
*/
|
|
export interface ICacheEventPayload {
|
|
url: string;
|
|
method?: string;
|
|
bytes?: number;
|
|
error?: string;
|
|
}
|
|
|
|
export interface IUpdateEventPayload {
|
|
oldVersion?: string;
|
|
newVersion?: string;
|
|
oldHash?: string;
|
|
newHash?: string;
|
|
error?: string;
|
|
}
|
|
|
|
export interface INetworkEventPayload {
|
|
url: string;
|
|
method?: string;
|
|
status?: number;
|
|
duration?: number;
|
|
bytes?: number;
|
|
error?: string;
|
|
}
|
|
|
|
export interface IConnectionEventPayload {
|
|
clientId?: string;
|
|
tabId?: string;
|
|
}
|
|
|
|
export interface ILifecycleEventPayload {
|
|
timestamp: number;
|
|
}
|
|
|
|
/**
|
|
* Union type for all event payloads
|
|
*/
|
|
export type TEventPayload =
|
|
| ICacheEventPayload
|
|
| IUpdateEventPayload
|
|
| INetworkEventPayload
|
|
| IConnectionEventPayload
|
|
| ILifecycleEventPayload
|
|
| Record<string, unknown>;
|
|
|
|
/**
|
|
* Event listener callback type
|
|
*/
|
|
export type TEventListener<T extends TEventPayload = TEventPayload> = (
|
|
event: ServiceWorkerEvent,
|
|
payload: T
|
|
) => void | Promise<void>;
|
|
|
|
/**
|
|
* Subscription interface
|
|
*/
|
|
export interface ISubscription {
|
|
unsubscribe: () => void;
|
|
}
|
|
|
|
/**
|
|
* Event bus for decoupled communication between service worker components.
|
|
* Implements a simple pub/sub pattern.
|
|
*/
|
|
export class EventBus {
|
|
private static instance: EventBus;
|
|
private listeners: Map<ServiceWorkerEvent, Set<TEventListener>>;
|
|
private globalListeners: Set<TEventListener>;
|
|
private eventHistory: Array<{ event: ServiceWorkerEvent; payload: TEventPayload; timestamp: number }>;
|
|
private readonly maxHistorySize = 100;
|
|
private debugMode = false;
|
|
|
|
private constructor() {
|
|
this.listeners = new Map();
|
|
this.globalListeners = new Set();
|
|
this.eventHistory = [];
|
|
}
|
|
|
|
/**
|
|
* Gets the singleton instance
|
|
*/
|
|
public static getInstance(): EventBus {
|
|
if (!EventBus.instance) {
|
|
EventBus.instance = new EventBus();
|
|
}
|
|
return EventBus.instance;
|
|
}
|
|
|
|
/**
|
|
* Enables or disables debug mode (logs all events)
|
|
*/
|
|
public setDebugMode(enabled: boolean): void {
|
|
this.debugMode = enabled;
|
|
}
|
|
|
|
/**
|
|
* Emits an event to all subscribed listeners
|
|
*/
|
|
public emit<T extends TEventPayload>(event: ServiceWorkerEvent, payload: T): void {
|
|
if (this.debugMode) {
|
|
logger.log('note', `[EventBus] Emit: ${event} ${JSON.stringify(payload)}`);
|
|
}
|
|
|
|
// Record in history
|
|
this.recordEvent(event, payload);
|
|
|
|
// Notify specific listeners
|
|
const specificListeners = this.listeners.get(event);
|
|
if (specificListeners) {
|
|
for (const listener of specificListeners) {
|
|
try {
|
|
const result = listener(event, payload);
|
|
if (result instanceof Promise) {
|
|
result.catch((err) => {
|
|
logger.log('error', `[EventBus] Async listener error for ${event}: ${err}`);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.log('error', `[EventBus] Listener error for ${event}: ${err}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify global listeners
|
|
for (const listener of this.globalListeners) {
|
|
try {
|
|
const result = listener(event, payload);
|
|
if (result instanceof Promise) {
|
|
result.catch((err) => {
|
|
logger.log('error', `[EventBus] Global async listener error for ${event}: ${err}`);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.log('error', `[EventBus] Global listener error for ${event}: ${err}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribes to a specific event
|
|
*/
|
|
public on<T extends TEventPayload>(
|
|
event: ServiceWorkerEvent,
|
|
listener: TEventListener<T>
|
|
): ISubscription {
|
|
if (!this.listeners.has(event)) {
|
|
this.listeners.set(event, new Set());
|
|
}
|
|
|
|
this.listeners.get(event)!.add(listener as TEventListener);
|
|
|
|
return {
|
|
unsubscribe: () => {
|
|
this.off(event, listener as TEventListener);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Subscribes to multiple events at once
|
|
*/
|
|
public onMany<T extends TEventPayload>(
|
|
events: ServiceWorkerEvent[],
|
|
listener: TEventListener<T>
|
|
): ISubscription {
|
|
const subscriptions = events.map((event) =>
|
|
this.on(event, listener as TEventListener)
|
|
);
|
|
|
|
return {
|
|
unsubscribe: () => {
|
|
subscriptions.forEach((sub) => sub.unsubscribe());
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Subscribes to all events
|
|
*/
|
|
public onAll<T extends TEventPayload>(listener: TEventListener<T>): ISubscription {
|
|
this.globalListeners.add(listener as TEventListener);
|
|
|
|
return {
|
|
unsubscribe: () => {
|
|
this.globalListeners.delete(listener as TEventListener);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Subscribes to an event for only one emission
|
|
*/
|
|
public once<T extends TEventPayload>(
|
|
event: ServiceWorkerEvent,
|
|
listener: TEventListener<T>
|
|
): ISubscription {
|
|
const onceListener: TEventListener = (evt, payload) => {
|
|
this.off(event, onceListener);
|
|
return listener(evt, payload as T);
|
|
};
|
|
|
|
return this.on(event, onceListener);
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes a listener from an event
|
|
*/
|
|
public off(event: ServiceWorkerEvent, listener: TEventListener): void {
|
|
const listeners = this.listeners.get(event);
|
|
if (listeners) {
|
|
listeners.delete(listener);
|
|
if (listeners.size === 0) {
|
|
this.listeners.delete(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all listeners for an event
|
|
*/
|
|
public removeAllListeners(event?: ServiceWorkerEvent): void {
|
|
if (event) {
|
|
this.listeners.delete(event);
|
|
} else {
|
|
this.listeners.clear();
|
|
this.globalListeners.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the count of listeners for an event
|
|
*/
|
|
public listenerCount(event: ServiceWorkerEvent): number {
|
|
const listeners = this.listeners.get(event);
|
|
return (listeners?.size ?? 0) + this.globalListeners.size;
|
|
}
|
|
|
|
/**
|
|
* Gets the event history
|
|
*/
|
|
public getHistory(): Array<{ event: ServiceWorkerEvent; payload: TEventPayload; timestamp: number }> {
|
|
return [...this.eventHistory];
|
|
}
|
|
|
|
/**
|
|
* Gets events of a specific type from history
|
|
*/
|
|
public getHistoryByType(event: ServiceWorkerEvent): Array<{ payload: TEventPayload; timestamp: number }> {
|
|
return this.eventHistory
|
|
.filter((entry) => entry.event === event)
|
|
.map(({ payload, timestamp }) => ({ payload, timestamp }));
|
|
}
|
|
|
|
/**
|
|
* Clears the event history
|
|
*/
|
|
public clearHistory(): void {
|
|
this.eventHistory = [];
|
|
}
|
|
|
|
/**
|
|
* Waits for an event to be emitted (returns a promise)
|
|
*/
|
|
public waitFor<T extends TEventPayload>(
|
|
event: ServiceWorkerEvent,
|
|
timeout?: number
|
|
): Promise<T> {
|
|
return new Promise((resolve, reject) => {
|
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
const subscription = this.once<T>(event, (_, payload) => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
resolve(payload);
|
|
});
|
|
|
|
if (timeout) {
|
|
timeoutId = setTimeout(() => {
|
|
subscription.unsubscribe();
|
|
reject(new Error(`Timeout waiting for event: ${event}`));
|
|
}, timeout);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Records an event in history
|
|
*/
|
|
private recordEvent(event: ServiceWorkerEvent, payload: TEventPayload): void {
|
|
this.eventHistory.push({
|
|
event,
|
|
payload,
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
// Trim history if needed
|
|
if (this.eventHistory.length > this.maxHistorySize) {
|
|
this.eventHistory = this.eventHistory.slice(-this.maxHistorySize);
|
|
}
|
|
}
|
|
|
|
// ===================
|
|
// Convenience Methods
|
|
// ===================
|
|
|
|
/**
|
|
* Emits a cache hit event
|
|
*/
|
|
public emitCacheHit(url: string, bytes?: number): void {
|
|
this.emit(ServiceWorkerEvent.CACHE_HIT, { url, bytes });
|
|
}
|
|
|
|
/**
|
|
* Emits a cache miss event
|
|
*/
|
|
public emitCacheMiss(url: string): void {
|
|
this.emit(ServiceWorkerEvent.CACHE_MISS, { url });
|
|
}
|
|
|
|
/**
|
|
* Emits a cache error event
|
|
*/
|
|
public emitCacheError(url: string, error?: string): void {
|
|
this.emit(ServiceWorkerEvent.CACHE_ERROR, { url, error });
|
|
}
|
|
|
|
/**
|
|
* Emits a cache invalidation event
|
|
*/
|
|
public emitCacheInvalidate(url?: string): void {
|
|
if (url) {
|
|
this.emit(ServiceWorkerEvent.CACHE_INVALIDATE, { url });
|
|
} else {
|
|
this.emit(ServiceWorkerEvent.CACHE_INVALIDATE_ALL, {});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits an update available event
|
|
*/
|
|
public emitUpdateAvailable(oldVersion: string, newVersion: string, oldHash: string, newHash: string): void {
|
|
this.emit(ServiceWorkerEvent.UPDATE_AVAILABLE, { oldVersion, newVersion, oldHash, newHash });
|
|
}
|
|
|
|
/**
|
|
* Emits an update applied event
|
|
*/
|
|
public emitUpdateApplied(newVersion: string, newHash: string): void {
|
|
this.emit(ServiceWorkerEvent.UPDATE_APPLIED, { newVersion, newHash });
|
|
}
|
|
|
|
/**
|
|
* Emits a network online event
|
|
*/
|
|
public emitNetworkOnline(): void {
|
|
this.emit(ServiceWorkerEvent.NETWORK_ONLINE, {});
|
|
}
|
|
|
|
/**
|
|
* Emits a network offline event
|
|
*/
|
|
public emitNetworkOffline(): void {
|
|
this.emit(ServiceWorkerEvent.NETWORK_OFFLINE, {});
|
|
}
|
|
}
|
|
|
|
// Export singleton getter for convenience
|
|
export const getEventBus = (): EventBus => EventBus.getInstance();
|