npm package: @nxtedition/yield
Description: Cooperative yielding for Node.js to keep servers and applications responsive.
Latest version: 1.0.16

# @nxtedition/yield

Cooperative yielding for Node.js to keep servers and applications responsive.

When using synchronous APIs like `DatabaseSync`, sync fs API's such as `fs.readFileSync`, or `fs.statSync` — or simply performing lots of synchronous computation — the event loop is blocked and cannot process I/O, timers, or incoming requests. Even long-running chains of asynchronous microtasks (e.g. deeply nested `Promise.then` chains) can starve the event loop in the same way.

This library provides a simple mechanism to periodically yield control back to the event loop during these long-running operations, preventing event loop starvation and keeping your application responsive.

## Install

```sh
npm install @nxtedition/yield
```

## Usage

### Promise-based

```js
import { maybeYield } from '@nxtedition/yield'
import { DatabaseSync } from 'node:sqlite'

const db = new DatabaseSync('data.db')

for (const row of db.prepare('SELECT * FROM large_table').iterate()) {
  processRow(row)
  const yielded = maybeYield()
  if (yielded) {
    await yielded // give the event loop a turn
  }
}
```

### Callback-based

```js
import { maybeYield } from '@nxtedition/yield'
import fs from 'node:fs'

function processFiles(files, i = 0) {
  for (; i < files.length; i++) {
    const data = fs.readFileSync(files[i])
    transform(data)
    if (maybeYield(processFiles, files, i + 1)) {
      return // will resume via callback after yielding
    }
  }
}
```

### Unconditional yield

```js
import { doYield } from '@nxtedition/yield'

// Promise-based
await doYield()

// Callback-based
doYield((opaque) => {
  continueWork(opaque)
}, data)
```

### Checking without yielding

```js
import { shouldYield, doYield } from '@nxtedition/yield'

while (hasWork()) {
  doWork()
  if (shouldYield()) {
    doMoreWork()
    break
  }
}
```

## API

### `shouldYield(timeout?): boolean`

Returns `true` when the current synchronous execution has exceeded the timeout threshold (default: 40ms). Does not yield — only checks whether yielding is recommended.

### `maybeYield(timeout?): Promise<void> | null`

If a yield is needed, queues a yield and returns a `Promise`. Otherwise returns `null`. Use this in `async` functions.

### `maybeYield(callback, opaque?, timeout?): void`

If a yield is needed, defers `callback(opaque)` to after the yield. Otherwise calls `callback(opaque)` synchronously.

### `doYield(): Promise<void>`

Unconditionally queues a yield and returns a `Promise` that resolves after the event loop has been given a turn.

### `doYield(callback, opaque?): void`

Unconditionally defers `callback(opaque)` to after the next yield.

### `setYieldTimeout(timeout): void`

Set the default yield timeout in milliseconds. Must be a non-negative number. Default: `40`.

### `setYieldDispatcher(dispatcher): void`

Override the scheduling function used to defer work. The dispatcher receives a callback and should invoke it asynchronously (e.g. via `setTimeout` or `setImmediate`). Pass `null` to restore the default dispatcher.

## How it works

The library tracks when the last yield occurred using `performance.now()`. When `shouldYield()` detects that more than `timeout` milliseconds have elapsed, it signals that a yield is due. `doYield` and `maybeYield` batch pending callbacks into a queue that is drained in a single microtask turn, yielding again if the timeout is exceeded during draining.

## License

MIT