refactor: split hooks types and utils by domain
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
type Listener<TPayload> = (payload: TPayload) => void;
|
||||
|
||||
type ListenerMap<TEvents extends Record<string, unknown>> = {
|
||||
[TKey in keyof TEvents]?: Set<Listener<TEvents[TKey]>>;
|
||||
};
|
||||
|
||||
function getListeners<
|
||||
TEvents extends Record<string, unknown>,
|
||||
TKey extends keyof TEvents,
|
||||
>(
|
||||
map: ListenerMap<TEvents>,
|
||||
key: TKey,
|
||||
): Set<Listener<TEvents[TKey]>> | undefined {
|
||||
return map[key] as Set<Listener<TEvents[TKey]>> | undefined;
|
||||
}
|
||||
|
||||
export class EventEmitter<TEvents extends Record<string, unknown>> {
|
||||
private readonly listeners: ListenerMap<TEvents> = {};
|
||||
|
||||
on<TKey extends keyof TEvents>(
|
||||
event: TKey,
|
||||
listener: Listener<TEvents[TKey]>,
|
||||
): () => void {
|
||||
const existing = getListeners(this.listeners, event);
|
||||
|
||||
if (existing) {
|
||||
existing.add(listener);
|
||||
} else {
|
||||
this.listeners[event] = new Set([listener]) as ListenerMap<TEvents>[TKey];
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.off(event, listener);
|
||||
};
|
||||
}
|
||||
|
||||
off<TKey extends keyof TEvents>(
|
||||
event: TKey,
|
||||
listener: Listener<TEvents[TKey]>,
|
||||
): void {
|
||||
const currentListeners = getListeners(this.listeners, event);
|
||||
|
||||
if (!currentListeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentListeners.delete(listener);
|
||||
|
||||
if (currentListeners.size === 0) {
|
||||
delete this.listeners[event];
|
||||
}
|
||||
}
|
||||
|
||||
emit<TKey extends keyof TEvents>(event: TKey, payload: TEvents[TKey]): void {
|
||||
const currentListeners = getListeners(this.listeners, event);
|
||||
|
||||
if (!currentListeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentListeners.forEach((listener) => {
|
||||
listener(payload);
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
for (const key of Object.keys(this.listeners) as (keyof TEvents)[]) {
|
||||
delete this.listeners[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
type SizeSnapshot = {
|
||||
width: number;
|
||||
height: number;
|
||||
pixelRatio: number;
|
||||
};
|
||||
|
||||
type SizeListener = (snapshot: SizeSnapshot) => void;
|
||||
|
||||
export class Sizes {
|
||||
private snapshot: SizeSnapshot;
|
||||
private readonly listeners = new Set<SizeListener>();
|
||||
private readonly handleResize = (): void => {
|
||||
this.snapshot = Sizes.readWindow();
|
||||
this.emit();
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.snapshot = Sizes.readWindow();
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
}
|
||||
|
||||
subscribe(listener: SizeListener): () => void {
|
||||
this.listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
getSnapshot(): SizeSnapshot {
|
||||
return this.snapshot;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
private emit(): void {
|
||||
this.listeners.forEach((listener) => listener(this.snapshot));
|
||||
}
|
||||
|
||||
private static readWindow(): SizeSnapshot {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
pixelRatio: Math.min(window.devicePixelRatio, 2),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
type TickListener = (delta: number, elapsed: number) => void;
|
||||
|
||||
export class Time {
|
||||
private readonly listeners = new Set<TickListener>();
|
||||
private animationFrameId = 0;
|
||||
private lastTick = performance.now();
|
||||
private elapsed = 0;
|
||||
|
||||
constructor() {
|
||||
this.tick = this.tick.bind(this);
|
||||
this.animationFrameId = window.requestAnimationFrame(this.tick);
|
||||
}
|
||||
|
||||
subscribe(listener: TickListener): () => void {
|
||||
this.listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
getElapsed(): number {
|
||||
return this.elapsed;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
window.cancelAnimationFrame(this.animationFrameId);
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
private tick(now: number): void {
|
||||
const delta = (now - this.lastTick) / 1000;
|
||||
this.lastTick = now;
|
||||
this.elapsed += delta;
|
||||
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(delta, this.elapsed);
|
||||
});
|
||||
|
||||
this.animationFrameId = window.requestAnimationFrame(this.tick);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type {
|
||||
LogContext,
|
||||
LogEntry,
|
||||
LogLevel,
|
||||
LoggerConfig,
|
||||
} from "@/types/logger/logger";
|
||||
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
||||
|
||||
const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
debug: 10,
|
||||
info: 20,
|
||||
warn: 30,
|
||||
error: 40,
|
||||
};
|
||||
|
||||
const LEVEL_LABELS: Record<LogLevel, string> = {
|
||||
debug: "DEBUG",
|
||||
info: "INFO",
|
||||
warn: "WARN",
|
||||
error: "ERROR",
|
||||
};
|
||||
|
||||
const LEVEL_STYLES: Record<LogLevel, string> = {
|
||||
debug: "color: #94a3b8; font-weight: 600;",
|
||||
info: "color: #60a5fa; font-weight: 600;",
|
||||
warn: "color: #f59e0b; font-weight: 600;",
|
||||
error: "color: #f87171; font-weight: 600;",
|
||||
};
|
||||
|
||||
const SCOPE_STYLE = "color: #e5e7eb; font-weight: 600;";
|
||||
const MESSAGE_STYLE = "color: inherit;";
|
||||
|
||||
class Logger {
|
||||
private readonly config: LoggerConfig;
|
||||
|
||||
constructor(config: LoggerConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
debug(scope: string, message: string, context?: LogContext): void {
|
||||
this.log("debug", scope, message, context);
|
||||
}
|
||||
|
||||
info(scope: string, message: string, context?: LogContext): void {
|
||||
this.log("info", scope, message, context);
|
||||
}
|
||||
|
||||
warn(scope: string, message: string, context?: LogContext): void {
|
||||
this.log("warn", scope, message, context);
|
||||
}
|
||||
|
||||
error(scope: string, message: string, context?: LogContext): void {
|
||||
this.log("error", scope, message, context);
|
||||
}
|
||||
|
||||
private log(
|
||||
level: LogLevel,
|
||||
scope: string,
|
||||
message: string,
|
||||
context?: LogContext,
|
||||
): void {
|
||||
if (!this.shouldLog(level)) return;
|
||||
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
scope,
|
||||
message,
|
||||
...(context ? { context } : {}),
|
||||
};
|
||||
|
||||
const formattedMessage = `%c[${LEVEL_LABELS[level]}]%c [${scope}]%c ${message}`;
|
||||
const args = [
|
||||
formattedMessage,
|
||||
LEVEL_STYLES[level],
|
||||
SCOPE_STYLE,
|
||||
MESSAGE_STYLE,
|
||||
entry,
|
||||
] as const;
|
||||
|
||||
switch (level) {
|
||||
case "debug":
|
||||
console.debug(...args);
|
||||
return;
|
||||
case "info":
|
||||
console.info(...args);
|
||||
return;
|
||||
case "warn":
|
||||
console.warn(...args);
|
||||
return;
|
||||
case "error":
|
||||
console.error(...args);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.config.minLevel];
|
||||
}
|
||||
}
|
||||
|
||||
function resolveMinLevel(): LogLevel {
|
||||
if (typeof window === "undefined") {
|
||||
return "info";
|
||||
}
|
||||
|
||||
return isDebugEnabled() ? "debug" : "info";
|
||||
}
|
||||
|
||||
export const logger = new Logger({
|
||||
minLevel: resolveMinLevel(),
|
||||
});
|
||||
Reference in New Issue
Block a user