/ src / events.ts
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;