import { expect, test, vi } from "vitest" import { ExpirationManager } from "#Source/timer/index.ts" test("ExpirationManager getNow delegates to the configured clock", () => { let now = 100 const expirationManager = new ExpirationManager<"alpha">({ enableRemainingManager: false, clock: { now: (): number => now, }, }) expect(expirationManager.getNow()).toBe(100) now = 250 expect(expirationManager.getNow()).toBe(250) expirationManager.terminate() }) test("ExpirationManager hasExpiration reports retained active, expired, and removed entries", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma" | "missing">({ enableRemainingManager: false, }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expirationManager.upsertExpiration("beta", Date.now() - 1) expirationManager.upsertExpiration("gamma", Date.now() + 1_000) expirationManager.removeExpiration("gamma") expect(expirationManager.hasExpiration("alpha")).toBe(true) expect(expirationManager.hasExpiration("beta")).toBe(true) expect(expirationManager.hasExpiration("gamma")).toBe(true) expect(expirationManager.hasExpiration("missing")).toBe(false) expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager getExpirationState returns a cloned state snapshot", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "missing">({ enableRemainingManager: false, }) const endAt = Date.now() + 1_000 expirationManager.upsertExpiration("alpha", endAt) const expirationState = expirationManager.getExpirationState("alpha") expect(expirationState).toEqual({ name: "alpha", endAt, state: "active", }) if (expirationState !== undefined) { expirationState.state = "removed" expirationState.endAt = 0 } expect(expirationManager.getExpirationState("alpha")).toEqual({ name: "alpha", endAt, state: "active", }) expect(expirationManager.getExpirationState("missing")).toBeUndefined() expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager getExpirationSnapshot only includes active entries", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({ enableRemainingManager: false, }) const alphaEndAt = Date.now() + 1_000 expirationManager.upsertExpiration("alpha", alphaEndAt) expirationManager.upsertExpiration("beta", Date.now() - 1) expirationManager.upsertExpiration("gamma", Date.now() + 1_000) expirationManager.removeExpiration("gamma") expect(expirationManager.getExpirationSnapshot()).toEqual({ alpha: alphaEndAt, }) expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager upsertExpiration stores state changes and rejects invalid timestamps", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha">({ enableRemainingManager: false, }) const expirationStates: Array> = [] const activeEndAt = Date.now() + 2_000 const expiredEndAt = Date.now() - 1 expirationManager.event.subscribe("expirationState", (expirationState): void => { expirationStates.push(structuredClone(expirationState)) }) expirationManager.upsertExpiration("alpha", activeEndAt) expirationManager.upsertExpiration("alpha", activeEndAt) expirationManager.upsertExpiration("alpha", expiredEndAt) expect(expirationStates).toEqual([ { alpha: { name: "alpha", endAt: activeEndAt, state: "active", }, }, { alpha: { name: "alpha", endAt: expiredEndAt, state: "expired", }, }, ]) expect(() => expirationManager.upsertExpiration("alpha", Number.NaN)).toThrow(RangeError) expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager updateExpirationBatch applies partial updates and active-only removals", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({ enableRemainingManager: false, }) const alphaEndAt = Date.now() + 1_000 const betaEndAt = Date.now() - 1 const gammaEndAt = Date.now() + 2_000 expirationManager.updateExpirationBatch({ alpha: alphaEndAt, beta: betaEndAt, }) expirationManager.updateExpirationBatch({ alpha: undefined, beta: undefined, gamma: gammaEndAt, }) expect(expirationManager.getExpirationState("alpha")).toEqual({ name: "alpha", endAt: alphaEndAt, state: "removed", }) expect(expirationManager.getExpirationState("beta")).toEqual({ name: "beta", endAt: betaEndAt, state: "expired", }) expect(expirationManager.getExpirationState("gamma")).toEqual({ name: "gamma", endAt: gammaEndAt, state: "active", }) expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager removeExpiration only marks active entries as removed", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({ enableRemainingManager: false, }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expirationManager.upsertExpiration("beta", Date.now() - 1) expirationManager.upsertExpiration("gamma", Date.now() + 1_000) expirationManager.removeExpiration("gamma") expirationManager.removeExpiration("alpha") expirationManager.removeExpiration("beta") expect(expirationManager.getExpirationState("alpha")?.state).toBe("removed") expect(expirationManager.getExpirationState("beta")?.state).toBe("expired") expect(expirationManager.getExpirationState("gamma")?.state).toBe("removed") expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager clearExpiredExpirations removes expired entries and returns the count", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({ enableRemainingManager: false, }) expirationManager.upsertExpiration("alpha", Date.now() - 1) expirationManager.upsertExpiration("beta", Date.now() - 2) expirationManager.upsertExpiration("gamma", Date.now() + 1_000) expect(expirationManager.clearExpiredExpirations()).toBe(2) expect(expirationManager.hasExpiration("alpha")).toBe(false) expect(expirationManager.hasExpiration("beta")).toBe(false) expect(expirationManager.hasExpiration("gamma")).toBe(true) expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager clearRemovedExpirations removes removed entries and returns the count", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta" | "gamma">({ enableRemainingManager: false, }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expirationManager.upsertExpiration("beta", Date.now() + 2_000) expirationManager.upsertExpiration("gamma", Date.now() - 1) expirationManager.removeExpiration("alpha") expirationManager.removeExpiration("beta") expect(expirationManager.clearRemovedExpirations()).toBe(2) expect(expirationManager.hasExpiration("alpha")).toBe(false) expect(expirationManager.hasExpiration("beta")).toBe(false) expect(expirationManager.getExpirationState("gamma")?.state).toBe("expired") expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager pause blocks queued emissions and scheduler ticks", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha">({ enableRemainingManager: false, }) const expirationStates: Array> = [] expirationManager.event.subscribe("expirationState", (expirationState): void => { expirationStates.push(structuredClone(expirationState)) }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expirationManager.pause() expirationManager.upsertExpiration("alpha", Date.now() + 3_000) vi.advanceTimersByTime(4_000) expect(expirationStates).toHaveLength(1) expect(expirationManager.getExpirationState("alpha")?.state).toBe("active") expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager resume flushes the latest paused snapshot and expires overdue entries", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta">({ enableRemainingManager: false, }) const expirationStates: Array> = [] const alphaInitialEndAt = Date.now() + 2_000 const alphaUpdatedEndAt = Date.now() + 5_000 const betaEndAt = Date.now() + 1_000 expirationManager.event.subscribe("expirationState", (expirationState): void => { expirationStates.push(structuredClone(expirationState)) }) expirationManager.upsertExpiration("alpha", alphaInitialEndAt) expirationManager.pause() expirationManager.updateExpirationBatch({ alpha: alphaUpdatedEndAt, beta: betaEndAt, }) expirationManager.updateExpirationBatch({ beta: undefined, }) vi.advanceTimersByTime(6_000) expirationManager.resume() expect(expirationStates).toEqual([ { alpha: { name: "alpha", endAt: alphaInitialEndAt, state: "active", }, }, { alpha: { name: "alpha", endAt: alphaUpdatedEndAt, state: "expired", }, beta: { name: "beta", endAt: betaEndAt, state: "removed", }, }, ]) expirationManager.terminate() } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager terminate stops future scheduling and ignores later writes", () => { vi.useFakeTimers() vi.setSystemTime(new Date(2_026, 0, 1, 0, 0, 0)) try { const expirationManager = new ExpirationManager<"alpha" | "beta">({ enableRemainingManager: false, }) const expirationStates: Array> = [] expirationManager.event.subscribe("expirationState", (expirationState): void => { expirationStates.push(structuredClone(expirationState)) }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expirationManager.terminate() vi.advanceTimersByTime(2_000) expirationManager.upsertExpiration("beta", Date.now() + 1_000) expect(expirationStates).toHaveLength(1) expect(expirationManager.hasExpiration("beta")).toBe(false) } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("ExpirationManager pause during expirationState does not interrupt the current emission", () => { const expirationManager = new ExpirationManager<"alpha">({ enableRemainingManager: false, }) const eventOrder: string[] = [] expirationManager.event.subscribe("expirationState", () => { eventOrder.push("state") expirationManager.pause() }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expect(eventOrder).toEqual(["state"]) expirationManager.terminate() }) test("ExpirationManager resume does not replay an already emitted state snapshot", () => { const expirationManager = new ExpirationManager<"alpha">({ enableRemainingManager: false, }) const eventOrder: string[] = [] expirationManager.event.subscribe("expirationState", () => { eventOrder.push("state") if (eventOrder.length === 1) { expirationManager.pause() } }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expirationManager.resume() expect(eventOrder).toEqual(["state"]) expirationManager.terminate() }) test("ExpirationManager terminate during expirationState does not throw", () => { const expirationManager = new ExpirationManager<"alpha">({ enableRemainingManager: false, }) const eventOrder: string[] = [] expirationManager.event.subscribe("expirationState", () => { eventOrder.push("state") expirationManager.terminate() }) expirationManager.upsertExpiration("alpha", Date.now() + 1_000) expect(eventOrder).toEqual(["state"]) })