2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-08-23

サンプル

playground

See the Pen interval example. by juner clarinet (@juner) on CodePen.

ソース
<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>

タスク系関数

setInterval

指定した コールバックを 指定 ミリ秒毎にキュー登録する。 clearInterval でキャンセルが可能。

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

/**
 * 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);
    }
  }
}

使うときはこんな感じ

// #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

指定コールバックを 指定ミリ秒後にキューに登録する。 clearTimeout でキャンセルが可能。

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

/**
 * 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);
    }
}

使うときはこんな感じ

// #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

指定コールバックを 次の再描画アニメーションを更新する際に 呼び出す。 cancelAnimationFrame でキャンセルが可能

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

/**
 * 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);
    }
}

使うときはこんな感じ

// #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.timeout(1000);
    await waitAnimationFrame({signal}).catch(() => undefined);
}
// #endregion

window.requestIdleCallback

指定時間内に アイドル状態になったら もしくは 指定タイムアウト時間が経過したなら 呼び出す。 cancelIdleCallback でキャンセルが可能

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

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

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);
    }
}

使うときはこんな感じ

// #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

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

キャンセルは不可

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

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() で繋げればいいというのはそう。

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

{
    await Promise.resolve();
}

scheduler.postTask

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

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

優先度は requestAnimationFrame から requestIdleCallback の間で user-blocking user-visible background が 優先度高い順で定義されている。

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

使うときはこんな感じ

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

v

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?