first commit
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
export type EventListener<TEvent> = (event: TEvent) => void;
|
||||
export type Unsubscribe = () => void;
|
||||
|
||||
export interface EventBusListenerError<TEvent> {
|
||||
error: unknown;
|
||||
event: TEvent;
|
||||
listener: EventListener<TEvent>;
|
||||
}
|
||||
|
||||
export interface EventBusOptions<TEvent> {
|
||||
onListenerError?: (error: EventBusListenerError<TEvent>) => void;
|
||||
}
|
||||
|
||||
export class EventBus<TEvent> {
|
||||
private readonly listeners = new Set<EventListener<TEvent>>();
|
||||
|
||||
constructor(private readonly options: EventBusOptions<TEvent> = {}) {}
|
||||
|
||||
subscribe(listener: EventListener<TEvent>): Unsubscribe {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
publish(event: TEvent) {
|
||||
for (const listener of this.listeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (error) {
|
||||
this.options.onListenerError?.({
|
||||
error,
|
||||
event,
|
||||
listener,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.listeners.size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export type ScheduledJob = {
|
||||
name: string;
|
||||
intervalMs: number;
|
||||
run: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
export class Scheduler {
|
||||
private readonly timers = new Map<string, number>();
|
||||
|
||||
add(job: ScheduledJob) {
|
||||
if (this.timers.has(job.name)) {
|
||||
throw new Error(`Scheduled job "${job.name}" already exists.`);
|
||||
}
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
void job.run();
|
||||
}, job.intervalMs);
|
||||
|
||||
this.timers.set(job.name, timer);
|
||||
}
|
||||
|
||||
stop() {
|
||||
for (const timer of this.timers.values()) {
|
||||
window.clearInterval(timer);
|
||||
}
|
||||
|
||||
this.timers.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface TelemetryIngestor {
|
||||
name: string;
|
||||
flushIntervalMs: number;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
flush(): void | Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { assert } from "@/lib/assertions";
|
||||
import type { ApiRequestEvent, CloudProvider } from "./types";
|
||||
|
||||
const CLOUD_PROVIDERS: ReadonlySet<CloudProvider> = new Set([
|
||||
"aws",
|
||||
"azure",
|
||||
"gcp",
|
||||
"system",
|
||||
]);
|
||||
|
||||
export function assertApiRequestEvent(
|
||||
event: ApiRequestEvent,
|
||||
): asserts event is ApiRequestEvent {
|
||||
assertCloudProvider(event.provider);
|
||||
assertNonEmptyString(event.service, "service");
|
||||
assertNonEmptyString(event.method, "method");
|
||||
assertNonEmptyString(event.path, "path");
|
||||
assertNonNegativeFiniteNumber(event.statusCode, "statusCode");
|
||||
assertNonNegativeFiniteNumber(event.latencyMs, "latencyMs");
|
||||
assertPositiveFiniteNumber(event.timestamp, "timestamp");
|
||||
}
|
||||
|
||||
export function assertCloudProvider(
|
||||
provider: CloudProvider,
|
||||
): asserts provider is CloudProvider {
|
||||
assert(
|
||||
CLOUD_PROVIDERS.has(provider),
|
||||
`Invalid cloud provider: ${String(provider)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function assertNonEmptyString(value: string, field: string) {
|
||||
assert(
|
||||
typeof value === "string" && value.trim() !== "",
|
||||
`Invalid API request event "${field}": expected string.`,
|
||||
);
|
||||
}
|
||||
|
||||
function assertNonNegativeFiniteNumber(value: number, field: string) {
|
||||
assert(
|
||||
typeof value === "number" && Number.isFinite(value) && value >= 0,
|
||||
`Invalid API request event "${field}": expected non-negative number.`,
|
||||
);
|
||||
}
|
||||
|
||||
function assertPositiveFiniteNumber(value: number, field: string) {
|
||||
assert(
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0,
|
||||
`Invalid API request event "${field}": expected positive number.`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export { EventBus } from "./EventBus";
|
||||
export { Scheduler } from "./Scheduler";
|
||||
export {
|
||||
apiRequestEventBus,
|
||||
emitApiRequest,
|
||||
subscribeApiRequests,
|
||||
} from "./requestEventBus";
|
||||
export { assertApiRequestEvent, assertCloudProvider } from "./assertions";
|
||||
export { assert, assertDefined, assertNever } from "@/lib/assertions";
|
||||
export type { ApiRequestEvent, CloudProvider } from "./types";
|
||||
export type {
|
||||
EventBusListenerError,
|
||||
EventBusOptions,
|
||||
EventListener,
|
||||
Unsubscribe,
|
||||
} from "./EventBus";
|
||||
export type { ScheduledJob } from "./Scheduler";
|
||||
export type { TelemetryIngestor } from "./TelemetryIngestor";
|
||||
@@ -0,0 +1,23 @@
|
||||
import { EventBus } from "./EventBus";
|
||||
import { assertApiRequestEvent } from "./assertions";
|
||||
import type { ApiRequestEvent } from "./types";
|
||||
|
||||
export const apiRequestEventBus = new EventBus<ApiRequestEvent>({
|
||||
onListenerError: ({ error, event }) => {
|
||||
console.warn("[Telemetry] API request listener failed", {
|
||||
error,
|
||||
event,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function subscribeApiRequests(
|
||||
listener: (event: ApiRequestEvent) => void,
|
||||
) {
|
||||
return apiRequestEventBus.subscribe(listener);
|
||||
}
|
||||
|
||||
export function emitApiRequest(event: ApiRequestEvent) {
|
||||
assertApiRequestEvent(event);
|
||||
apiRequestEventBus.publish(event);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export type CloudProvider = "aws" | "azure" | "gcp" | "system";
|
||||
|
||||
export interface ApiRequestEvent {
|
||||
provider: CloudProvider;
|
||||
service: string;
|
||||
method: string;
|
||||
path: string;
|
||||
statusCode: number;
|
||||
latencyMs: number;
|
||||
timestamp: number;
|
||||
}
|
||||
Reference in New Issue
Block a user