Created
September 18, 2025 14:44
-
-
Save eladcandroid/0d002548ad3ab5ed94dfcbd23d54c454 to your computer and use it in GitHub Desktop.
Angular Singleton Pattern Example - Configuration Service and Logger Service with single instance guarantee
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Singleton Pattern Example - Configuration and Logger Services | |
import { Injectable } from '@angular/core'; | |
import { HttpClient } from '@angular/common/http'; | |
import { APP_INITIALIZER } from '@angular/core'; | |
// Configuration Model | |
export interface AppConfig { | |
apiUrl: string; | |
version: string; | |
features: { | |
enableAnalytics: boolean; | |
enableChat: boolean; | |
enableDarkMode: boolean; | |
}; | |
environment: 'development' | 'staging' | 'production'; | |
} | |
// SINGLETON CONFIGURATION SERVICE | |
// Ensures only one instance exists across the entire application | |
@Injectable({ | |
providedIn: 'root' // This ensures singleton at root level | |
}) | |
export class ConfigurationService { | |
private static instance: ConfigurationService; | |
private config: AppConfig; | |
private configLoaded = false; | |
constructor(private http: HttpClient) { | |
// Ensure single instance | |
if (ConfigurationService.instance) { | |
return ConfigurationService.instance; | |
} | |
ConfigurationService.instance = this; | |
console.log('ConfigurationService singleton instance created'); | |
} | |
// Load configuration once at app startup | |
async loadConfiguration(): Promise<AppConfig> { | |
if (this.configLoaded) { | |
return this.config; | |
} | |
try { | |
this.config = await this.http.get<AppConfig>('/assets/config.json').toPromise(); | |
this.configLoaded = true; | |
console.log('Configuration loaded:', this.config); | |
return this.config; | |
} catch (error) { | |
console.error('Failed to load configuration, using defaults', error); | |
this.config = this.getDefaultConfig(); | |
this.configLoaded = true; | |
return this.config; | |
} | |
} | |
getConfig(): AppConfig { | |
if (!this.configLoaded) { | |
throw new Error('Configuration not loaded. Call loadConfiguration() first'); | |
} | |
return this.config; | |
} | |
getValue(key: keyof AppConfig): any { | |
return this.config[key]; | |
} | |
isFeatureEnabled(feature: keyof AppConfig['features']): boolean { | |
return this.config.features[feature]; | |
} | |
private getDefaultConfig(): AppConfig { | |
return { | |
apiUrl: 'http://localhost:3000', | |
version: '1.0.0', | |
features: { | |
enableAnalytics: false, | |
enableChat: false, | |
enableDarkMode: true | |
}, | |
environment: 'development' | |
}; | |
} | |
} | |
// Log Level Enum | |
export enum LogLevel { | |
DEBUG = 0, | |
INFO = 1, | |
WARN = 2, | |
ERROR = 3 | |
} | |
// Log Entry Interface | |
export interface LogEntry { | |
timestamp: Date; | |
level: LogLevel; | |
message: string; | |
context?: any; | |
source?: string; | |
} | |
// SINGLETON LOGGER SERVICE | |
// Maintains a single logging instance for the entire application | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class LoggerService { | |
private logs: LogEntry[] = []; | |
private logLevel: LogLevel = LogLevel.INFO; | |
private maxLogs = 1000; | |
private static instance: LoggerService; | |
constructor(private config: ConfigurationService) { | |
if (LoggerService.instance) { | |
return LoggerService.instance; | |
} | |
LoggerService.instance = this; | |
console.log('LoggerService singleton instance created'); | |
// Set log level based on environment | |
this.initializeLogLevel(); | |
} | |
private initializeLogLevel(): void { | |
try { | |
const appConfig = this.config.getConfig(); | |
switch (appConfig.environment) { | |
case 'development': | |
this.logLevel = LogLevel.DEBUG; | |
break; | |
case 'staging': | |
this.logLevel = LogLevel.INFO; | |
break; | |
case 'production': | |
this.logLevel = LogLevel.WARN; | |
break; | |
} | |
} catch { | |
// Config not loaded yet, use default | |
this.logLevel = LogLevel.INFO; | |
} | |
} | |
setLogLevel(level: LogLevel): void { | |
this.logLevel = level; | |
} | |
debug(message: string, context?: any, source?: string): void { | |
this.log(LogLevel.DEBUG, message, context, source); | |
} | |
info(message: string, context?: any, source?: string): void { | |
this.log(LogLevel.INFO, message, context, source); | |
} | |
warn(message: string, context?: any, source?: string): void { | |
this.log(LogLevel.WARN, message, context, source); | |
} | |
error(message: string, error?: any, source?: string): void { | |
this.log(LogLevel.ERROR, message, error, source); | |
} | |
private log(level: LogLevel, message: string, context?: any, source?: string): void { | |
if (level >= this.logLevel) { | |
const entry: LogEntry = { | |
timestamp: new Date(), | |
level, | |
message, | |
context, | |
source: source || this.getCallerSource() | |
}; | |
this.logs.push(entry); | |
// Console output with color coding | |
const prefix = `[${LogLevel[level]}] ${entry.timestamp.toISOString()}`; | |
const style = this.getConsoleStyle(level); | |
console.log(`%c${prefix}: ${message}`, style, context || ''); | |
// Maintain max log size | |
if (this.logs.length > this.maxLogs) { | |
this.logs.shift(); | |
} | |
// Send to remote logging in production | |
if (level >= LogLevel.ERROR && this.isProduction()) { | |
this.sendToRemoteLogging(entry); | |
} | |
} | |
} | |
private getConsoleStyle(level: LogLevel): string { | |
switch (level) { | |
case LogLevel.DEBUG: return 'color: gray'; | |
case LogLevel.INFO: return 'color: blue'; | |
case LogLevel.WARN: return 'color: orange'; | |
case LogLevel.ERROR: return 'color: red; font-weight: bold'; | |
default: return ''; | |
} | |
} | |
private getCallerSource(): string { | |
// Get caller information from stack trace | |
const stack = new Error().stack; | |
if (stack) { | |
const lines = stack.split('\n'); | |
return lines[3]?.trim() || 'Unknown'; | |
} | |
return 'Unknown'; | |
} | |
private isProduction(): boolean { | |
try { | |
return this.config.getValue('environment') === 'production'; | |
} catch { | |
return false; | |
} | |
} | |
private sendToRemoteLogging(entry: LogEntry): void { | |
// Implementation for remote logging service | |
// This would send logs to services like Sentry, LogRocket, etc. | |
console.log('Sending to remote logging:', entry); | |
} | |
getLogs(level?: LogLevel): LogEntry[] { | |
if (level !== undefined) { | |
return this.logs.filter(log => log.level === level); | |
} | |
return [...this.logs]; | |
} | |
clearLogs(): void { | |
this.logs = []; | |
} | |
exportLogs(): string { | |
return JSON.stringify(this.logs, null, 2); | |
} | |
} | |
// APP INITIALIZER - Ensures config is loaded before app starts | |
export function initializeApp(configService: ConfigurationService): () => Promise<any> { | |
return () => configService.loadConfiguration(); | |
} | |
// In app.module.ts | |
/* | |
@NgModule({ | |
// ... | |
providers: [ | |
{ | |
provide: APP_INITIALIZER, | |
useFactory: initializeApp, | |
deps: [ConfigurationService], | |
multi: true | |
} | |
] | |
}) | |
export class AppModule { } | |
*/ | |
// USAGE EXAMPLE IN COMPONENT | |
import { Component, OnInit } from '@angular/core'; | |
@Component({ | |
selector: 'app-dashboard', | |
template: ` | |
<div class="dashboard"> | |
<h1>Dashboard</h1> | |
<p>API URL: {{ apiUrl }}</p> | |
<p>Version: {{ version }}</p> | |
<p>Environment: {{ environment }}</p> | |
<div class="features"> | |
<h3>Features:</h3> | |
<p>Analytics: {{ features.enableAnalytics ? 'Enabled' : 'Disabled' }}</p> | |
<p>Chat: {{ features.enableChat ? 'Enabled' : 'Disabled' }}</p> | |
<p>Dark Mode: {{ features.enableDarkMode ? 'Enabled' : 'Disabled' }}</p> | |
</div> | |
<div class="logs"> | |
<h3>Recent Logs ({{ logCount }})</h3> | |
<button (click)="clearLogs()">Clear Logs</button> | |
<button (click)="exportLogs()">Export Logs</button> | |
<button (click)="testLogging()">Test Logging</button> | |
</div> | |
</div> | |
` | |
}) | |
export class DashboardComponent implements OnInit { | |
apiUrl: string; | |
version: string; | |
environment: string; | |
features: any; | |
logCount: number = 0; | |
constructor( | |
private config: ConfigurationService, | |
private logger: LoggerService | |
) { | |
// Both services are singletons - same instance everywhere | |
this.logger.info('DashboardComponent initialized', null, 'DashboardComponent'); | |
} | |
ngOnInit(): void { | |
// Get configuration values | |
const config = this.config.getConfig(); | |
this.apiUrl = config.apiUrl; | |
this.version = config.version; | |
this.environment = config.environment; | |
this.features = config.features; | |
// Log component lifecycle | |
this.logger.debug('Dashboard loaded with config', config, 'DashboardComponent'); | |
// Update log count | |
this.updateLogCount(); | |
} | |
testLogging(): void { | |
this.logger.debug('Debug message', { test: true }); | |
this.logger.info('Info message', { timestamp: Date.now() }); | |
this.logger.warn('Warning message', { alert: 'Check this' }); | |
this.logger.error('Error message', new Error('Test error')); | |
this.updateLogCount(); | |
} | |
clearLogs(): void { | |
this.logger.clearLogs(); | |
this.updateLogCount(); | |
} | |
exportLogs(): void { | |
const logs = this.logger.exportLogs(); | |
const blob = new Blob([logs], { type: 'application/json' }); | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = `logs-${Date.now()}.json`; | |
a.click(); | |
} | |
private updateLogCount(): void { | |
this.logCount = this.logger.getLogs().length; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment