/** * Value Object: TimeWindow * Encapsulates the concept of a time window used for presence detection. * The default window is 180 seconds (3 minutes), meaning a user is considered * "online" if their last heartbeat was within this window. */ export class TimeWindow { /** Default presence detection window: 180 seconds (3 minutes) */ static readonly DEFAULT_SECONDS = 180; /** Minimum allowed window: 30 seconds */ static readonly MIN_SECONDS = 30; /** Maximum allowed window: 600 seconds (10 minutes) */ static readonly MAX_SECONDS = 600; private constructor(private readonly seconds: number) {} static create(seconds: number = TimeWindow.DEFAULT_SECONDS): TimeWindow { if (seconds < TimeWindow.MIN_SECONDS) { throw new Error(`Time window must be at least ${TimeWindow.MIN_SECONDS} seconds, got ${seconds}`); } if (seconds > TimeWindow.MAX_SECONDS) { throw new Error(`Time window must be at most ${TimeWindow.MAX_SECONDS} seconds, got ${seconds}`); } if (!Number.isInteger(seconds)) { throw new Error('Time window must be an integer number of seconds'); } return new TimeWindow(seconds); } /** Create the default 180-second window */ static default(): TimeWindow { return new TimeWindow(TimeWindow.DEFAULT_SECONDS); } /** Get the window duration in seconds */ toSeconds(): number { return this.seconds; } /** Get the window duration in milliseconds */ toMilliseconds(): number { return this.seconds * 1000; } /** Calculate the threshold timestamp (now - window) as Unix epoch seconds */ getThresholdEpoch(): number { return Math.floor(Date.now() / 1000) - this.seconds; } /** Calculate the threshold timestamp as a Date */ getThresholdDate(): Date { return new Date(Date.now() - this.toMilliseconds()); } equals(other: TimeWindow): boolean { return this.seconds === other.seconds; } toString(): string { return `${this.seconds}s`; } }