import { Injectable, Inject, ApplicationRef } from '@angular/core'; import { Subject } from 'rxjs'; declare const p3xr: any; declare const io: any; /** * Angular Socket.IO service — standalone, no AngularJS dependency. * All callbacks run inside Angular's zone for automatic change detection. */ @Injectable({ providedIn: 'root' }) export class SocketService { private ioClient: any; private reconnect = false; private licenseRefreshInterval: any; private connectErrorWas = false; readonly connections$ = new Subject(); readonly redisDisconnected$ = new Subject(); readonly redisStatus$ = new Subject(); readonly configuration$ = new Subject(); readonly licenseUpdate$ = new Subject(); readonly socketError$ = new Subject(); readonly stateChanged$ = new Subject(); constructor(@Inject(ApplicationRef) private appRef: ApplicationRef) { this.initConnection(); } tick(): void { setTimeout(() => { this.appRef.tick(); }); } private initConnection(): void { const ioOptions: any = { rejectUnauthorized: false, path: '/socket.io', secure: true, }; if ((globalThis as any).p3xrDevMode === true) { ioOptions.transports = ['websocket']; } this.ioClient = io.connect(p3xr.api.host, ioOptions); this.ioClient.on('connect', async () => { if (this.reconnect) { console.log('p3xr-socket RE-connected', this.ioClient.id); } else { console.log('p3xr-socket connected', this.ioClient.id); } this.reconnect = true; if (this.licenseRefreshInterval) { clearInterval(this.licenseRefreshInterval); } this.licenseRefreshInterval = setInterval(() => { this.refreshLicenseStatus(); }, 1000 * 60 * 60); await this.refreshLicenseStatus(); }); this.ioClient.on('disconnect', () => { if (this.licenseRefreshInterval) { clearInterval(this.licenseRefreshInterval); this.licenseRefreshInterval = undefined; } location.reload(); }); this.ioClient.on('info-interval', (data: any) => { this.applyLicenseData(data); this.tick(); }); this.ioClient.on('error', (error: any) => { this.handleSocketError(error); }); this.ioClient.on('connect_error', (error: any) => { this.handleSocketError(error); }); this.ioClient.on('connections', (data: any) => { if (data.status === 'error') { p3xr.connectionsReset(); this.tick(); return; } p3xr.state.connections = data.connections; this.connections$.next(data); this.tick(); }); this.ioClient.on('redis-disconnected', (data: any) => { if (p3xr.state.connection !== undefined && p3xr.state.connection.id === data.connectionId) { p3xr.state.monitor = false; p3xr.state.connection = undefined; if (data.status === 'error') { const msg = p3xr.strings?.status?.redisDisconnected?.(data) ?? 'Redis disconnected'; this.showToast(msg); } else if (data.status === 'code') { const codes = p3xr.strings?.code ?? {}; const msg = codes[data.code] ?? `unknown redis disconnect code: ${data.code}`; this.showToast(msg); } this.redisDisconnected$.next(data); this.tick(); this.request({ action: 'trigger-redis-disconnect', enableResponse: false }).catch(() => {}); } }); this.ioClient.on('redis-status', (data: any) => { p3xr.state.redisConnections = data.redisConnections; this.redisStatus$.next(data); this.tick(); }); let receivedVersion = false; this.ioClient.on('configuration', (data: any) => { p3xr.state.cfg = data; if (data.snapshot === true) { p3xr.state.version = 'SNAPSHOT'; } else { p3xr.state.version = 'v' + data.version; if (!receivedVersion) { receivedVersion = true; try { (window as any).gtag?.('config', p3xr.settings.googleAnalytics, { page_path: '/version/' + p3xr.state.version }); } catch { /* noop */ } } } this.configuration$.next(data); this.tick(); }); } // --- License --- private applyLicenseData(data: any = {}): void { const nextLicense: any = Object.assign({ licenseEditable: true, editableActive: true, disabled: false, hasLicenseKey: false, licenseKeyMasked: '', tier: 'free', valid: false, reason: 'LICENSE_MISSING', licenseStatus: 'inactive', maxDevices: null, activeDevices: null, deviceLease: null, daysLeft: null, features: [], }, (data.license && typeof data.license === 'object') ? data.license : {}); if (typeof data.hasLicenseKey === 'boolean') { nextLicense.hasLicenseKey = data.hasLicenseKey; } else { nextLicense.hasLicenseKey = nextLicense.hasLicenseKey === true; } if (typeof data.licenseEditable === 'boolean') { nextLicense.licenseEditable = data.licenseEditable; } else if (typeof data.editableActive === 'boolean') { nextLicense.licenseEditable = data.editableActive; } else if (typeof data.disabled === 'boolean') { nextLicense.licenseEditable = !data.disabled; } else if (typeof nextLicense.licenseEditable !== 'boolean') { nextLicense.licenseEditable = true; } nextLicense.editableActive = nextLicense.licenseEditable; nextLicense.disabled = !nextLicense.licenseEditable; if (typeof data.licenseKeyMasked === 'string') { nextLicense.licenseKeyMasked = data.licenseKeyMasked; } else if (typeof nextLicense.licenseKeyMasked !== 'string') { nextLicense.licenseKeyMasked = ''; } if (typeof data.tier === 'string' && data.tier.length > 0) { nextLicense.tier = data.tier; } if (!Array.isArray(nextLicense.features)) { nextLicense.features = []; } if (!nextLicense.deviceLease || typeof nextLicense.deviceLease !== 'object') { nextLicense.deviceLease = null; } if (typeof nextLicense.maxDevices !== 'number') { nextLicense.maxDevices = nextLicense.deviceLease?.maxDevices ?? null; } if (typeof nextLicense.activeDevices !== 'number') { nextLicense.activeDevices = nextLicense.deviceLease?.activeDevices ?? null; } const wasDonated = p3xr.state.donated === true; // All features are free — always enterprise const isDonated = true; const activeForFeatures = true; const hasProOrEnterpriseJsonBinary = true; p3xr.state.license = nextLicense; p3xr.state.donated = isDonated; // All features are free — always enterprise p3xr.state.hasProOrEnterpriseJsonBinary = true; // p3xr.state.hasProOrEnterpriseJsonBinary = hasProOrEnterpriseJsonBinary; if (p3xr.state.cfg && typeof data.readonlyConnections === 'boolean') { p3xr.state.cfg.readonlyConnections = data.readonlyConnections; } if (wasDonated !== isDonated) { try { if (!p3xr.isBot?.()) { (window as any).gtag?.('config', p3xr.settings.googleAnalytics, { page_path: isDonated ? '/donated' : '/free' }); } } catch { /* noop */ } } this.licenseUpdate$.next(nextLicense); } private async refreshLicenseStatus(): Promise { try { const data = await this.request({ action: 'license-status' }); this.applyLicenseData(data); } catch (e: any) { console.warn('license-status refresh failed', e.message); } } private handleSocketError(error: any): void { if (!this.connectErrorWas) { this.connectErrorWas = true; this.socketError$.next(error); } } private showToast(message: string): void { try { const snackBar = (globalThis as any).__p3xr_snackbar; if (snackBar) { const ref = snackBar.open(message, 'x', { duration: 5000, horizontalPosition: 'right', verticalPosition: 'bottom', }); ref.onAction().subscribe(() => ref.dismiss()); } } catch { /* noop */ } } // --- Request API --- request(options: { action: string; payload?: any; enableResponse?: boolean; }): Promise { if (!this.ioClient) { return Promise.reject(new Error('Socket.IO client unavailable')); } if (!options.payload) { options.payload = {}; } options.payload.maxKeys = parseInt(p3xr.settings?.maxKeys ?? '10000'); const enableResponse = options.enableResponse !== false; if (!enableResponse) { this.ioClient.emit('p3xr-request', options); return Promise.resolve(); } return new Promise((resolve, reject) => { const requestId = p3xr.nextId(); (options as any).requestId = requestId; const responseEvent = `p3xr-response-${requestId}`; let timeout: any; const response = (data: any) => { clearTimeout(timeout); this.ioClient.off(responseEvent); if (data?.status === 'ok') { if (data.license || data.licenseKey || data.donated || options.action === 'license-status') { this.applyLicenseData(data); } resolve(data); } else { let errMsg = 'Unknown error'; try { const err = data?.error; if (typeof err === 'string') { errMsg = err; } else if (err?.message) { errMsg = err.message; } else if (err !== undefined && err !== null) { errMsg = String(err); } } catch { /* noop */ } reject(new Error(errMsg)); } // Tick after await continuations settle (avoids NG0100 in dev mode) this.tick(); }; timeout = setTimeout(() => { this.ioClient.off(responseEvent, response); const msg = p3xr.strings?.label?.socketIoTimeout?.({ timeout: p3xr.settings.socket.timeout }) ?? `Socket.IO request timeout (${p3xr.settings.socket.timeout}ms)`; reject(new Error(msg)); this.tick(); }, p3xr.settings.socket.timeout); this.ioClient.on(responseEvent, response); this.ioClient.emit('p3xr-request', options); }); } getClient(): any { return this.ioClient; } }