Skip to content

Instantly share code, notes, and snippets.

@trvswgnr
Created June 27, 2024 20:39
Show Gist options
  • Save trvswgnr/76c60ba77aefa810d9a22ffc914a7a38 to your computer and use it in GitHub Desktop.
Save trvswgnr/76c60ba77aefa810d9a22ffc914a7a38 to your computer and use it in GitHub Desktop.
custom setTimeout implementation in TypeScript
import { describe, it, expect, mock } from "bun:test";
import { Timeout } from "./shared";
describe("Timeout", () => {
it("should call the callback after a specific time", async () => {
expect.hasAssertions();
return await new Promise<void>((resolve) => {
const start = Date.now();
Timeout.set(() => {
expect(Date.now() - start).toBeGreaterThanOrEqual(100);
resolve();
}, 100);
});
});
it("should cancel the timeout", async () => {
const timeout1 = Timeout.set(() => {
expect(true).toBe(false);
}, 0);
Timeout.clear(timeout1);
expect(true).toBe(true);
let called = false;
const timeout2 = Timeout.set(() => {
called = true;
}, 10);
await new Promise<void>((resolve) => Timeout.set(resolve, 15));
Timeout.clear(timeout2);
expect(called).toBe(true);
});
it("should set and clear a timeout", async () => {
const callback = mock(() => {});
const timeout = Timeout.set(callback, 200);
await new Promise<void>((resolve) => Timeout.set(resolve, 100));
Timeout.clear(timeout);
expect(callback).not.toHaveBeenCalled();
});
it("should call the callback when the timeout is up", async () => {
const callback = mock(() => {});
const timeout = Timeout.set(callback, 10);
await new Promise<void>((resolve) => Timeout.set(resolve, 15));
Timeout.clear(timeout);
expect(callback).toHaveBeenCalled();
});
it.skip(
"should work with long timeouts",
async () => {
const callback1 = mock(() => {});
const callback2 = mock(() => {});
const timeout1 = Timeout.set(callback1, 10000);
const timeout2 = Timeout.set(callback2, 17000);
await new Promise<void>((resolve) => Timeout.set(resolve, 15000));
Timeout.clear(timeout1);
Timeout.clear(timeout2);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
},
{ timeout: 20000 },
);
});
// describe("Builtin Timeouts (control)", () => {
// it("should set and clear a timeout", async () => {
// const callback = mock(() => {});
// const timeout = setTimeout(callback, 200);
// await new Promise<void>((resolve) => setTimeout(resolve, 100));
// clearTimeout(timeout);
// expect(callback).not.toHaveBeenCalled();
// });
// it("should call the callback when the timeout is up", async () => {
// const callback = mock(() => {});
// const timeout = setTimeout(callback, 10);
// await new Promise<void>((resolve) => setTimeout(resolve, 15));
// clearTimeout(timeout);
// expect(callback).toHaveBeenCalled();
// });
// it.skip(
// "should work with long timeouts",
// async () => {
// const callback1 = mock(() => {});
// const callback2 = mock(() => {});
// const timeout1 = setTimeout(callback1, 10000);
// const timeout2 = setTimeout(callback2, 17000);
// await new Promise<void>((resolve) => setTimeout(resolve, 15000));
// clearTimeout(timeout1);
// clearTimeout(timeout2);
// expect(callback1).toHaveBeenCalled();
// expect(callback2).not.toHaveBeenCalled();
// },
// { timeout: 20000 },
// );
// });
export type Args = readonly any[];
export function nextTick() {
return new Promise<void>((resolve) => resolve());
}
export type Thunk<T> = () => T;
export module Thunk {
export function force<T>(fn: Thunk<T>): T {
return fn();
}
export function from<T>(fn: () => T): Thunk<T> {
return fn;
}
}
export type Timeout = number;
export module Timeout {
let id = 0;
const timeouts = new Set<Timeout>();
export function set(callback: Thunk<void>, ms: number): Timeout {
let timeout: Timeout;
let execTime: number;
Thunk.force(() => {
timeout = id++;
timeouts.add(timeout);
execTime = Date.now() + ms;
waitUntil(execTime).then(() => {
if (!timeouts.has(timeout)) return;
Thunk.force(callback);
timeouts.delete(timeout);
});
});
return timeout!;
}
async function waitUntil(time: number): Promise<void> {
await nextTick();
while (Date.now() < time) await nextTick();
}
export function clear(timeout: Timeout): void {
return void timeouts.delete(timeout);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment