// oxlint-disable require-await import { expect, test, vi } from "vitest" import { promiseIsFailResult, promiseCatch, promiseCreateFailResult, promiseDeferred, promiseFilterFailResults, promiseFilterSuccessResults, promiseFinally, promiseForever, promiseInterval, promiseQueue, promiseRetryUntil, promiseRetryWhile, promiseThen, } from "#Source/basic/index.ts" test("promiseThen chains and transforms resolved values", async () => { const example1 = await promiseThen(Promise.resolve(3), (value: number) => value * 2) const example2 = await promiseThen(Promise.resolve("ok"), (value: string) => `${value}!`) expect(example1).toBe(6) expect(example2).toBe("ok!") }) test("promiseCatch handles rejected and resolved promises", async () => { const example1 = await promiseCatch(Promise.reject(new Error("x")), () => "fallback") const example2 = await promiseCatch(Promise.resolve(3), () => 0) expect(example1).toBe("fallback") expect(example2).toBe(3) }) test("promiseFinally runs finalizer and preserves resolution", async () => { let cleaned = false const example1 = await promiseFinally(Promise.resolve(10), () => { cleaned = true }) expect(example1).toBe(10) expect(cleaned).toBe(true) }) test("promiseDeferred exposes external resolve and reject for the created promise", async () => { const resolvedDeferred = promiseDeferred() resolvedDeferred.resolve(42) await expect(resolvedDeferred.promise).resolves.toBe(42) expect(typeof resolvedDeferred.resolve).toBe("function") expect(typeof resolvedDeferred.reject).toBe("function") const rejectedDeferred = promiseDeferred() const reason = new Error("deferred failure") rejectedDeferred.reject(reason) await expect(rejectedDeferred.promise).rejects.toBe(reason) }) test("promiseCreateFailResult creates standardized failure objects", () => { const reason = new Error("x") const failResult = promiseCreateFailResult(reason) expect(promiseIsFailResult(failResult)).toBe(true) expect(failResult.reason).toBe(reason) }) test("promiseIsFailResult identifies standardized failure objects", () => { const failResult = promiseCreateFailResult(new Error("x")) expect(promiseIsFailResult(failResult)).toBe(true) expect(promiseIsFailResult({ reason: "x" })).toBe(false) expect(promiseIsFailResult(null)).toBe(false) }) test("promiseFilterSuccessResults keeps only successful values", () => { const failResult = promiseCreateFailResult(new Error("x")) const filtered = promiseFilterSuccessResults([1, failResult, 2]) expect(filtered).toEqual([1, 2]) }) test("promiseFilterFailResults keeps failure values with original indices", async () => { const results = await promiseQueue([ async (): Promise => 1, async () => { throw new Error("a") }, async () => { throw new Error("b") }, ]) const filtered = promiseFilterFailResults(results) expect(filtered).toHaveLength(2) expect(filtered[0]?.index).toBe(1) expect(filtered[1]?.index).toBe(2) expect(promiseIsFailResult(filtered[0])).toBe(true) expect(promiseIsFailResult(filtered[1])).toBe(true) }) test("promiseQueue executes makers in sequence and keeps failed results", async () => { const results = await promiseQueue([ async (): Promise => 1, async ({ previousResult, index }): Promise => { return promiseIsFailResult(previousResult) ? -1 : previousResult + index + 1 }, async ({ previousResult }): Promise => { if (promiseIsFailResult(previousResult)) { return -1 } throw new Error(String(previousResult)) }, async ({ previousResult }): Promise => { return promiseIsFailResult(previousResult) ? -1 : previousResult + 1 }, ]) expect(results[0]).toBe(1) expect(results[1]).toBe(3) expect(promiseIsFailResult(results[2])).toBe(true) expect(results[3]).toBe(-1) }) test("promiseRetryWhile retries while predicate is true", async () => { let successAttempts = 0 const success = await promiseRetryWhile( (value) => value < 3, async () => { successAttempts = successAttempts + 1 return successAttempts }, { maxTryIndex: 5 }, ) let failedAttempts = 0 const failed = await promiseRetryWhile( () => true, async () => { failedAttempts = failedAttempts + 1 throw new Error("x") }, { maxTryIndex: 2 }, ) let singleAttemptCount = 0 const singleAttempt = await promiseRetryWhile( () => true, async () => { singleAttemptCount = singleAttemptCount + 1 return singleAttemptCount }, { maxTryIndex: 0 }, ) await expect( promiseRetryWhile( async () => false, async () => 1, { maxTryIndex: -1 }, ), ).rejects.toThrow("`maxTryIndex` must be greater than or equal to 0.") expect(success).toBe(3) expect(successAttempts).toBe(3) expect(promiseIsFailResult(failed)).toBe(true) expect(failedAttempts).toBe(3) expect(singleAttempt).toBe(1) expect(singleAttemptCount).toBe(1) }) test("promiseRetryUntil retries until predicate becomes true", async () => { let successAttempts = 0 const success = await promiseRetryUntil( (value, index) => value >= 2 && index >= 1, async () => { successAttempts = successAttempts + 1 return successAttempts }, { maxTryIndex: 5 }, ) let failedAttempts = 0 const failed = await promiseRetryUntil( () => false, async () => { failedAttempts = failedAttempts + 1 throw new Error("x") }, { maxTryIndex: 2 }, ) let singleAttemptCount = 0 const singleAttempt = await promiseRetryUntil( () => false, async () => { singleAttemptCount = singleAttemptCount + 1 return singleAttemptCount }, { maxTryIndex: 0 }, ) await expect( promiseRetryUntil( async () => true, async () => 1, { maxTryIndex: -1 }, ), ).rejects.toThrow("`maxTryIndex` must be greater than or equal to 0.") expect(success).toBe(2) expect(successAttempts).toBe(2) expect(promiseIsFailResult(failed)).toBe(true) expect(failedAttempts).toBe(3) expect(singleAttempt).toBe(1) expect(singleAttemptCount).toBe(1) }) test("promiseInterval runs by interval and returns a stopper", async () => { vi.useFakeTimers() try { const calls: number[] = [] const stop = promiseInterval(10, async (time) => { calls.push(time) return time }) vi.advanceTimersByTime(35) await Promise.resolve() stop() vi.advanceTimersByTime(50) expect(calls).toEqual([0, 1, 2]) expect(typeof stop).toBe("function") } finally { vi.useRealTimers() } }) test("promiseInterval logs rejected runs instead of leaving them unhandled", async () => { vi.useFakeTimers() const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined) try { promiseInterval(10, async () => { throw new Error("interval failure") }) vi.advanceTimersByTime(10) await Promise.resolve() await Promise.resolve() expect(errorSpy).toHaveBeenCalledTimes(1) expect(errorSpy.mock.calls[0]?.[0]).toBe("[promiseInterval] unexpected error occurred:") expect(errorSpy.mock.calls[0]?.[1]).toBeInstanceOf(Error) } finally { vi.clearAllTimers() errorSpy.mockRestore() vi.useRealTimers() } }) test("promiseForever loops continuously and reports rejections", async () => { vi.useFakeTimers() try { let value = 0 const onRejected = vi.fn() const promiseMaker = vi.fn(async () => { value = value + 1 if (value === 1) { throw new Error("first") } return value }) const result = promiseForever(promiseMaker, { breakTime: 5, onRejected }) await Promise.resolve() await Promise.resolve() expect(promiseMaker).toHaveBeenCalledTimes(1) expect(onRejected).toHaveBeenCalledTimes(1) expect(onRejected.mock.calls[0]?.[0]).toBe(0) expect(promiseIsFailResult(onRejected.mock.calls[0]?.[1])).toBe(true) expect(result.index).toBe(0) expect(result.isStopped).toBe(false) vi.advanceTimersByTime(16) await Promise.resolve() await Promise.resolve() expect(promiseMaker.mock.calls.length).toBeGreaterThanOrEqual(2) result.stop() expect(result.isStopped).toBe(true) } finally { vi.clearAllTimers() vi.useRealTimers() } }) test("promiseForever stop prevents scheduling the next run when current run has not settled", async () => { vi.useFakeTimers() try { const resolvers: Array<(value: number) => void> = [] const promiseMaker = vi.fn(async () => { return await new Promise((resolve) => { resolvers.push(resolve) }) }) const result = promiseForever(promiseMaker, { breakTime: 5 }) expect(promiseMaker).toHaveBeenCalledTimes(1) result.stop() expect(result.isStopped).toBe(true) resolvers[0]?.(1) await Promise.resolve() await Promise.resolve() vi.advanceTimersByTime(20) await Promise.resolve() await Promise.resolve() expect(promiseMaker).toHaveBeenCalledTimes(1) } finally { vi.clearAllTimers() vi.useRealTimers() } })