CLOVER🍀

That was when it all began.

Node.jsでPromise版setTimeout/setInterval(Timers Promises API)ってスリープや一定時間での繰り返し呼び出しを行う

これは、なにをしたくて書いたもの?

Node.js v15からPromise版のsetTimeoutsetIntervalが導入されているのですが、どこのモジュールなのかよく忘れるのでメモ。

Timers Promises API

Promise版のsetTimeoutsetInterval(とsetImmediate)は、timersモジュールに含まれています。

Timers | Node.js v18.15.0 Documentation

こちらですね。

Timers / Timers Promises API

Node.js v15で導入され、v16で実験的モジュールの位置づけを脱した、と書かれています。

これらの関数では、これまでのsetTimeoutsetIntervalsetImmediateとは異なり、Promiseを返すようになっています。

軽く試しておきましょう。

環境

今回の環境は、こちら。

$ node --version
v18.15.0


$ npm --version
9.5.0

準備

確認は、TypeScriptで行うことにします。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v18
$ npm i -D prettier
$ mkdir src

依存関係。

  "devDependencies": {
    "@types/node": "^18.15.0",
    "prettier": "^2.8.4",
    "typescript": "^4.9.5"
  }

scripts

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "format": "prettier --write src"
  },

設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

setTimeout

まずはsetTimeoutから。

src/set-timeout.ts

import { setTimeout } from 'node:timers/promises';

async function setTimeoutTest(): Promise<void> {
  console.log(`[${new Date().toISOString()}] start`);

  await setTimeout(2 * 1000);

  console.log(`[${new Date().toISOString()}] end`);
}

setTimeoutTest();

こんな感じで、setTimeout関数の戻り値がPromiseになっているのでasyncawaitと組み合わせて使えます。

  await setTimeout(2 * 1000);

確認。

$ npm run build
$ node dist/set-timeout.js
[2023-03-12T14:51:32.292Z] start
[2023-03-12T14:51:34.296Z] end

指定時間、停止しているのが確認できます。

src/set-timeout-with-return-value.ts

import { setTimeout } from 'node:timers/promises';

async function setTimeoutWithReturnTest(): Promise<void> {
  console.log(`[${new Date().toISOString()}] start`);

  const value = await setTimeout(2 * 1000, 'return value');

  console.log(`[${new Date().toISOString()}] end, value = ${value}`);
}

setTimeoutWithReturnTest();

また、第2引数を指定すると、戻り値のPromiseに含める値を指定できます。

  const value = await setTimeout(2 * 1000, 'return value');

確認。

$ npm run build
$ node dist/set-timeout-with-return-value.js
[2023-03-12T14:53:46.062Z] start
[2023-03-12T14:53:48.068Z] end, value = return value

setInterval

続いて、setInterval

src/set-interval.ts

import {setInterval } from 'node:timers/promises';

async function setIntervalTest(): Promise<void> {
  for await (const _ of setInterval(1 * 1000)) {
    console.log(`[${new Date().toISOString()}] print...`)
  }
}

setIntervalTest();

1秒おきに繰り返すようにしています。

  for await (const _ of setInterval(1 * 1000)) {
    console.log(`[${new Date().toISOString()}] print...`)
  }

確認。

$ npm run build
$ node dist/set-interval.js
[2023-03-12T15:11:53.096Z] print...
[2023-03-12T15:11:54.097Z] print...
[2023-03-12T15:11:55.099Z] print...
[2023-03-12T15:11:56.099Z] print...
[2023-03-12T15:11:57.101Z] print...
[2023-03-12T15:11:58.102Z] print...
[2023-03-12T15:11:59.103Z] print...
[2023-03-12T15:12:00.105Z] print...
[2023-03-12T15:12:01.105Z] print...
[2023-03-12T15:12:02.106Z] print...
[2023-03-12T15:12:03.107Z] print...

setIntervalPromiseの戻り値を指定することができます。各繰り返しで返される値になります。

src/set-interval-with-return-value.ts

import {setInterval } from 'node:timers/promises';

async function setIntervalTest(): Promise<void> {
  for await (const startTime of setInterval(1 * 1000, Date.now())) {
    const now = Date.now();

    console.log(`[${new Date().toISOString()}] startTime = ${new Date(startTime).toISOString()}`)

    if ((now - startTime) > 10000) {
      console.log(`[${new Date().toISOString()}] break`)
      break;
    }
  }
}

setIntervalTest();

10秒経過したら打ち切るように作成。

  for await (const startTime of setInterval(1 * 1000, Date.now())) {
    const now = Date.now();

    console.log(`[${new Date().toISOString()}] startTime = ${new Date(startTime).toISOString()}`)

    if ((now - startTime) > 10000) {
      console.log(`[${new Date().toISOString()}] break`)
      break;
    }
  }

確認。

$ npm run build
$ node dist/set-interval-with-return-value.js
[2023-03-12T15:14:10.059Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:11.060Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:12.061Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:13.062Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:14.063Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:15.065Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:16.065Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:17.066Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:18.067Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:19.068Z] startTime = 2023-03-12T15:14:09.057Z
[2023-03-12T15:14:19.069Z] break

その他

今回、setTimeoutsetIntervalの第2引数までを使いましたが、第3引数にオプションを指定できます。

  • ref … 残っている処理がタイマーのみとなっている場合、Node.jsのイベントループを終了する場合はfalse。デフォルトはtrue
  • signal … タイマー処理をキャンセルするためのAbortSignalを指定

キャンセルについては、こちらに記載があります。

Timers / Cancelling timers

AbortControllerと一緒に使うようです。

こちらについては、この記載のみで。

まとめ

Promise版のsetTimeoutsetIntervalを試してみました。

よくどこのモジュールだったか忘れるので…メモとして残しておきます。