prutill
Version:
Environment-agnostic production-ready promise utility library for managing promise stacks and race conditions. Supports Node.js, Deno, and browsers.
308 lines (250 loc) • 9.44 kB
text/typescript
import {
sleep,
retry,
withTimeout,
sequential,
createDeferred,
makeCancellable,
withExtendableTimeout,
} from "./utilities";
import { promiseWrapper } from "./promiseWrapper";
jest.mock("./promiseWrapper", () => ({
promiseWrapper: jest.fn().mockImplementation(fn => fn()),
}));
describe("utilities", () => {
describe("sleep", () => {
it("should resolve after specified time", async () => {
const start = Date.now();
await sleep(100);
const duration = Date.now() - start;
expect(duration).toBeGreaterThanOrEqual(95); // Allow small timing variance
expect(duration).toBeLessThan(150); // Should not take too long
});
it("should handle zero delay", async () => {
const start = Date.now();
await sleep(0);
const duration = Date.now() - start;
expect(duration).toBeLessThanOrEqual(50);
});
});
describe("retry", () => {
it("should retry failed operations", async () => {
let attempts = 0;
const fn = () => {
attempts++;
if (attempts === 2) throw new Error("Not yet");
if (attempts <= 1) return Promise.reject("Not yet");
return Promise.resolve("success");
};
const result = await retry(fn, 3, 100);
expect(result).toBe("success");
expect(attempts).toBe(3);
});
it("should throw if max attempts reached", async () => {
const fn1 = () => Promise.reject(new Error("Always fails"));
await expect(retry(fn1, 2, 100)).rejects.toThrow("Always fails");
const fn2 = () => Promise.reject(-1);
await expect(retry(fn2, 2, 100)).rejects.toBe(-1);
});
it("should work with sync functions", async () => {
let attempts = 0;
const fn = () => {
attempts++;
if (attempts < 2) throw new Error("Not yet");
return "sync success";
};
const result = await retry(fn, 2, 100);
expect(result).toBe("sync success");
expect(attempts).toBe(2);
});
it("should handle immediate success", async () => {
const fn = () => "immediate";
const result = await retry(fn, 3, 100);
expect(result).toBe("immediate");
});
it("should handle direct promise resolution", async () => {
const fn = jest.fn().mockResolvedValue("direct success");
const result = await retry(fn, 3, 100);
expect(result).toBe("direct success");
expect(fn).toHaveBeenCalledTimes(1);
});
it("should directly use promiseWrapper for successful promises", async () => {
const testFn = () => Promise.resolve("success from wrapper");
const result = await retry(testFn);
expect(promiseWrapper).toHaveBeenCalledWith(testFn);
expect(result).toBe("success from wrapper");
jest.unmock("./promiseWrapper");
});
});
describe("withTimeout", () => {
it("should resolve if promise completes within timeout", async () => {
const promise = sleep(100).then(() => "done");
const result = await withTimeout(promise, 200);
expect(result).toBe("done");
});
it("should reject if promise takes too long", async () => {
const promise = sleep(200).then(() => "done");
await expect(withTimeout(promise, 100)).rejects.toThrow("Operation timed out");
});
it("should use custom error message", async () => {
const promise = sleep(200);
await expect(withTimeout(promise, 100, "Custom timeout")).rejects.toThrow("Custom timeout");
});
it("should handle immediate resolution", async () => {
const promise = Promise.resolve("instant");
const result = await withTimeout(promise, 100);
expect(result).toBe("instant");
});
it("should handle promise rejection", async () => {
const promise = Promise.reject(new Error("Original error"));
await expect(withTimeout(promise, 100)).rejects.toThrow("Original error");
});
});
describe("sequential", () => {
it("should process items in sequence", async () => {
const order: number[] = [];
const tasks = Array(3)
.fill(0)
.map((_, i) => async () => {
order.push(i);
await sleep(50);
return i * 2;
});
const results = await sequential(tasks);
expect(results).toEqual([0, 2, 4]);
expect(order).toEqual([0, 1, 2]);
});
it("should handle empty task list", async () => {
const results = await sequential([]);
expect(results).toEqual([]);
});
it("should stop on first error", async () => {
const order: number[] = [];
const tasks = [
async () => {
order.push(1);
return 1;
},
async () => {
order.push(2);
throw new Error("Task 2 failed");
},
async () => {
order.push(3);
return 3;
},
];
await expect(sequential(tasks)).rejects.toThrow("Task 2 failed");
expect(order).toEqual([1, 2]); // Third task should not execute
});
});
describe("createDeferred", () => {
it("should allow external resolution", async () => {
const deferred = createDeferred<string>();
setTimeout(() => deferred.resolve("done"), 100);
const result = await deferred.promise;
expect(result).toBe("done");
});
it("should allow external rejection", async () => {
const deferred = createDeferred<string>();
setTimeout(() => deferred.reject(new Error("failed")), 100);
await expect(deferred.promise).rejects.toThrow("failed");
});
it("should handle immediate resolution", async () => {
const deferred = createDeferred<string>();
deferred.resolve("instant");
const result = await deferred.promise;
expect(result).toBe("instant");
});
it("should handle immediate rejection", async () => {
const deferred = createDeferred<string>();
deferred.reject(new Error("instant fail"));
await expect(deferred.promise).rejects.toThrow("instant fail");
});
});
describe("makeCancellable", () => {
it("should resolve normally if not cancelled", async () => {
const promise = sleep(100).then(() => "done");
const { promise: cancellable } = makeCancellable(promise);
const result = await cancellable;
expect(result).toBe("done");
});
it("should reject when cancelled", async () => {
const promise = sleep(200).then(() => "done");
const { promise: cancellable, cancel } = makeCancellable(promise);
setTimeout(cancel, 100);
await expect(cancellable).rejects.toThrow("Promise was cancelled");
});
it("should handle immediate cancellation", async () => {
const promise = sleep(100).then(() => "done");
const { promise: cancellable, cancel } = makeCancellable(promise);
cancel();
await expect(cancellable).rejects.toThrow("Promise was cancelled");
});
it("should handle cancellation of already rejected promise", async () => {
const promise = Promise.reject(new Error("Original error"));
const { promise: cancellable, cancel } = makeCancellable(promise);
cancel();
await expect(cancellable).rejects.toThrow("Promise was cancelled");
});
it("should handle cancellation after promise resolves", async () => {
const promise = Promise.resolve("success");
const { promise: cancellable, cancel } = makeCancellable(promise);
const result = await cancellable;
expect(result).toBe("success");
cancel();
});
it("should handle promise rejection and cancellation correctly", async () => {
const errorPromise = Promise.reject(new Error("Test error"));
const { promise: cancellablePromise, cancel } = makeCancellable(errorPromise);
cancel();
await expect(cancellablePromise).rejects.toThrow("Promise was cancelled");
const delayedErrorPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Delayed error")), 50);
});
const { promise: delayedCancellable, cancel: delayedCancel } = makeCancellable(delayedErrorPromise);
delayedCancel();
await expect(delayedCancellable).rejects.toThrow("Promise was cancelled");
});
});
describe("withExtendableTimeout", () => {
it("should resolve if completed within timeout", async () => {
const promise = sleep(100).then(() => "done");
const { promise: withTimeout } = withExtendableTimeout(promise, 200);
const result = await withTimeout;
expect(result).toBe("done");
});
it("should reject if timeout reached", async () => {
const promise = sleep(300).then(() => "done");
const { promise: withTimeout } = withExtendableTimeout(promise, 100);
await expect(withTimeout).rejects.toThrow("Operation timed out");
});
it("should allow extending timeout", async () => {
const promise = sleep(300).then(() => "done");
const { promise: withTimeout, resetTimeout } = withExtendableTimeout(promise, 200);
setTimeout(resetTimeout, 150);
const result = await withTimeout;
expect(result).toBe("done");
});
it("should use custom error message", async () => {
const promise = sleep(200);
const { promise: withTimeout } = withExtendableTimeout(promise, 100, "Custom timeout");
await expect(withTimeout).rejects.toThrow("Custom timeout");
});
it("should handle multiple timeout resets", async () => {
const promise = sleep(400).then(() => "done");
const { promise: withTimeout, resetTimeout } = withExtendableTimeout(promise, 200);
setTimeout(resetTimeout, 150);
setTimeout(resetTimeout, 250);
setTimeout(resetTimeout, 350);
const result = await withTimeout;
expect(result).toBe("done");
});
it("should handle immediate resolution", async () => {
const promise = Promise.resolve("instant");
const { promise: withTimeout } = withExtendableTimeout(promise, 100);
const result = await withTimeout;
expect(result).toBe("instant");
});
});
});