Repository: denoland/std (TypeScript)

Issue:
`@std/async` `delay(Infinity)` or `delay(0x80000000)` resolve immediately
**Describe the bug**

`@std/async` `delay(Infinity)` or `delay(0x80000000)` resolve immediately. This is due to the value passed to `setTimeout` being coerced to an i32, with a [max value of `0x7fffffff`](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#maximum_delay_value).

**Steps to Reproduce**

```ts
import { delay } from '@std/async'
delay(0x80000000).then(() => console.log('done')) // logs immediately

await Promise.race([1, Infinity].map((n) => delay(n).then(() => n)))
// returns Infinity
```

**Expected behavior**

`delay` not to resolve until the time specified has elapsed (never resolve if `Infinity`).

This probably isn't important for most use cases, as `Deno.cron` is a much better solution for scheduling tasks etc, but `delay(Infinity)` (maybe also passing an abort signal etc) would be a handy shorthand for "never resolve" for cases like long-polling to implement simple dev server watch/reload logic.

**Environment**

- OS: WSL
- deno version: 2.4.0
- std version: @std/async@1.0.13



Discussion:

Patch:
diff --git a/async/delay.ts b/async/delay.ts
index 7beb7fd0519b..48486e65c17d 100644
--- a/async/delay.ts
+++ b/async/delay.ts
@@ -48,14 +48,14 @@ export function delay(ms: number, options: DelayOptions = {}): Promise<void> {
   if (signal?.aborted) return Promise.reject(signal.reason);
   return new Promise((resolve, reject) => {
     const abort = () => {
-      clearTimeout(i);
+      clearTimeout(+i);
       reject(signal?.reason);
     };
     const done = () => {
       signal?.removeEventListener("abort", abort);
       resolve();
     };
-    const i = setTimeout(done, ms);
+    const i = setArbitraryLengthTimeout(done, ms);
     signal?.addEventListener("abort", abort, { once: true });
     if (persistent === false) {
       try {
@@ -71,3 +71,26 @@ export function delay(ms: number, options: DelayOptions = {}): Promise<void> {
     }
   });
 }
+
+const I32_MAX = 2 ** 31 - 1;
+
+function setArbitraryLengthTimeout(
+  callback: () => void,
+  delay: number,
+): { valueOf(): number } {
+  // ensure non-negative integer (but > I32_MAX is OK, even if Infinity)
+  let currentDelay = delay = Math.trunc(Math.max(delay, 0) || 0);
+  const start = Date.now();
+  let timeoutId: number;
+
+  const queueTimeout = () => {
+    currentDelay = delay - (Date.now() - start);
+    timeoutId = currentDelay > I32_MAX
+      ? setTimeout(queueTimeout, I32_MAX)
+      : setTimeout(callback, currentDelay);
+  };
+
+  queueTimeout();
+
+  return { valueOf: () => timeoutId };
+}


Test patch:
diff --git a/async/delay_test.ts b/async/delay_test.ts
index af2622a361fb..5246a44ed7cc 100644
--- a/async/delay_test.ts
+++ b/async/delay_test.ts
@@ -8,6 +8,7 @@ import {
   assertStrictEquals,
 } from "@std/assert";
 import { assertSpyCalls, stub } from "@std/testing/mock";
+import { FakeTime } from "@std/testing/time";
 
 // https://dom.spec.whatwg.org/#interface-AbortSignal
 function assertIsDefaultAbortReason(reason: unknown) {
@@ -137,3 +138,55 @@ Deno.test({
   sanitizeResources: false,
   sanitizeOps: false,
 });
+
+Deno.test({
+  name: "delay() doesn't resolve early with `ms` argument > 0x7fffffff",
+  async fn(t) {
+    for (
+      const bigNum of [
+        0x7fffffff,
+        0x80000000,
+        Number.MAX_SAFE_INTEGER,
+        Infinity,
+      ]
+    ) {
+      await t.step(`delay(${bigNum})`, async () => {
+        const result = await Promise.race(
+          [1, bigNum].map((n) => delay(n).then(() => n)),
+        );
+        assertEquals(result, 1);
+      });
+    }
+  },
+  sanitizeResources: false,
+  sanitizeOps: false,
+});
+
+async function advanceUntilComplete<T>(
+  time: FakeTime,
+  tickMs: number,
+  task: () => Promise<T>,
+): Promise<T> {
+  let done = false;
+
+  const result = (async () => {
+    try {
+      return await task();
+    } finally {
+      done = true;
+    }
+  })();
+
+  while (!done) await time.tickAsync(tickMs);
+
+  return result;
+}
+
+Deno.test({
+  name: "delay() runs to completion with `ms` argument > 0x7fffffff",
+  async fn() {
+    using time = new FakeTime(0);
+    await advanceUntilComplete(time, 2 ** 30, () => delay(2 ** 32));
+    assertEquals(time.now, 2 ** 32);
+  },
+});