Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 114 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ setRecursive(runsForOneSecond, 500)

- **Dual Module Support:** Works seamlessly with both ECMAScript Modules (`import`) and CommonJS (`require`).
- **Familiar API:** Designed as a drop-in replacement for `setInterval`.
- **Promise-based API:** ⚠️ _(coming soon)_ ⚠️ A promise-based interface for use with asynchronous callbacks.
- **Promise-based API:** A promise-based interface using `for await...of` loops, inspired by Node.js `timers/promises`.
- Returns an AsyncIterator for use with `for await...of`
- Supports AbortController for cancellation
- **Waits for async work to complete** before scheduling the next iteration (unlike Node.js `setInterval`)

## Installation

Expand Down Expand Up @@ -99,17 +102,70 @@ setRecursive(sum, 100, 42, 17)
// ✅ OK (logs 59 every ~100 milliseconds)
```

#### ECMAScript (promise-based) – _coming soon_ ⚠️
#### ECMAScript (promise-based)

The promise-based API uses `for await...of` loops and returns an AsyncIterator, similar to Node.js `timers/promises`:

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

// Basic usage - first iteration happens immediately, then waits 1000ms between iterations
for await (const _ of setRecursive(1000)) {
console.log('tick')
// break when done
}
```

**Key differences from Node.js `setInterval`:**

1. **First iteration is immediate** (no initial delay)
2. **Delay happens AFTER the loop body completes** (not before)

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

// Node.js setInterval behavior:
// Wait 1000ms → yield → work (500ms) → Wait 1000ms → yield → work
// Iterations at: 1000ms, 2000ms, 3000ms...

// setRecursive behavior:
// Yield immediately → work (500ms) → Wait 1000ms → yield → work (500ms) → Wait 1000ms
// Iterations at: 0ms, 1500ms, 3000ms...

for await (const _ of setRecursive(1000)) {
console.log('start')
await doAsyncWork() // Takes 500ms
console.log('end')
// Next iteration starts 1000ms AFTER doAsyncWork() completes
// Total time between iterations: 500ms (work) + 1000ms (delay) = 1500ms
}
```

**Cancellation with AbortController:**

```js
import { setRecursive, clearRecursive } from 'recursive-timeout/promises'
const controller = new AbortController()

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000)

try {
for await (const _ of setRecursive(1000, undefined, { signal: controller.signal })) {
console.log('tick')
}
} catch (err) {
console.log('Cancelled')
}
```

**Alternative import style:**

```js
import { promises } from 'recursive-timeout'

promises.setRecursive(…)
promises.clearRecursive(…)
for await (const _ of promises.setRecursive(1000)) {
console.log('tick')
}
```

#### CommonJS
Expand All @@ -118,19 +174,68 @@ promises.clearRecursive(…)
const { setRecursive, clearRecursive } = require('recursive-timeout')
```

#### CommonJS (promise-based) – _coming soon_ ⚠️
#### CommonJS (promise-based)

```js
const { setRecursive, clearRecursive } = require('recursive-timeout/promises')
const { setRecursive } = require('recursive-timeout/promises')

;(async () => {
for await (const _ of setRecursive(1000)) {
console.log('tick')
}
})()
```

**Alternative import style:**

```js
const { promises } = require('recursive-timeout')

promises.setRecursive(…)
promises.clearRecursive(…)
;(async () => {
for await (const _ of promises.setRecursive(1000)) {
console.log('tick')
}
})()
```

## Promise-based API: Comparison with Node.js `timers/promises`

The promise-based API is inspired by Node.js `timers/promises` but with crucial differences:

### Node.js `setInterval` (from `timers/promises`)
- Waits for delay **before** yielding
- Schedules next iteration at fixed intervals regardless of work duration
- First iteration waits for the delay

```js
import { setInterval } from 'node:timers/promises'

for await (const _ of setInterval(100)) {
await asyncWork() // Takes 50ms
// Iterations happen at fixed 100ms intervals
}
```
Timeline: **Wait 100ms** → yield → work (50ms) → **Wait 100ms** → yield → work (50ms)
Schedule: Yields at 100ms, 200ms, 300ms...

### `recursive-timeout` `setRecursive` (promise-based)
- Yields **immediately** on first iteration
- Waits for delay **after** work completes
- Schedules next iteration after work finishes

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

for await (const _ of setRecursive(100)) {
await asyncWork() // Takes 50ms
// Next iteration waits for work to complete, then delays
}
```
Timeline: Yield immediately → work (50ms) → **Wait 100ms** → yield → work (50ms) → **Wait 100ms**
Schedule: Yields at 0ms, 150ms, 300ms...

This matches the "recursive timeout" behavior of the callback-based API, ensuring the delay happens between iterations, not before them.

## License

This project is licensed under the MIT License.
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions src/with-promises/clear-recursive-timeout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createRecursiveTimeout } from './create-recursive-timeout'
import { clearRecursiveTimeout } from './clear-recursive-timeout'

describe(clearRecursiveTimeout, () => {
it('should stop the recursive timeout', async () => {
const recursive = createRecursiveTimeout(50)

let count = 0
const promise = (async () => {
try {
for await (const _ of recursive) {
count++
}
} catch (err) {
// Expected when clear() is called
}
})()

// Wait a bit and then clear
await new Promise(resolve => setTimeout(resolve, 125))
clearRecursiveTimeout(recursive)

// Wait for the loop to finish
await promise

// Should have completed ~2 iterations before being cleared
expect(count).toBeGreaterThanOrEqual(2)
expect(count).toBeLessThanOrEqual(3)
})

it('should be idempotent', () => {
const recursive = createRecursiveTimeout(50)

clearRecursiveTimeout(recursive)
clearRecursiveTimeout(recursive) // Should not throw

expect(true).toBe(true) // If we get here, no error was thrown
})
})
12 changes: 10 additions & 2 deletions src/with-promises/clear-recursive-timeout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { RecursiveTimeout, ArgsShape } from './recursive-timeout'
import type { RecursiveTimeout } from './recursive-timeout'

export function clearRecursiveTimeout(recursive: RecursiveTimeout<ArgsShape>): void {
/**
* Cancels a recursive timeout created by `setRecursive`.
*
* Note: When using the promise-based API, you can also use AbortController
* to cancel the timeout, or simply break out of the `for await...of` loop.
*
* @param recursive - The RecursiveTimeout instance to clear
*/
export function clearRecursiveTimeout<T>(recursive: RecursiveTimeout<T>): void {
recursive.clear()
}
Loading