Skip to content

Conversation

Copy link

Copilot AI commented Jan 7, 2026

Adds a promise-based interface for setRecursive modeled after Node.js timers/promises, enabling for await...of usage with critical enhancements that match the callback-based behavior: first iteration is immediate and delays happen after async work completes before scheduling the next iteration.

Implementation

  • AsyncIterator interface returning RecursiveTimeout<T> that yields optional values
  • Immediate first iteration - yields immediately without initial delay (matches callback behavior)
  • Delay after work - waits for loop body completion before applying delay for next iteration
  • AbortController integration via signal option, throwing proper DOMException with AbortError name
  • Resource cleanup via pendingReject callback and event listener removal

API

import { setRecursive } from 'recursive-timeout/promises'

// Basic usage - first iteration is immediate, then delays between iterations
for await (const _ of setRecursive(1000)) {
  await processWork()  // Next iteration waits for this to complete, THEN delays
  if (done) break
}

// With cancellation
const ac = new AbortController()
setTimeout(() => ac.abort(), 5000)

try {
  for await (const value of setRecursive(1000, 'tick', { signal: ac.signal })) {
    console.log(value)
  }
} catch (err) {
  // err.name === 'AbortError'
}

Key Differences from Node.js

Unlike Node.js setInterval which delays before each iteration, setRecursive:

  1. Yields immediately on first iteration (no initial delay)
  2. Delays after work completes (not before)
// Node.js setInterval: delays BEFORE yielding, fixed intervals
for await (const _ of setInterval(100)) {
  await work() // 50ms
}
// Timeline: Wait 100ms → yield → work (50ms) → Wait 100ms → yield
// Schedule: Yields at 100ms, 200ms, 300ms

// setRecursive: yields immediately, delays AFTER work
for await (const _ of setRecursive(100)) {
  await work() // 50ms
}
// Timeline: Yield → work (50ms) → Wait 100ms → yield → work (50ms) → Wait 100ms
// Schedule: Yields at 0ms, 150ms, 300ms

This matches the callback-based setRecursive behavior where the callback executes first, then the next timeout is scheduled.

Testing

Added 11 tests covering:

  • Basic iteration and value yielding with immediate first iteration
  • AbortController cancellation (both pre-abort and during)
  • Async work completion before next delay starts
  • Manual cleanup via clearRecursive()
  • Iterator protocol methods (next(), return())
  • Timing verification that delay happens after work completes
Original prompt

Purpose an idea for a promise-based implementation (and usage) of setRecursive, by analogy with promise-based setTimeout and setInterval from the built-in NodeJS module 'node:timers/promises'


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 4 commits January 7, 2026 21:28
Co-authored-by: parzhitsky <59528510+parzhitsky@users.noreply.github.com>
Co-authored-by: parzhitsky <59528510+parzhitsky@users.noreply.github.com>
…examples

Co-authored-by: parzhitsky <59528510+parzhitsky@users.noreply.github.com>
…and remove unused types

Co-authored-by: parzhitsky <59528510+parzhitsky@users.noreply.github.com>
Copilot AI changed the title [WIP] Add promise-based implementation for setRecursive Implement promise-based API for setRecursive with AsyncIterator Jan 7, 2026
Copilot AI requested a review from parzhitsky January 7, 2026 21:42
Copy link
Member

@parzhitsky parzhitsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no different from the built-in setInterval:

import { setInterval } from 'timers/promises'

for await (const _ of setInterval(1000)) {
  console.log('tick')
}

The value of setRecursive should be that it doesn't just blindly waits for whatever there delay is, but also waits for the handler to finish before starting the new countdown.

This already works with the callback-based interface (where the handler is provided as a callback argument). Now define a promise-based version with whatever interface makes most sense (developing an interface is part of the task).

…delay after work

Co-authored-by: parzhitsky <59528510+parzhitsky@users.noreply.github.com>
Copy link
Author

Copilot AI commented Jan 7, 2026

This is no different from the built-in setInterval:

Fixed in commit 6abe8f9. The promise-based API now correctly matches the callback-based behavior:

  1. First iteration yields immediately (no initial delay)
  2. Delay happens AFTER the loop body completes (not before)

This ensures the delay is truly BETWEEN iterations, just like the callback version where the callback executes first, then the next timeout is scheduled.

Timeline comparison:

  • Node.js setInterval: Wait 100ms → yield → work → Wait 100ms → yield (yields at 100ms, 200ms, 300ms)
  • Our setRecursive: Yield → work → Wait 100ms → yield → work (yields at 0ms, 150ms, 300ms with 50ms work)

The implementation tracks whether it's the first iteration and skips the delay only for that first call, ensuring subsequent iterations wait for the full delay after the previous iteration's work completes.

Copilot AI requested a review from parzhitsky January 7, 2026 22:00
@parzhitsky parzhitsky closed this Jan 7, 2026
@parzhitsky parzhitsky added Change: minor [Issue / PR] describes a non-breaking change, such as adding a new functionality Domain: main [Issue / PR] describes change in the functionality, its optimization Priority: high [Issue / PR] must be addressed as soon as possible Rejected: impractical [Issue] exists, but its resolution brings zero to low benefit and/or does not worth the effort Type: improvement [Issue / PR] addresses lack of a functionality or an open possibility of enhancement labels Jan 7, 2026
@parzhitsky parzhitsky deleted the copilot/add-promise-based-setrecursive branch January 7, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Change: minor [Issue / PR] describes a non-breaking change, such as adding a new functionality Domain: main [Issue / PR] describes change in the functionality, its optimization Priority: high [Issue / PR] must be addressed as soon as possible Rejected: impractical [Issue] exists, but its resolution brings zero to low benefit and/or does not worth the effort Type: improvement [Issue / PR] addresses lack of a functionality or an open possibility of enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants