import type { BuildEvents } from "#Source/event/index.ts" import { EventManager } from "#Source/event/index.ts" import { MinHeap } from "./min-heap.ts" import { RemainingManager } from "./remaining-manager.ts" const MAX_SCHEDULER_DELAY = 2_147_483_647 /** * @description 提供当前墙钟时间(wall-clock time)的毫秒值。 */ export interface ExpirationManagerClock { /** * @description 返回当前时间戳,单位为毫秒。 */ now(): number } /** * @description 表示一个以毫秒为单位的过期时点。 */ export type ExpirationEndAt = number /** * @description 描述一组按过期名称索引的过期时点字典。 */ export type ExpirationDict = { [K in ExpirationName]?: ExpirationEndAt | undefined } /** * @description 描述 `ExpirationManager` 对外发出的事件表。 * * 当管理器处于暂停状态时,事件不会立即发出,而是延后并折叠为恢复时的最新快照。 */ export type ExpirationManagerEvents = BuildEvents<{ /** * @description 在保留的过期状态快照发生可观察变化时发出最新结果。 */ expirationState: (expirationState: ExpirationState) => void }> /** * @description Hold one coalesced emission payload derived from current manager state. */ interface ExpirationEmission { expirationState: ExpirationState } /** * @description 描述一条被管理器保留的过期状态记录。 */ export interface ExpirationStateItem { /** * @description 该过期项的名称。 */ name: ExpirationName /** * @description 该过期项对应的绝对到期时点,单位为毫秒。 */ endAt: ExpirationEndAt /** * @description 该过期项当前的状态。 */ state: "active" | "expired" | "removed" } /** * @description 描述一组按过期名称索引的保留状态字典。 */ export type ExpirationState = { [K in ExpirationName]?: ExpirationStateItem | undefined } /** * @description 描述创建 `ExpirationManager` 时可用的配置项。 */ export interface ExpirationManagerOptions { /** * @description 用于初始化管理器的过期项集合。 */ initialExpirations?: ExpirationDict | undefined /** * @description 控制是否同时启用派生剩余时间管理器的周期性检查。 */ enableRemainingManager?: boolean | undefined /** * @description 提供自定义时间来源;未提供时默认使用 `Date.now()`。 */ clock?: ExpirationManagerClock | undefined } /** * @description 管理具名过期项的状态、事件与定时调度。 * * `ExpirationManager` owns the source-of-truth expiration state dictionary and uses a `MinHeap` * to keep the nearest `endAt` value at the top. That makes rescheduling efficient because * the manager only needs to inspect the heap root to determine the next wake-up time, * instead of scanning all expiration entries on every update. * * When the next expiration is farther away than the platform timer limit, scheduling is * performed in bounded segments until the target timestamp becomes reachable. * * Pausing does not freeze time or reject writes. State updates may continue while paused, * but expiration events and derived remaining events are deferred. Multiple paused-time * state changes are coalesced into the latest snapshot and flushed after `resume()`. * * Remaining time is treated as derived state rather than primary state. The companion * `RemainingManager` reads snapshots from `ExpirationManager`, computes second-based * remaining values on demand, and can periodically emit those derived values without * duplicating expiration ownership. */ export class ExpirationManager { private expirationState: ExpirationState private clock: ExpirationManagerClock private heap: MinHeap private schedulerTimer: ReturnType | null private paused: boolean private terminated: boolean private expirationEmissionQueue: Array> private flushingExpirationEmissionQueue: boolean readonly event: EventManager> readonly remainingManager: RemainingManager constructor(options: ExpirationManagerOptions) { const { initialExpirations, enableRemainingManager, clock } = options const initialExpirationSource: ExpirationDict = initialExpirations ?? {} this.expirationState = {} this.clock = clock ?? { now: (): number => Date.now(), } this.heap = new MinHeap() this.schedulerTimer = null this.paused = false this.terminated = false this.expirationEmissionQueue = [] this.flushingExpirationEmissionQueue = false this.event = new EventManager() this.remainingManager = new RemainingManager({ expirationManager: this, enabled: enableRemainingManager, }) this.updateExpirationBatch(initialExpirationSource) } /** * @description 返回当前管理器使用的墙钟时间。 */ getNow(): number { return this.clock.now() } /** * @description 判断某个具名过期项当前是否仍有保留状态。 * * 只要该名称当前处于 `active`、`expired` 或 `removed` 任一保留状态,就会返回 `true`。 */ hasExpiration(name: ExpirationName): boolean { return this.expirationState[name] !== undefined } /** * @description 返回某个具名过期项的状态快照。 */ getExpirationState(name: ExpirationName): ExpirationStateItem | undefined { return structuredClone(this.expirationState[name]) } /** * @description 返回当前所有活跃过期项的绝对到期时点快照。 */ getExpirationSnapshot(): ExpirationDict { const expirationDict: ExpirationDict = {} for (const name of Object.keys(this.expirationState)) { const expirationName = name as ExpirationName const expirationState = this.expirationState[expirationName] if (expirationState === undefined) { continue } if (expirationState.state !== "active") { continue } expirationDict[expirationName] = expirationState.endAt } return expirationDict } /** * @description Check whether event emission is currently blocked. */ private isEmissionBlocked(): boolean { return this.terminated === true || this.paused === true } /** * @description Flush queued state emissions in order until emission becomes blocked again. */ private flushExpirationEmissionQueue(): void { if (this.isEmissionBlocked() === true) { return } if (this.flushingExpirationEmissionQueue === true) { return } this.flushingExpirationEmissionQueue = true try { while (true) { if (this.isEmissionBlocked() === true) { return } const emission = this.expirationEmissionQueue.shift() if (emission === undefined) { return } this.event.emit("expirationState", emission.expirationState) } } finally { this.flushingExpirationEmissionQueue = false } } /** * @description Build the latest event payload from current manager state. */ private createExpirationEmission(): ExpirationEmission { return { expirationState: structuredClone(this.expirationState), } } /** * @description Replace any queued paused-time emission with the latest coalesced snapshot. */ private replaceQueuedExpirationEmission(): void { const emission = this.createExpirationEmission() this.expirationEmissionQueue = [emission] } /** * @description Queue an emission for the current state. * * While paused, the queue is collapsed to a single latest snapshot so `resume()` * only flushes the final observable state. */ private enqueueExpirationEmission(): void { if (this.paused === true) { this.replaceQueuedExpirationEmission() return } const emission = this.createExpirationEmission() this.expirationEmissionQueue.push(emission) this.flushExpirationEmissionQueue() } /** * @description Mark overdue active entries as expired when their stored timestamps are no longer in the * future. */ private updateExpired(now: number): boolean { let changed = false while (true) { const top = this.heap.peek() if (top === undefined || top.endAt > now) { return changed } this.heap.pop() const expiration = this.expirationState[top.name] if (expiration === undefined) { continue } if (expiration.state === "active" && expiration.endAt === top.endAt) { this.expirationState[top.name] = { ...expiration, state: "expired" } changed = true } } } private tick(): void { if (this.terminated === true || this.paused === true) { return } const changed = this.updateExpired(this.getNow()) if (changed === true) { this.enqueueExpirationEmission() } this.schedule() } private schedule(): void { if (this.terminated === true || this.paused === true) { return } if (this.schedulerTimer !== null) { clearTimeout(this.schedulerTimer) this.schedulerTimer = null } const next = this.heap.peek() if (next === undefined) { return } const delay = Math.min(MAX_SCHEDULER_DELAY, Math.max(0, next.endAt - this.getNow())) this.schedulerTimer = setTimeout(() => this.tick(), delay) } private validateExpirationEndAt(endAt: number): void { if (Number.isFinite(endAt) !== true || endAt < 0) { throw new RangeError( "Expiration endAt must be a finite timestamp greater than or equal to 0.", ) } } private internalUpsertExpiration(name: ExpirationName, endAt: number, now: number): boolean { this.validateExpirationEndAt(endAt) const oldState = this.expirationState[name] const newState = { name, endAt, state: endAt <= now ? "expired" : "active" } as const if ( oldState !== undefined && oldState.name === newState.name && oldState.endAt === newState.endAt && oldState.state === newState.state ) { return false } this.expirationState[name] = newState this.heap.remove(name) if (newState.state === "active") { this.heap.push({ name, endAt }) } return true } /** * @description 使用毫秒级绝对时点设置或替换一个具名过期项。 */ upsertExpiration(name: ExpirationName, endAt: number): void { if (this.terminated === true) { return } const changed = this.internalUpsertExpiration(name, endAt, this.getNow()) if (changed === true) { this.enqueueExpirationEmission() this.schedule() } } /** * @description 使用毫秒级绝对时点执行一次局部批量更新。 * * 对于 `dict` 中每个被显式提供的键,行为如下: * - 数值会把对应过期项设置或替换为给定的绝对 `endAt` 时点。 * - 显式的 `undefined` 只会在当前状态为 `active` 时把该项标记为 `removed`。 * - 当前已处于 `expired` 或 `removed` 的项,遇到显式 `undefined` 时保持不变。 * - `dict` 中未出现的键不会被处理,现有状态保持不变。 * * 当至少有一项真实发生变化时,会在所有显式更新完成后统一发出一次状态变化通知。 */ updateExpirationBatch(dict: ExpirationDict): void { if (this.terminated === true) { return } for (const name of Object.keys(dict)) { const expirationName = name as ExpirationName const endAt = dict[expirationName] if (endAt === undefined) { continue } this.validateExpirationEndAt(endAt) } let changed = false const now = this.getNow() for (const name of Object.keys(dict)) { const expirationName = name as ExpirationName const endAt = dict[expirationName] if (endAt === undefined) { const expiration = this.expirationState[expirationName] if (expiration === undefined) { continue } if (expiration.state === "active") { this.expirationState[expirationName] = { ...expiration, state: "removed" } this.heap.remove(expirationName) changed = true } continue } changed = this.internalUpsertExpiration(expirationName, endAt, now) || changed } if (changed === true) { this.enqueueExpirationEmission() this.schedule() } } /** * @description 将一个当前处于活跃状态的过期项标记为 `removed`。 * * 已经处于 `expired` 或 `removed` 的项不会被再次修改。 */ removeExpiration(name: ExpirationName): void { if (this.terminated === true) { return } const expiration = this.expirationState[name] if (expiration === undefined) { return } if (expiration.state !== "active") { return } this.expirationState[name] = { ...expiration, state: "removed" } this.heap.remove(name) this.enqueueExpirationEmission() this.schedule() } /** * @description 清除所有当前状态为 `expired` 的保留项,并返回清除数量。 */ clearExpiredExpirations(): number { if (this.terminated === true) { return 0 } let clearedCount = 0 for (const name of Object.keys(this.expirationState)) { const expirationName = name as ExpirationName const expirationState = this.expirationState[expirationName] if (expirationState === undefined) { continue } if (expirationState.state !== "expired") { continue } this.heap.remove(expirationName) delete this.expirationState[expirationName] clearedCount = clearedCount + 1 } if (clearedCount > 0) { this.enqueueExpirationEmission() } return clearedCount } /** * @description 清除所有当前状态为 `removed` 的保留项,并返回清除数量。 */ clearRemovedExpirations(): number { if (this.terminated === true) { return 0 } let clearedCount = 0 for (const name of Object.keys(this.expirationState)) { const expirationName = name as ExpirationName const expirationState = this.expirationState[expirationName] if (expirationState === undefined) { continue } if (expirationState.state !== "removed") { continue } this.heap.remove(expirationName) delete this.expirationState[expirationName] clearedCount = clearedCount + 1 } if (clearedCount > 0) { this.enqueueExpirationEmission() } return clearedCount } /** * @description 暂停过期调度与事件发出。 * * 这不会冻结绝对到期时点,也不会阻止状态写入。暂停期间时间仍会继续流逝, * 只是过期推进与事件通知会延后到后续恢复时再统一处理。 */ pause(): void { if (this.terminated === true) { return } this.paused = true if (this.schedulerTimer !== null) { clearTimeout(this.schedulerTimer) this.schedulerTimer = null } this.remainingManager.pause() } /** * @description 恢复过期调度,并刷新暂停期间积累的最新状态通知。 * * 在暂停期间已经越过其绝对 `endAt` 的项,可能会在恢复后立即转为 `expired`。 * 若暂停期间发生过多次状态变更,对外只会发出折叠后的最新快照。 */ resume(): void { if (this.terminated === true) { return } if (this.paused !== true) { return } this.paused = false const changed = this.updateExpired(this.getNow()) if (changed === true) { this.replaceQueuedExpirationEmission() } this.schedule() this.remainingManager.resume() this.flushExpirationEmissionQueue() } /** * @description 终止当前管理器及其派生剩余时间管理器,并释放它们拥有的定时资源。 */ terminate(): void { if (this.terminated === true) { return } this.terminated = true this.paused = true if (this.schedulerTimer !== null) { clearTimeout(this.schedulerTimer) this.schedulerTimer = null } this.expirationEmissionQueue = [] this.remainingManager.terminate() } }