import { Injectable, signal, computed } from '@angular/core'; /** Default timeout (ms) for the NTP-style HTTP sync request. */ const DEFAULT_SYNC_TIMEOUT_MS = 5000; /** * Maintains a clock-offset between the local system time and the * remote signaling server. * * The offset is estimated using a simple NTP-style round-trip * measurement and is stored as a reactive Angular signal so that * any dependent computed value auto-updates when a new sync occurs. */ @Injectable({ providedIn: 'root' }) export class TimeSyncService { /** * Internal offset signal: * `serverTime = Date.now() + offset`. */ private readonly _offset = signal(0); /** Epoch timestamp of the most recent successful sync. */ private lastSyncTimestamp = 0; /** Reactive read-only offset (milliseconds). */ readonly offset = computed(() => this._offset()); /** * Return a server-adjusted "now" timestamp. * * @returns Epoch milliseconds aligned to the server clock. */ now(): number { return Date.now() + this._offset(); } /** * Set the offset from a known server timestamp. * * @param serverTime - Epoch timestamp reported by the server. * @param receiveTimestamp - Local epoch timestamp when the server time was * observed. Defaults to `Date.now()` if omitted. */ setFromServerTime(serverTime: number, receiveTimestamp?: number): void { const observedAt = receiveTimestamp ?? Date.now(); this._offset.set(serverTime - observedAt); this.lastSyncTimestamp = Date.now(); } /** * Perform an HTTP-based clock synchronisation using a simple * NTP-style round-trip. * * 1. Record `clientSendTime` (`t0`). * 2. Fetch `GET {baseApiUrl}/time`. * 3. Record `clientReceiveTime` (`t2`). * 4. Estimate one-way latency as `(t2 − t0) / 2`. * 5. Compute offset: `serverNow − midpoint(t0, t2)`. * * Any network or parsing error is silently ignored so that the * last known offset (or zero) is retained. * * @param baseApiUrl - API base URL (e.g. `http://host:3001/api`). * @param timeoutMs - Maximum time to wait for the response. */ async syncWithEndpoint( baseApiUrl: string, timeoutMs: number = DEFAULT_SYNC_TIMEOUT_MS, ): Promise { try { const controller = new AbortController(); const clientSendTime = Date.now(); const timer = setTimeout(() => controller.abort(), timeoutMs); const response = await fetch(`${baseApiUrl}/time`, { signal: controller.signal, }); const clientReceiveTime = Date.now(); clearTimeout(timer); if (!response.ok) return; const data = await response.json(); const serverNow = Number(data?.now) || Date.now(); const midpoint = (clientSendTime + clientReceiveTime) / 2; this._offset.set(serverNow - midpoint); this.lastSyncTimestamp = Date.now(); } catch { // Sync failure is non-fatal; retain the previous offset. } } }