events.ts
1 import type { SetReturnType } from 'type-fest'; 2 import type { AugmentedBrowser as Browser, Events } from 'wxt/browser'; 3 4 import { Logger } from '@/utils/logger'; 5 6 import { isObject } from './helpers/isObject'; 7 import type { StorageEvent } from './storage/types'; 8 9 interface I18nLocaleEvent { 10 type: 'I18nLocaleEvent'; 11 locale: string; 12 } 13 14 interface StorageUpdatedEvent { 15 type: 'StorageUpdatedEvent'; 16 data: StorageEvent; 17 } 18 19 interface GetTabIdEvent { 20 type: 'GetTabId'; 21 } 22 23 interface GetUnloadStylesClassWithPrefixEvent { 24 type: 'GetUnloadStylesClassWithPrefix'; 25 } 26 27 interface GetUnloadStylesClassEvent { 28 type: 'GetUnloadStylesClass'; 29 } 30 31 type Event = 32 | I18nLocaleEvent 33 | StorageUpdatedEvent 34 | GetTabIdEvent 35 | GetUnloadStylesClassWithPrefixEvent 36 | GetUnloadStylesClassEvent; 37 38 type RawEventHandler = OnMessageHandlerWithMessageType<Event>; 39 type EventHandler = SetReturnType<RawEventHandler, void | ReturnType<RawEventHandler>>; 40 41 const EVENT_IDS: ReadonlySet<Event['type']> = new Set([ 42 'I18nLocaleEvent', 43 'StorageUpdatedEvent', 44 'GetTabId', 45 'GetUnloadStylesClassWithPrefix', 46 'GetUnloadStylesClass', 47 ]); 48 const logger = Logger.create('events'); 49 50 export class EventDispatcher { 51 private static dispatchEvent<Response = void>(event: Event) { 52 return EventDispatcher.dispatch<Response>('runtime', event, event => 53 browser.runtime.sendMessage(event), 54 ); 55 } 56 57 static async dispatch<Response>( 58 target: string, 59 event: Event, 60 sendMessage: (event: Event) => Promise<Response>, 61 ) { 62 try { 63 logger.debug('dispatching event', event, 'to', target); 64 return await sendMessage(event); 65 } catch (err) { 66 if (String(err).includes('Receiving end does not exist')) { 67 // it's ok if there are no receivers 68 logger.debug(target, 'dispatching error:', err); 69 } else { 70 logger.error(target, 'dispatching error:', err); 71 } 72 } 73 } 74 75 static dispatchI18nLocale = (locale: I18nLocaleEvent['locale']) => 76 EventDispatcher.dispatchEvent({ type: 'I18nLocaleEvent', locale }); 77 78 static dispatchStorageUpdate = (data: StorageUpdatedEvent['data']) => 79 EventDispatcher.dispatchEvent({ type: 'StorageUpdatedEvent', data }); 80 81 static getTabId = () => EventDispatcher.dispatchEvent<number>({ type: 'GetTabId' }); 82 83 static getUnloadStylesClassWithPrefix = () => 84 EventDispatcher.dispatchEvent<[prefix: string, className: string]>({ 85 type: 'GetUnloadStylesClassWithPrefix', 86 }); 87 88 static getUnloadStylesClass = () => 89 EventDispatcher.dispatchEvent<string>({ type: 'GetUnloadStylesClass' }); 90 } 91 92 export class EventListener implements Disposable { 93 readonly #logger: Logger; 94 readonly #unsubscribe: () => void; 95 96 constructor(logger: Logger, handler: EventHandler) { 97 this.#logger = logger.getChildLogger('EventListener'); 98 99 this.#logger.debug('setting up handlers'); 100 101 const handleEvent: OnMessageHandler = (e: unknown, ...args) => { 102 try { 103 if (isEvent(e)) { 104 this.#logger.debug('received event', e); 105 return (handler as RawEventHandler)(e as Event, ...args); 106 } 107 } catch (err) { 108 this.#logger.error('failed to handle event', e, err); 109 } 110 }; 111 112 browser.runtime.onMessage.addListener(handleEvent); 113 114 this.#unsubscribe = () => { 115 browser.runtime.onMessage.removeListener(handleEvent); 116 }; 117 } 118 119 [Symbol.dispose]() { 120 this.#unsubscribe(); 121 } 122 } 123 124 const isEvent = (value: unknown): value is Event => 125 isObject(value) && 'type' in value && EVENT_IDS.has(value.type as never); 126 127 type OnMessageHandler = 128 Browser['runtime']['onMessage'] extends Events.Event<infer Handler> ? Handler : never; 129 130 type OnMessageHandlerWithMessageType<Message> = OnMessageHandler extends ( 131 message: unknown, 132 ...args: infer Rest 133 ) => infer Res 134 ? (message: Message, ...args: Rest) => Res 135 : never;