import { symbolToken, SymbolToken } from '@servicetitan/react-ioc'; import mitt from 'mitt'; import { JSON, Serializable } from './types'; export const EVENT_BUS_TOKEN = symbolToken('EVENT_BUS_TOKEN'); export function typedEventBusToken>() { return symbolToken>('EVENT_BUS_TOKEN'); } export type EventHandler = ( data: [T] extends [never] ? Event extends SymbolToken ? Serializable : Event extends keyof EventTypes ? EventTypes[Event] : T : Serializable ) => void; type JobPayload = JSON | void; export type EventType = string | number | symbol; export type SymbolTokenEventType = U extends SymbolToken ? SymbolToken : U; export type JobEventHandler = (defer: { /** * Successfully complete a job. * @param result The value to return from the successfully completed job. */ resolve( ...result: Event extends SymbolToken ? undefined extends ResultType ? [Serializable?] : [Serializable] : [JobPayload] ): void; /** * Fail a job. * @param reason The error to return from the failed job. */ reject(reason?: string | Error): void; }) => void; export class EventBus = never> { job = { /** * Register a job event handler. Jobs event handlers return a result or an error. * @param event The job event to handle. * @param handler The handler to call when the event occurs. This handler is passed a `resolve` callback to return a successful result, and a `reject` callback to return an error. */ on: | keyof EventTypes>( event: Event, handler: JobEventHandler ) => { this.validateEvent(event); this.emitter.on(event, handler); }, /** * Deregister a job event handler. * @param event The previously registered event. * @param handler The previously registered handler. */ off: | keyof EventTypes>( event: Event, handler: JobEventHandler ) => { this.emitter.off(event, handler); }, /** * Send a job event. * @param event The event to send. * @returns a Promise that resolves or rejects depending on whether the job completes successfully. */ emit: | keyof EventTypes>(event: Event) => { this.validateEvent(event); return new Promise< Event extends SymbolToken ? Serializable : JobPayload >((resolve, reject) => { this.emitter.emit(event, { resolve, reject: (reason?: string | Error) => { if (typeof reason === 'string' || reason instanceof Error) { reject(reason); } else { reject(); } }, }); }); }, }; private emitter = mitt(); /** * Register an event handler. * @param event The event to handle. * @param handler The handler to call when the event occurs. */ on = = never, Event extends SymbolToken | keyof EventTypes = any>( event: SymbolTokenEventType, handler: EventHandler ) => { this.validateEvent(event); this.emitter.on(event, handler); }; /** * Deregister an event handler. * @param event The previously registered event. * @param handler The previously registered handler. */ off = < T extends Serializable = never, Event extends SymbolToken | keyof EventTypes = any, >( event: SymbolTokenEventType, handler: EventHandler ) => { this.emitter.off(event, handler); }; /** * Send an event. * @param event The event to send. * @param data The data to pass to the event's handler. */ emit = , Event extends SymbolToken | keyof EventTypes = any>( event: SymbolTokenEventType, ...data: Event extends SymbolToken ? undefined extends T ? [T?] : [T] : [EventTypes] extends [never] ? [T?] : Event extends keyof EventTypes ? undefined extends EventTypes[Event] ? [EventTypes[Event]?] : [EventTypes[Event]] extends [never] ? [] : [EventTypes[Event]] : never ) => { this.validateEvent(event); this.emitter.emit(event, data[0]); }; private validateEvent(event: any) { if (process.env.NODE_ENV === 'production') { return; } const name = this.getName(event); if (name === undefined || /^[^:]+:.+/.test(name) || allowListedEvents.has(name)) { return; } throw new Error('event must have format {module}:{name}'); } private getName(event: any) { if (typeof event === 'string') { return event; } if (typeof event === 'symbol') { return Symbol.keyFor(event) ?? ''; } return undefined; } } const allowListedEvents = new Set([ ':widgets:message', '[EH] Module Link Clicked', '[EH] Module Loaded', '[internal] reset', 'blink', 'CLOSE_COMPONENT', 'CLOSE_JOB_EDIT_FLYOUT', 'company-profile-request-save', 'company-profile-saved-error', 'company-profile-saved', 'dismiss-discrepancy-event', 'dismiss-discrepancy-map-event', 'dispatch-JobEditFlyout-fetchData', 'DRAWER_CREATED_CUSTOMER', 'employees-emails-processed', 'employees-request-save', 'employees-saved-error', 'employees-saved', 'ium-ep-cancel-changes', 'ium-ep-form-can-be-submitted-change', 'ium-ep-save-changes', 'loaded', 'MESSAGE_TECHNICIAN', 'my-custom-event', 'NEW_EVENT', 'NonJobEventSelectedEvent', 'ON_WIDGET_INIT', 'OPEN_ASSIST_SCREEN', 'OPEN_CREATE_CUSTOMER_DRAWER', 'OPEN_DC_SIDE_PANEL_COMPONENT', 'OPEN_JOB_EDIT_FLYOUT', 'OPEN_RESCHEDULE_FLYOUT', 'open-troubleshooter', 'resolve-discrepancy-map-event', 'restore-discrepancy-event', 'setLabel', 'SHOW_POP_UP_MESSAGE', 'TEAMS_SELECTED', 'technicians-emails-processed', 'technicians-request-save', 'technicians-saved-error', 'technicians-saved', 'timesheet-activity-saved', 'TOGGLE_DC_SIDE_PANEL_COMPONENT', 'updateUserEmail', 'VIEW_NON_JOB_EVENT', 'visibility', ]);