Files
Toju/src/app/core/services/time-sync.service.ts
2026-03-02 03:30:22 +01:00

95 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<number>(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<void> {
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.
}
}
}