diff --git a/packages/core/src/util/timer.baktest.ts b/packages/core/src/util/timer.baktest.ts deleted file mode 100644 index 607c6b745..000000000 --- a/packages/core/src/util/timer.baktest.ts +++ /dev/null @@ -1,273 +0,0 @@ -import dayjs, { Dayjs } from "dayjs"; -import { Timer } from "@core/util/timer"; -import { waitUntilEvent } from "@core/util/wait-until-event.util"; - -describe("Timer", () => { - const _id = "test-timer"; - - describe("initialization", () => { - it("creates a timer with ObjectId, start, and end dates", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - expect(timer._id).toEqual(_id); - expect(timer.startDate).toEqual(startDate); - expect(timer.endDate).toEqual(endDate); - - timer.on("end", done); - }); - - it("throws error when end date is in the past", () => { - const startDate = dayjs().subtract(1, "seconds").toDate(); - const endDate = dayjs(startDate).add(1, "second").toDate(); - - expect(() => new Timer({ _id, startDate, endDate })).toThrow( - "End date must be in the future", - ); - }); - - it("throws error when end date is before or equal to start date", () => { - const startDate = dayjs().add(5, "seconds").toDate(); - const endDate = dayjs().add(1, "second").toDate(); - - expect(() => new Timer({ _id, startDate, endDate })).toThrow( - "Start date must be before End date", - ); - }); - - it("accepts custom interval in milliseconds", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - const interval = 100; // 100ms - const ticks: Dayjs[] = []; - - const timer = new Timer({ _id, startDate, endDate, interval }); - - timer.on("tick", () => ticks.push(dayjs())); - - expect( - ticks.reduce( - (acc, tick, index) => - acc + - (index === 0 ? 100 : tick.diff(ticks[index - 1], "millisecond")), - 0, - ), - ).toEqual(ticks.length * interval); - - timer.on("end", done); - }); - - it("restarts the timer", async () => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(3, "seconds").toDate(); - const ticks: number[] = []; - - const timer = new Timer({ _id, startDate, endDate }); - - timer.once("start", () => { - const endTimer = setTimeout(() => { - timer.end(); - clearTimeout(endTimer); - }, 500); - }); - - await waitUntilEvent(timer, "end", 5000); - - timer.on("tick", (interval) => ticks.push(interval)); - - await waitUntilEvent(timer, "end", 5000, async () => timer.start()); - - expect(ticks.length).toBeGreaterThan(1); - }, 10000); - }); - - describe("start event", () => { - it("emits start event when timer starts", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - timer.on("start", (interval) => expect(interval).toBe(0)); - - timer.on("end", done); - }); - - it("does not restart the timer if it is not stopped", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(2, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - let startCount = 0; - - timer.on("start", () => { - startCount++; - }); - - timer.start(); - timer.start(); - timer.start(); - - timer.on("end", () => { - expect(startCount).toBe(1); - done(); - }); - }); - - it("emits start event automatically when start date is reached", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - timer.on("start", (interval) => expect(interval).toBe(0)); - timer.on("end", done); - }); - - it("allows manual start before scheduled start date", (done) => { - const start = dayjs().add(1, "seconds"); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ - _id, - startDate, - endDate, - interval: 100, - }); - - timer.on("start", () => { - expect(dayjs().isBefore(startDate)).toBe(true); - }); - - timer.on("end", done); - - timer.start(); - }); - }); - - describe("tick event", () => { - it("emits tick events at specified interval", (done) => { - const start = dayjs().add(2, "seconds"); - const startDate = start.toDate(); - const endDate = start.add(1, "second").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 200 }); - - const ticks: number[] = []; - - timer.on("tick", (interval: number) => ticks.push(interval)); - - timer.on("end", () => { - // Expect approximately 5 ticks in 1 second at 200ms interval - expect(ticks).toHaveLength(5); - done(); - }); - }); - - it("emits tick intervals as numbers", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 200 }); - - const ticks: number[] = []; - - timer.on("tick", (interval: number) => ticks.push(interval)); - - timer.on("end", () => { - expect(ticks.every((tick) => typeof tick === "number")).toBe(true); - done(); - }); - - timer.start(); - }); - - it("emits tick with interval values", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 200 }); - - const ticks: number[] = []; - - timer.on("tick", (interval: number) => ticks.push(interval)); - - timer.on("end", () => { - expect(ticks).toEqual(expect.arrayContaining([0, 1, 2, 3, 4])); - done(); - }); - }); - }); - - describe("end event", () => { - it("emits end event when timer completes", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(500, "milliseconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - timer.on("end", done); - - timer.start(); - }); - - it("can be ended manually before end date", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - timer.on("end", () => { - expect(dayjs().isBefore(endDate)).toBe(true); - done(); - }); - - timer.on("start", () => timer.end()); - }); - - it("emits end event even if ended before the start date is reached", (done) => { - const start = dayjs().add(2, "seconds"); - const startDate = start.toDate(); - const endDate = start.add(1, "seconds").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - timer.on("end", () => { - expect(dayjs().isBefore(startDate)).toBe(true); - expect(dayjs().isBefore(endDate)).toBe(true); - done(); - }); - - timer.end(); - }); - - it.skip("ends timer on end date", (done) => { - const start = dayjs(); - const startDate = start.toDate(); - const endDate = start.add(2, "second").toDate(); - - const timer = new Timer({ _id, startDate, endDate, interval: 100 }); - - timer.on("end", () => { - const end = dayjs(); - expect(end.isSameOrAfter(endDate)).toBe(true); - expect(end.valueOf() - 10).toBeLessThan(endDate.getTime()); - done(); - }); - }); - }); -}); diff --git a/packages/core/src/util/timer.ts b/packages/core/src/util/timer.ts deleted file mode 100644 index 7435c4526..000000000 --- a/packages/core/src/util/timer.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { EventEmitter2, ListenerFn } from "eventemitter2"; -import { - BehaviorSubject, - Observable, - Subject, - Subscription, - race, - switchMap, - takeUntil, - tap, - timer, -} from "rxjs"; -import { z } from "zod/v4"; -import { StringV4Schema } from "@core/types/type.utils"; -import dayjs from "@core/util/date/dayjs"; - -export class Timer { - public readonly _id: string; - public readonly startDate: Date; - public readonly endDate: Date; - - #interval: number; // in milliseconds - #start: Observable; - #end: Observable<0>; - #tick: Observable; - #manualStart: BehaviorSubject; - #manualEnd: Subject<0> = new Subject<0>(); - #subscription: Subscription | undefined; - #firstStart: boolean = true; - #emitter: EventEmitter2 = new EventEmitter2({ - wildcard: false, - delimiter: ".", - newListener: false, - removeListener: false, - maxListeners: 10, - verboseMemoryLeak: false, - ignoreErrors: false, - }); - - /** - * Timer utility to emit events at specified intervals - * between start and end dates. - * - * specify interval in milliseconds (default: 1000ms) - */ - constructor({ - _id, - startDate, - endDate, - interval, - autoStart = true, - }: { - _id: string; - startDate: Date; - endDate: Date; - interval?: number; // in milliseconds - autoStart?: boolean; - }) { - this._id = StringV4Schema.parse(_id); - this.startDate = this.#validateStartDate(startDate, endDate); - this.endDate = this.#validateEndDate(endDate, startDate); - this.#interval = z.number().min(100).default(1000).parse(interval); - this.#manualStart = new BehaviorSubject(this.startDate); - - this.#start = this.#manualStart - .pipe(switchMap((date) => timer(date, this.#interval))) - .pipe( - tap((interval) => { - if (interval === 0) this.#emitter.emit("start", interval); - }), - ); - - this.#end = race(timer(this.endDate), this.#manualEnd).pipe( - tap((interval) => this.#emitter.emit("end", interval)), - ); - - this.#tick = this.#start.pipe( - takeUntil(this.#end), - tap((interval) => this.#emitter.emit("tick", interval)), - ); - - if (autoStart) this.#init(true); - } - - get currentStartDate(): Date { - return this.#manualStart.getValue(); - } - - #validateStartDate(startDate: Date, endDate: Date): Date { - return z - .date() - .pipe( - z.custom((v) => dayjs(endDate).isAfter(v as Date), { - error: () => `Start date must be before End date`, - }), - ) - .parse(startDate); - } - - #validateEndDate(endDate: Date, startDate: Date): Date { - return z - .date() - .pipe( - z.custom((v) => dayjs(startDate).isBefore(v as Date), { - error: () => "End date must be after Start date", - }), - ) - .pipe( - z.custom((v) => dayjs().isBefore(v as Date), { - error: () => "End date must be in the future", - }), - ) - .parse(endDate); - } - - #init(first: boolean = false): void { - const firstStart = this.#firstStart; - - this.#firstStart = first; - - this.#subscription = this.#tick.subscribe({ - error: () => { - this.#firstStart = firstStart; - }, - }); - } - - public start(): void { - const started = !this.#subscription?.closed; - const startDate = this.#validateStartDate(new Date(), this.endDate); - - if (!this.#firstStart && started) return; - - if (started) { - this.once("end", () => { - this.#manualStart.next(startDate); - this.#init(); - }); - - return this.end(); - } - - this.#manualStart.next(startDate); - this.#init(); - } - - public end(): void { - this.#manualEnd.next(0); - } - - public close(): void { - this.#subscription?.unsubscribe(); - this.#emitter.removeAllListeners(); - Object.assign(this, { - start: () => {}, - end: () => {}, - on: () => {}, - off: () => {}, - once: () => {}, - }); - } - - public on(event: "start" | "end" | "tick", listener: ListenerFn): void { - this.#emitter.on(event, listener); - } - - public once(event: "start" | "end" | "tick", listener: ListenerFn): void { - this.#emitter.once(event, listener); - } - - public off(event: "start" | "end" | "tick", listener: ListenerFn): void { - this.#emitter.off(event, listener); - } -} diff --git a/packages/core/src/util/wait-until-event.util.ts b/packages/core/src/util/wait-until-event.util.ts index d4d16693e..98d580583 100644 --- a/packages/core/src/util/wait-until-event.util.ts +++ b/packages/core/src/util/wait-until-event.util.ts @@ -7,7 +7,6 @@ import type { CompassSocket, CompassSocketServer, } from "@core/types/websocket.types"; -import type { Timer } from "@core/util/timer"; export async function waitUntilEvent< Payload extends unknown[], @@ -20,8 +19,7 @@ export async function waitUntilEvent< | Server | CompassSocketServer | EventEmitter - | EventEmitter2 - | Timer, + | EventEmitter2, "once" >, event: Parameters<(typeof emitter)["once"]>["0"],