Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 95x 95x 95x 95x 1x 1x 1x 80x 80x 80x 32x 32x 32x 32x 32x 80x 80x 80x | /**
* Typed search-lifecycle events, dispatched on `window` as `CustomEvent`s.
*
* Mirrors the pattern in {@link "../hotkeys/events"} — we use `window`-dispatched
* events rather than lifting state into context so producers (the search hook,
* the results table, the popup load path) stay decoupled from consumers. The
* sole consumer today is the badge controller ({@link "../utils/badgeController"}),
* but these are general-purpose: history, analytics, or telemetry could subscribe
* later without touching the producers.
*
* {@link emitSearchEvent} / {@link onSearchEvent} are thin typed wrappers over
* `dispatchEvent`/`addEventListener` so payloads are checked at the call site
* (no new dependency, unlike a dedicated emitter library).
*
* @category Events
* @source
*/
/**
* Search-lifecycle event names. The string values are the actual `CustomEvent`
* types dispatched on `window` (kept in the `chempal:` namespace, matching the
* hotkey events). Use the enum member everywhere — never the raw string.
* @source
*/
export enum SearchEvent {
/** A search has started; the loading animation should begin. */
STARTED = 'chempal:search-started',
/** The current visible/filtered result count changed (streaming, filtering, exclusion). */
RESULTS_COUNT = 'chempal:search-results-count',
/** A search finished (successfully or with zero results). */
COMPLETED = 'chempal:search-completed',
/** A search was aborted by the user. */
ABORTED = 'chempal:search-aborted',
/** A search failed with an error. */
FAILED = 'chempal:search-failed',
}
/**
* What every terminal search event reports about the run that just ended, so a
* consumer can tell a full search from one that stopped early without knowing
* how the search itself is orchestrated.
* @source
*/
export interface SearchOutcomeDetail {
/** Products the search produced before it ended. */
count: number;
/** Wall-clock milliseconds from the search starting to it ending. */
durationMs: number;
/** Suppliers the search actually queried (post shipping/permission filtering). */
suppliersQueried: number;
/** How many of those had finished when the search ended. */
suppliersCompleted: number;
}
/**
* Maps each {@link SearchEvent} to the shape of its `CustomEvent.detail`.
* `undefined` means the event carries no payload.
*/
export interface SearchEventDetailMap {
[SearchEvent.STARTED]: { query: string };
[SearchEvent.RESULTS_COUNT]: { count: number };
// `abortReason` is set when the stream drained only because the search was
// aborted mid-flight — the run completed, but not all of the work did.
[SearchEvent.COMPLETED]: SearchOutcomeDetail & { abortReason?: string };
[SearchEvent.ABORTED]: SearchOutcomeDetail & { reason?: string };
[SearchEvent.FAILED]: SearchOutcomeDetail & { error?: string };
}
/** Union of all search-event names (equivalent to {@link SearchEvent}). */
export type SearchEventType = keyof SearchEventDetailMap;
/**
* Dispatch a search-lifecycle event on `window` with a typed payload.
* @param type - The event ({@link SearchEvent} member).
* @param detail - The payload matching {@link SearchEventDetailMap} for `type`.
* @example
* ```ts
* emitSearchEvent(SearchEvent.STARTED, { query: "acetone" });
* emitSearchEvent(SearchEvent.RESULTS_COUNT, { count: 12 });
* emitSearchEvent(SearchEvent.ABORTED, {
* count: 3,
* durationMs: 1240,
* suppliersQueried: 8,
* suppliersCompleted: 2,
* reason: "user_aborted",
* });
* ```
* @source
*/
export function emitSearchEvent<K extends SearchEventType>(
type: K,
...[detail]: SearchEventDetailMap[K] extends undefined ? [] : [SearchEventDetailMap[K]]
): void {
window.dispatchEvent(new CustomEvent(type, { detail }));
}
/**
* Subscribe to a search-lifecycle event. Returns an unsubscribe function, so it
* drops straight into a `useEffect` cleanup.
* @param type - The event ({@link SearchEvent} member).
* @param handler - Receives the typed `detail` for `type`.
* @returns A function that removes the listener.
* @example
* ```ts
* useEffect(() => onSearchEvent(SearchEvent.COMPLETED, ({ count, durationMs }) => {
* console.log("done", count, durationMs);
* }), []);
* ```
* @source
*/
export function onSearchEvent<K extends SearchEventType>(
type: K,
handler: (detail: SearchEventDetailMap[K]) => void,
): () => void {
const listener = (event: Event) => {
// DOM `addEventListener` types the callback arg as `Event`; this event is
// only ever dispatched via `emitSearchEvent` as a `CustomEvent` with the
// matching `detail`, so narrowing to `CustomEvent` is safe.
handler((event as CustomEvent<SearchEventDetailMap[K]>).detail);
};
window.addEventListener(type, listener);
return () => window.removeEventListener(type, listener);
}
|