Created
June 27, 2024 20:39
-
-
Save trvswgnr/76c60ba77aefa810d9a22ffc914a7a38 to your computer and use it in GitHub Desktop.
custom setTimeout implementation in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }, | |
// ); | |
// }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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