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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | 1x 1x 11x 11x 1x 21x 21x 21x 1x 21x 7x 7x 1x 6x 6x 1x 5x 2x 4x 3x 14x 14x 6x 5x 5x 5x 20x 20x 5x 1x | /**
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import type { Theme } from '@siemens/element-ng/theme';
import { fromEvent, Observable, Subject } from 'rxjs';
type Filter = { key: unknown; value: unknown } | DateRangeChange | TimeZoneChange;
type LanguageChange = string;
type ThemeChange = Theme;
type DateRangeChange = { key: 'dateRange'; value: { start: Date; end: Date } };
type TimeZoneChange = { key: 'timeZone'; value: string };
/**
* Default event type union used by `SiEventBus` when no generic parameter is provided.
* Consumers can override this by passing their own event type union as a generic argument,
* e.g. `inject(SiEventBus<MyCustomEventType>)`.
*
* The following events are available by default:
* - `filter` – a single {@link Filter} or an array of filters (including date range and time zone changes)
* - `languageChange` – the new language as a string
* - `themeChange` – the new {@link Theme}
*/
export type SiEventType =
| { name: 'filter'; data: Filter[] }
| { name: 'languageChange'; data: LanguageChange }
| { name: 'themeChange'; data: ThemeChange };
/**
* Converts the union {@link SiEventType} into an object type with event names as keys
* and data types as values.
*
* @example
* ```ts
* Input union:
* | { name: 'filter'; data: Filter[] }
* | { name: 'languageChange'; data: LanguageChange }
* | { name: 'themeChange'; data: ThemeChange }
*
* Output object type:
* { filter: Filter[]; languageChange: LanguageChange; themeChange: ThemeChange }
* ```
*/
type EventNameToData<ET extends { name: string; data: unknown }> = {
[K in ET as K['name']]: K['data'];
};
/**
* Narrows array-typed event data to only include items whose `key` property
* matches one of the given keys. If `Data` is not an array, it is returned as-is.
*
* @typeParam Data - The event data type to narrow.
* @typeParam K - The string literal union of keys to filter by.
*
* @example
* ```ts
* NarrowByKeys<Filter[], 'dateRange'>
* // => (Filter & { key: 'dateRange' })[]
*
* NarrowByKeys<string, 'dateRange'>
* // => string (unchanged, not an array)
* ```
*/
type NarrowByKeys<Data, K extends string> = Data extends (infer U)[]
? (U & { key: K; value: unknown })[]
: Data;
export class SiEventBusBase<ET extends { name: string; data: unknown } = SiEventType> {
private eventObservables: Map<ET['name'], Subject<any>> = new Map();
private customEventSuffix = 'θ';
/**
* Shared state symbol used as a key on `window` to store the event bus state.
* A Symbol is used instead of a string key so the property is not discoverable
* via `Object.keys(window)` or accidental string-based access.
*/
private static sharedStateSymbol = Symbol.for('eventBusSharedState');
/**
* Returns the shared events state object stored on `window`.
*
* Widgets running in separate Angular runtimes each get their own `SiEventBusBase`
* instance, so per-instance state would be out of sync. By storing the state on
* `window`, all instances share a single source of truth.
*
* The property is defined with `writable: false` and `configurable: false` so
* that outside code cannot replace the object, while its own properties remain
* mutable for internal event state updates.
*/
private get sharedEventsState(): Record<string, unknown> {
const win = window as unknown as Record<symbol, unknown>;
const key = SiEventBusBase.sharedStateSymbol;
if (!win[key]) {
Object.defineProperty(window, key, {
value: {},
writable: false,
enumerable: false,
configurable: false
});
}
return win[key] as Record<string, unknown>;
}
/**
* Returns a point-in-time snapshot of the current event bus state.
*
* @returns The full state object when called without arguments.
* @param eventName - Optional event name to retrieve data for a single event.
* @param keys - Optional array of keys to narrow array-typed event data
* (e.g. filter items) to only those matching the given keys.
* A custom return type `R` can be provided to override the inferred
* payload type when keys are passed.
*
* @example
* ```ts
* // With a custom return type override
* eventBus.snapshot<{key: string; value: boolean}[]>('filter', ['processed', 'sent']);
* // => {key: 'processed' | 'sent', value: boolean}[]
* ```
*/
snapshot(): EventNameToData<ET | SiEventType>;
snapshot<
R = never,
A extends keyof EventNameToData<ET | SiEventType> = keyof EventNameToData<ET | SiEventType>,
K extends string = string
>(
eventName: A,
keys: K[]
): [R] extends [never]
? NarrowByKeys<EventNameToData<ET | SiEventType>[A], K>
: NarrowByKeys<R, K>;
snapshot<A extends keyof EventNameToData<ET | SiEventType>>(
eventName: A
): EventNameToData<ET | SiEventType>[A];
snapshot<A extends keyof EventNameToData<ET | SiEventType>, K extends string>(
eventName?: A,
keys?: K[]
):
| EventNameToData<ET>
| EventNameToData<ET>[A]
| NarrowByKeys<EventNameToData<ET>[A], K>
| undefined {
const state = this.sharedEventsState as EventNameToData<ET>;
if (!eventName) {
return state;
}
const data = state[eventName];
if (!data) {
return undefined;
}
if (keys?.length) {
const items = Array.isArray(data) ? data : [data];
return items.filter(f => keys.includes(f.key)) as EventNameToData<ET>[A];
}
return data;
}
/**
* Emits an event, updating the shared state so that all observables
* returned by {@link on} for the same event type receive the new value.
*
* @param eventName - The name of the event to emit.
* @param payload - The data associated with the event.
*/
emit<A extends keyof EventNameToData<ET | SiEventType>>(
eventName: A,
payload?: EventNameToData<ET | SiEventType>[A]
): void {
this.sharedEventsState[String(eventName)] = payload;
// We just propagate this as custom event so widgets in other angular runtime contexts can also be notified
window.dispatchEvent(
new CustomEvent(`${this.customEventSuffix}${String(eventName)}`, { detail: payload })
);
}
/**
* Subscribes to an event and returns an observable that emits whenever
* {@link emit} is called with the matching event name.
*
* @param eventName - The name of the event to listen for.
* @returns An observable of the event payload. A custom return type `R` can
* be provided to override the inferred payload type.
*
* @example
* ```ts
* eventBus.on('themeChange').subscribe(theme => {
* console.log('Theme changed to', theme);
* });
*
* // With a custom return type override
* eventBus.on<MyCustomType>('filter').subscribe(data => {
* // data is typed as MyCustomType
* });
* ```
*/
on<
R = never,
A extends keyof EventNameToData<ET | SiEventType> = keyof EventNameToData<ET | SiEventType>
>(eventName: A): Observable<[R] extends [never] ? EventNameToData<ET | SiEventType>[A] : R> {
if (!this.eventObservables.has(eventName)) {
const eventSubject = new Subject<
[R] extends [never] ? EventNameToData<ET | SiEventType>[A] : R
>();
this.eventObservables.set(eventName, eventSubject);
// emit will dispatch a custom event on window, we listen to that and forward to our subject
fromEvent(window, `${this.customEventSuffix}${String(eventName)}`).subscribe(
(event: Event) => {
const customEvent = event as CustomEvent;
this.eventObservables.get(eventName)!.next(customEvent.detail);
}
);
return eventSubject.asObservable();
}
return this.eventObservables.get(eventName)!.asObservable();
}
}
|