javascript のタスク系処理を Promise / AsyncIterator を交えつつまとめてみた

# サンプル

## playground

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="rNEzQoq" data-pen-title="interval example." data-user="juner" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/juner/pen/rNEzQoq">
  interval example.</a> by juner clarinet (<a href="https://codepen.io/juner">@juner</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<details>
<summary>ソース</summary>

```html
<div id="ui">
  <div id="inner-ui">
    <label>count down start:<input type="number" value="100" min="1" id="countdownStartInput" /></label>
    <label>interval:<input type="number" value="1000" min="1" id="intervalInput" /></label>
    <label>mode:<select id="modeSelect"></select></label>
    <label>count downs:<select id="countDownSelect"></select></label>
    <label><input type="checkbox" checked id="autoStart" />auto start</label>
  </div>
  <button id="startButton">start</button>
  <button id="button"></button>
</div>
<ul id="target">
  <template id="template">
    <li><span class="counter"></span><button class="delete">🗑️</button></li>
  </template>
</ul>
<style>
:root {
  display: flex;
  flex-flow: column nowrap;
  height: calc(100% - 2px);
  --gap: 0.5ex;
  gap: var(--gap);
  padding: 0.1ex;
  & > body {
    display: contents;
    & > :where(#ui) {
      display: flex;
      flex-flow: row nowrap;
      gap: var(--gap);
      & > :where(#inner-ui) {
        display: flex;
        flex: 1 1 auto;
        flex-flow: row wrap;
        gap: var(--gap);
        height: min-content;
        & > :not(#button) {
          height: min-content;
        }
      }
    }
    :where(#button)::before {
      display: inline;
      content: "set count down";
    }
    & :where(#inner-ui):has(#autoStart:checked) {
      & ~ :where(#button)::before {
        content: "start count down";
      }
    }
    :where(#startButton.empty) {
      visibility: hidden;
    }
  }
  input:not([type="checkbox"], [type="radio"]) {
    width: 5em;
  }
  label {
    display: flex;
    min-height: 1em;
    flex-flow: row nowrap;
    select option[disabled] {
      pointer-events: none;
      display: none;
    }
  }
  ul {
    display: flex;
    flex-flow: row wrap;
    padding: 0;
    margin: 0;
    gap: var(--gap);
    align-items: flex-start;
    justify-content: flex-start;
    li {
      text-align: end;
      --base: 0;
      &.mode-setTimeout {
        --base: -0.5;
        --name: "setTimeout";
      }
      &.mode-setInterval {
        --base: -0.45;
        --name: "setInterval";
      }
      &.mode-requestAnimationFrame {
        --base: -0.4;
        --name: "requestAnimationFrame";
      }
      &.mode-queueMicrotask {
        --base: -0.35;
        --name: "queueMicrotask";
      }
      &.mode-requestIdleCallback {
        --base: -0.3;
        --name: "requestIdleCallback";
      }
      &.mode-Worker\:setInterval {
        --base: -0.25;
        --name: "Worker:setInterval";
      }
      &.mode-Worker\:setTimeout {
        --base: -0.2;
        --name: "Worker:setTimeout";
      }
      &.mode-Worker\:requestAnimationFrame {
        --base: -0.15;
        --name: "Worker:requestAnimationFrame";
      }
      &.mode-postTask\:user-visible {
        --base: 0.2;
        --name: "postTaask:user-visible";
      }
      &.mode-postTask\:user-blocking {
        --base: 0.3;
        --name: "postTask:user-blocking";
      }
      &.mode-postTask\:background {
        --base: 0.5;
        --name: "postTask:background";
      }
      &.counter-time {
        --border-color: black;
      }
      &.counter-number {
        --border-color: gray;
      }
      list-style-type: none;
      border: 1px solid var(--border-color);
      padding: 0.5ex;
      background-color: oklab(
        100% var(--base) calc(1 - (var(--value) / var(--max)))
      );
      &::before {
        display: block;
        content: var(--name);
        font-size: 0.9ex;
        background-color: var(--border-color);
        color: white;
        padding-inline: 0.5ex;
        position: relative;
        margin-block-start: -0.7ex;
        margin-inline-start: -0.7ex;
        width: min-content;
        white-space: nowrap;
      }
      .counter {
        margin-inline-end: 0.5ex;
      }
    }
  }
}
</style>
<script type="module">
// #region constant
const constants = Object.freeze({
  countStart: 100,
  start: 1000
});
// #endregion

// #region autoStart
let startWaiter = [];
// #endregion

// #region startButton
startButton.addEventListener("click", startButtonClick);
function startButtonClick() {
  for (const start of [...startWaiter]) start();
  startWaiter = [];
  refleshStartButtonStatus();
}
refleshStartButtonStatus();
function refleshStartButtonStatus() {
  const hasWaiter = startWaiter.length > 0;
  const list = startButton.classList;
  if (hasWaiter) list.remove("empty");
  else list.add("empty");
}
// #endregion

// #region button event
button.addEventListener("click", buttonClick);
function buttonClick() {
  const controller = new AbortController();
  const signal = controller.signal;
  const countStart = inputToNum(countdownStartInput, constants.countStart);
  const start = inputToNum(intervalInput, constants.start);
  const [mode, iterator] = modeToInterval(modeSelect);
  const [countDown, counter] = countDownToActor(countDownSelect);
  const enableAutoStart = autoStart.checked;
  const $template = template.content.cloneNode(true);
  const $li = $template.querySelector("li");
  const c = counter(countStart, start);
  const i = iterator.bind(null, start, { signal });
  $li.classList.add(`mode-${mode}`);
  $li.classList.add(`counter-${countDown}`);
  $li.querySelector(".counter").innerText = `${countStart}/${countStart}`;
  $li.querySelector(".delete").addEventListener("click", (e) => {
    e.preventDefault();
    if (!controller.signal.aborted) controller.abort();
    $li.remove();
  });
  $li.style.setProperty("--value", countStart);
  $li.style.setProperty("--max", countStart);
  target.insertAdjacentElement("afterbegin", $li);
  if (enableAutoStart) {
    startFunction();
    return;
  }
  startWaiter.push(startFunction);
  refleshStartButtonStatus();

  function startFunction() {
    {
      const {
        value: [i, cs],
        done
      } = c.next();
      $li.querySelector(".counter").innerText = `${i}/${cs}`;
    }
    (async ({ $li, iterator, counter, signal }) => {
      for await (const _ of iterator()) {
        if (signal.aborted) break;
        const { value: v, done } = counter.next();
        if (done) break;
        const [i, c] = v;
        $li.querySelector(".counter").innerText = `${i}/${c}`;
        $li.style.setProperty("--value", i);
      }
      $li.remove();
    })({ $li, iterator: i, counter: c, signal });
  }
}
// #endregion

/**
 * input element to number value.
 * @param {HTMLInputElement} elem
 * @param {number} init
 * @return {number}
 */
function inputToNum(elem, init) {
  const num = elem.valueAsNumber;
  if (!isNaN(num)) return num;
  return init;
}

const modeToInterval = (() => {
  /** interval mode */
  const intervals = new Map([
    ["setInterval", setInterval],
    [
      "setTimeout",
      (delay, option) => toIterater(setTimeout.bind(null, delay, option))
    ],
    [
      "requestAnimationFrame",
      (delay, option) =>
        toIterater(requestAnimationFrame.bind(null, delay, option))
    ],
    [
      "queueMicrotask",
      (delay, option) => toIterater(queueMicrotask.bind(null, delay, option))
    ],
    ["Worker:setInterval", workeredSetInterval],
    [
      "Worker:setTimeout",
      (delay, option) =>
        toIterater(workeredSetTimeout.bind(null, delay, option))
    ],
    [
      "Worker:requestAnimationFrame",
      (delay, option) =>
        toIterater(workeredRequestAnimationFrame.bind(null, delay, option))
    ]
  ]);
  if (typeof globalThis?.requestIdleCallback === "function") {
    intervals.set("requestIdleCallback", (delay, option) =>
      toIterater(requestIdleCallback.bind(null, delay, option))
    );
  }
  if (typeof globalThis?.scheduler?.postTask === "function") {
    intervals.set("postTask:user-blocking", (delay, option) =>
      toIterater(postTask.bind(null, "user-blocking", delay, option))
    );
    intervals.set("postTask:user-visible", (delay, option) =>
      toIterater(postTask.bind(null, "user-visible", delay, option))
    );
    intervals.set("postTask:background", (delay, option) =>
      toIterater(postTask.bind(null, "background", delay, option))
    );
  }
  for (const key of intervals.keys())
    modeSelect.insertAdjacentHTML(
      "beforeend",
      `<option value="${key}">${key}</option>`
    );
  return modeToInterval;

  /**
   * @param {HTMLSelectElement} modeElement
   * @return {[string, (start: number, option:{signal: AbortSignal} = {}) => AsyncIterator<void>]}
   */
  function modeToInterval(modeElement) {
    const value = modeElement.value;
    if (intervals.has(value)) return [value, intervals.get(value)];
    throw new Error(`not found intervals '${value}' type.`);
  }

  /**
   * shorthand scheduler.postTask(callback, {delay, priority}) to Promise<void>
   * @param {string} priority
   * @param {null|number} delay
   * @return {Promise<void>}
   */
  function postTask(priority, delay, { signal } = {}) {
    if (typeof globalThis?.scheduler?.postTask !== "function")
      throw new Error("not support. scheduler.postTask");
    return scheduler
      .postTask(() => undefined, {
        delay,
        priority,
        signal
      })
      .catch(() => undefined);
  }

  /**
   * setTimeout's Promise<void> version.
   * @param {number|null} delay = null - not work
   * @return {Promise<void>}
   */
  function setTimeout(delay = null, { signal } = {}) {
    const d = Promise.withResolvers();
    const clear = globalThis.setTimeout(d.resolve, delay);
    if (signal) {
      signal.addEventListener("abort", abort);
      d.promise.finally(() => signal.removeEventListener("abort", abort));
    }
    return d.promise;
    function abort() {
      clearTimeout(clear);
      d.resolve();
    }
  }
  /**
   * setTimeout's in Web Worker version.
   * @param {number|null} delay = null - not work
   * @return {Promise<void>}
   */
  function workeredSetTimeout(delay = null, { signal } = {}) {
    const d = Promise.withResolvers();
    if (signal) {
      signal.addEventListener("abort", abort);
      d.promise.finally(() => signal.removeEventListener("abort", abort));
    }
    const source = `
    globalThis.addEventListener('message', msg => 
      globalThis.setTimeout(() => globalThis.postMessage(null), msg.data)
    )`;
    const url = `data:text/javascript;base64,${btoa(source)}`;
    const worker = new Worker(url);
    worker.addEventListener("message", () => {
      d.resolve();
    });
    worker.postMessage(delay);
    return d.promise;
    function abort() {
      worker?.terminate();
      worker = null;
      d.resolve();
    }
  }

  /**
   * requestAnimationFrame's Promise<void> version.
   * @param {number|null} delay = null
   * @return {Promise<void>}
   */
  async function requestAnimationFrame(delay = null, { signal } = {}) {
    const raf = globalThis.requestAnimationFrame;
    if (signal?.aborted) return Promise.resolve();
    const d = Promise.withResolvers();
    const clear = raf(d.resolve);
    try {
      if (signal) {
        signal.addEventListener("abort", abort);
      }
      await d.promise;
    } finally {
      if (signal) {
        signal.removeEventListener("abort", abort);
      }
    }
    function abort() {
      cancelAnimationFrame(clear);
      d.resolve();
    }
  }

  /**
   * requestAnimationFrame's in Web Worker version.
   * @param {number|null} delay = null - not work
   * @return {Promise<void>}
   */
  function workeredRequestAnimationFrame(delay = null, { signal } = {}) {
    const d = Promise.withResolvers();
    if (signal) {
      signal.addEventListener("abort", abort);
      d.promise.finally(() => signal.removeEventListener("abort", abort));
    }
    const source = `
    globalThis.addEventListener('message', () => 
      globalThis.requestAnimationFrame(() => globalThis.postMessage(null))
    )`;
    const url = `data:text/javascript;base64,${btoa(source)}`;
    const worker = new Worker(url);
    worker.addEventListener("message", () => {
      d.resolve();
    });
    worker.postMessage(null);
    return d.promise;
    function abort() {
      worker?.terminate();
      worker = null;
      d.resolve();
    }
  }

  /**
   * requestIdleCallback's Promise<void> version.
   * @param {number|null} delay = null
   * @return {Promise<void>}
   */
  async function requestIdleCallback(delay = null, { signal } = {}) {
    let timeout = delay;
    const ric = globalThis.requestIdleCallback;
    if (signal?.aborted) return Promise.resolve();
    const d = Promise.withResolvers();
    const clear = ric(d.resolve, { timeout });
    try {
      if (signal) {
        signal.addEventListener("abort", abort);
      }
      await d.promise;
    } finally {
      if (signal) {
        signal.removeEventListener("abort", abort);
      }
    }
    function abort() {
      d.resolve();
      cancelIdleCallback(clear);
    }
  }

  /**
   * queueMicrotask's Promise<void> version.
   * @param {number|null} delay = null - not work
   * @return {Promise<void>}
   */
  async function queueMicrotask(delay = null, { signal } = {}) {
    const raf = globalThis.queueMicrotask;
    const d = Promise.withResolvers();
    raf(d.resolve);
    try {
      if (signal) {
        signal.addEventListener("abort", abort);
      }
      await d.promise;
    } finally {
      if (signal) {
        signal.removeEventListener("abort", abort);
      }
    }
    if (signal?.aborted) {
      return;
    }
    function abort() {
      d.resolve();
    }
  }

  /**
   * setInterval's AsyncIterator<void> version.
   * @param delay = null
   * @return {AsyncIterator<void>}
   */
  async function* setInterval(delay = null, { signal } = {}) {
    const intervals = [Promise.withResolvers()];
    const clear = globalThis.setInterval(() => {
      let last = intervals[intervals.length - 1];
      last.resolve();
      intervals.push(Promise.withResolvers());
    }, delay);
    if (signal) signal.addEventListener("abort", abort);
    try {
      do {
        const first = intervals[0];
        yield await first.promise;
        intervals.shift();
      } while (intervals.length);
    } finally {
      abort();
      if (signal) signal.removeEventListener("abort", abort);
    }
    function abort() {
      clearInterval(clear);
      if (intervals.length) {
        do {
          const v = intervals.pop();
          if (v) v.resolve();
        } while (intervals.length);
      }
    }
  }

  /**
   * setInterval's in Web Worker version.
   * @param delay = null
   * @return {AsyncIterator<void>}
   */
  async function* workeredSetInterval(delay = null, { signal } = {}) {
    const intervals = [Promise.withResolvers()];
    if (signal) signal.addEventListener("abort", abort);
    const source = `
    globalThis.addEventListener('message', msg => 
      globalThis.setInterval(() => globalThis.postMessage(null), msg.data)
    )`;
    const url = `data:text/javascript;base64,${btoa(source)}`;
    let worker = null;
    try {
      worker = new Worker(url);
      worker.addEventListener("message", () => {
        let last = intervals[intervals.length - 1];
        last.resolve();
        intervals.push(Promise.withResolvers());
      });
      worker.postMessage(delay);
      do {
        const first = intervals[0];
        yield await first.promise;
        intervals.shift();
      } while (intervals.length);
    } finally {
      abort();
    }
    function abort() {
      worker?.terminate();
      worker = null;
      if (intervals.length) {
        do {
          const v = intervals.pop();
          if (v) v.resolve();
        } while (intervals.length);
      }
    }
  }

  /**
   * return Promise<T> method to AsyncIterator<T>
   * @return {AsyncIterator<T>}
   */
  async function* toIterater(callback) {
    while (true) {
      yield await callback();
    }
  }
})();
const countDownToActor = (() => {
  /** count down pattern */
  const countDowns = new Map([
    [
      "time",
      function* (countStart, milliseconds) {
        const startTime = new Date().getTime();
        let currentTime = startTime;
        let count = toCount();
        do {
          currentTime = new Date().getTime();
          count = toCount();
          yield [count, countStart];
        } while (count > 0);
        function toCount() {
          return Math.max(
            0,
            Math.ceil(
              (countStart * milliseconds - (currentTime - startTime)) /
                milliseconds
            )
          );
        }
      }
    ],
    [
      "number",
      function* (startCount, millisecondss) {
        let count = startCount;
        while (count >= 0) {
          yield [count--, startCount];
        }
      }
    ]
  ]);
  for (const key of countDowns.keys())
    countDownSelect.insertAdjacentHTML(
      "beforeend",
      `<option value="${key}">${key}</option>`
    );
  return countDownToActor;

  /**
   * get counter
   * @param {HTMLSelectElement} countDownElement
   * @return {[string,(startCount:number, millisecondss:number) => Iterator<[number, number]>]}
   */
  function countDownToActor(countDownElement) {
    const value = countDownElement.value;
    if (countDowns.has(value)) return [value, countDowns.get(value)];
    throw new Error(`not found countDowns '${value}' type.`);
  }
})();
</script>
```

</details>

# タスク系関数

## setInterval

https://developer.mozilla.org/ja/docs/Web/API/setInterval

指定した コールバックを 指定 ミリ秒毎にキュー登録する。 [clearInterval](https://developer.mozilla.org/ja/docs/Web/API/clearInterval) でキャンセルが可能。

async iterator に対応させるなら次のような感じ

```js
/**
 * setInterval's return AsyncIterator<void> version.
 * 
 * @return {AsyncIterator<void>}
 */
async function* startInterval({ signal, delay, args } = {}) {
  if (signal) {
    signal.throwIfAborted();
    signal.addEventListener("abort", abort);
  }
  const intervals = [Promise.withResolvers()];
  const clear = globalThis.setInterval((...args) => {
    let last = intervals[intervals.length - 1];
    last.resolve(args);
    intervals.push(Promise.withResolvers());
  }, delay, ...(args ?? []));
  try {
    do {
      const first = intervals[0];
      yield await first.promise;
      intervals.shift();
    } while (intervals.length);
  } finally {
    abort();
    if (signal) signal.removeEventListener("abort", abort);
  }
  function abort() {
    globalThis.clearInterval(clear);
    if (intervals.length) {
      do {
        const v = intervals.pop();
        if (signal.reason) v.reject(signal.reason)
        else v.resolve();
      } while (intervals.length);
    }
  }
}
```

使うときはこんな感じ

```js
// #region キャンセルされる前提の場合
{
    const signal = AbortSignal.timeout(10000);
    try {
        for await (const [v] of startInterval({delay:1000, signal, args:['called']}))
            console.log(v);
    } catch (e) {
        if (signal.reason !== e) throw e;
    }
}
// #endregion
// #region break される場合
{
    let i = 0;
    for await (const _ of startInterval({delay:1000})) {
        console.log('called: %o', i);
        if (i++ > 3) break;
    }
}
// #endregion
```

## setTimeout

https://developer.mozilla.org/ja/docs/Web/API/setTimeout

指定コールバックを 指定ミリ秒後にキューに登録する。 [clearTimeout](https://developer.mozilla.org/ja/docs/Web/API/clearTimeout) でキャンセルが可能。

戻り値 `Promise<void>` 対応するならこんな感じ

```js
/**
 * setTimeout's return Promise<void> version.
 */
function startTimeout({signal, delay, args} = {})
{
    if (signal) {
        if (signal.reason) return Promise.reject(signal.reason);
        signal.addEventListener("abort", abort);
    }
    const d = Promise.withResolvers();
    if (signal) {
        d.promise.finally(() => signal.removeEventListener("abort", abort));
    }
    const clear = globalThis.setTimeout((...args) => d.resolve(args), delay, ...(args ?? []));
    return d.promise;
    function abort() {
        globalThis.clearInterval(clear);
        d.reject(signal.reason);
    }
}
```

使うときはこんな感じ

```js
// #region 通常利用
{
    await startTimeout({delay:1000});
}
// #endregion
// #region 正常完了 / キャンセルで値を変える
{
    const signal = AbortSignal.timeout(1000);
    const result = await startTimeout({delay: 10000, signal})
        .then(() => 'ok')
        .catch(() => 'abort');
    console.log({result});
    // -> abort
}
// #endregion
// #region キャンセルを無視して続行する
{
    const signal = AbortSignal.timeout(1000);
    await startTimeout({delay:10000, signal}).catch(() => undefined);
}
// #endregion
```

## window.requestAnimationFrame

https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame

指定コールバックを 次の再描画アニメーションを更新する際に 呼び出す。 [cancelAnimationFrame](https://developer.mozilla.org/ja/docs/Web/API/Window/cancelAnimationFrame) でキャンセルが可能

戻り値 `Promise<void>` 対応するならこんな感じ


```js
/**
 * requestAnimationFrame's return Promise<void> version.
 */
function waitAnimationFrame({signal} = {})
{
    if (signal) {
        if (signal.reason) return Promise.reject(signal.reason);
        signal.addEventListener("abort", abort);
    }
    const d = Promise.withResolvers();
    if (signal) {
        d.promise.finally(() => signal.removeEventListener("abort", abort));
    }
    const clear = globalThis.requestAnimationFrame(d.resolve);
    return d.promise;
    function abort() {
        globalThis.cancelAnimationFrame(clear);
        d.reject(signal.reason);
    }
}
```

使うときはこんな感じ

```js
// #region 通常利用
{
    await waitAnimationFrame();
}
// #endregion
// #region 正常完了 / キャンセルで値を変える
{
    const signal = AbortSignal.timeout(1000);
    const result = await waitAnimationFrame({signal})
        .then(() => 'ok')
        .catch(() => 'abort');
    console.log({result});
    // -> ok
}
// #endregion
// #region キャンセルを無視して続行する
{
    const signal = AbortSignal.abort();
    await waitAnimationFrame({signal}).catch(() => undefined);
}
// #endregion
```

## window.requestIdleCallback

https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback

指定時間内に アイドル状態になったら もしくは 指定タイムアウト時間が経過したなら 呼び出す。 [cancelIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback) でキャンセルが可能

2024/08/22 時点では Safari 以外は実装されており、 Safari の TP で実装されている との caniuse 情報がある

https://caniuse.com/requestidlecallback

https://webkit.org/blog/14435/release-notes-for-safari-technology-preview-178/

戻り値 `Promise<IdleDeadline>` 対応させるのであれば次の様な感じ

```js
function waitIdle({signal, ...options} = {})
{
    if (signal) {
        if (signal.reason) return Promise.reject(signal.reason);
        signal.addEventListener("abort", abort);
    }
    const d = Promise.withResolvers();
    if (signal) {
        d.promise.finally(() => signal.removeEventListener("abort", abort));
    }
    const clear = globalThis.requestIdleCallback(d.resolve, options);
    return d.promise;
    function abort() {
        globalThis.cancelIdleCallback(clear);
        d.reject(signal.reason);
    }
}
```

使うときはこんな感じ

```js
// #region 通常利用 (アイドルになる OR 1秒経過で完了する)
{
    const deadline = await waitIdle({timeout: 1000});

    // timeout かどうか
    console.log(deadline.didTimeout);

    // 残りの timeout までの時間 (didTimeout が true なら 0固定)
    console.log(deadline.timeRemaining());
}
// #endregion
// #region AbortSignal 経由で timeout
{
    const signal = AbortSignal.timeout(10000);
    const deadline = await waitIdle().catch(() => ({didTimeout:true}));

    // timeout かどうか
    console.log(deadline.didTimeout);

    // 残りの timeout までの時間 (didTimeout が true なら 0固定)
    console.log(deadline?.timeRemaining() ?? 0);
}
// #endregion
```

## queueMicrotask

https://developer.mozilla.org/ja/docs/Web/API/queueMicrotask

指定したコールバックを マイクロタスクキューとして登録する（これは連続して登録しても UIキューは実行できない為、注意が必要。間を開けたくないコールバック登録の際に利用する

キャンセルは不可

戻り値を `Promise<void>` にして書くなら次の様になる。

```js
function queueMicrotask({signal} = {})
{
    if (signal) {
        if (signal.reason) return Promise.reject(signal.reason);
        signal.addEventListener("abort", abort);
    }
    const d = Promise.withResolvers();
    if (signal) {
        d.promise.finally(() => signal.removeEventListener("abort", abort));
    }
    const clear = globalThis.queueMicrotask(d.resolve);
    return d.promise;
    function abort() {
        d.reject(signal.reason);
    }
}
```

ただ、queueMicrotask() は `promise.then()` の登録の際の内部実装なので `.then()` で繋げればいいというのはそう。

つまりこれと変わらないとも言える。

```js
{
    await Promise.resolve();
}
```

最小単位とするなら

```js
{
    await null;
}
```

と同じともいえる。

## scheduler.postTask

https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask

指定した 優先度 で 指定された 遅延後 コールバックを実行する 戻り値は `Promise<T>` (コールバックの戻り値に影響される為)

firefox と safari 以外実装されている。 firefox は実装途中（フラグをつけることで有効化される）、safari はまだである。

https://caniuse.com/mdn-api_scheduler_posttask

優先度は [requestAnimationFrame](https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame) から [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) の間で `user-blocking` `user-visible` `background` が 優先度高い順で定義されている。

https://github.com/WICG/scheduling-apis/blob/main/explainers/prioritized-post-task.md

この関数は `Promise<T>` の戻り値fを持つ為、 そのまま使います。

使うときはこんな感じ

```js
// #region 通常利用 (優先度を user-visible で 1秒後 に完了)
{
    await scheduler.postTask(() => undefined, {delay:1000});
}
// #region
// #region 通常利用 (優先度を background で 2秒後 に完了)
{
    await scheduler.postTask(() => undefined, {
      priority: 'background', 
      delay: 2000}
    );
}
// #region
```

以上。