Sequential user feedback from next.js api call

huangapple go评论56阅读模式
英文:

Sequential user feedback from next.js api call

问题

I have a long running process (~30sec) that calls several external apis. That process should run on the server in my next.js app.

我有一个长时间运行的过程(约30秒),它调用了几个外部API。这个过程应该在我的Next.js应用程序中在服务器上运行。

I created an /api route and things work, I however want to give the user feedback on the process that is composed of several subtasks. E.g. the user should see:

我创建了一个/api路由,一切都运行正常,但我想要为由多个子任务组成的过程向用户提供反馈。例如,用户应该看到:

  • processing task 1

  • processing task 2

  • ...

  • finished (with data callback)

  • 处理任务1

  • 处理任务2

  • ...

  • 完成(带有数据回调)

I tried several ways to do it:

我尝试了几种方法来实现它:

  1. Streams:

  2. 流:

api/hello.js

export default async function handler(req, res) {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.write(JSON.stringify({ data: "Step1" }));
  res.write(JSON.stringify({ data: "Step2" }));
  res.write(JSON.stringify({ data: "Step3" }));
  res.end();
}

client

      const response = await fetch("/api/hello", {
        method: "POST",
        body: JSON.stringify(config),
        keepalive: true,
        mode: "cors",
      });

      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        console.log('Received:', decoder.decode(value));
      }

      console.log("Response fully received");

The above code does output

上面的代码会输出

received {"data":"Step1"}{"data":"Step2"}{"data":"Step3"}
"Response fully received"

我预期的是每个块会依次记录,而不是一次性记录?

  1. The other way was is putting all subprocesses it all in separate API routes and calling all sequentially with the result data from the one before. But that seems complicated.

  2. 另一种方法是将所有子进程都放在单独的API路由中,然后按顺序调用它们,使用前一个子进程的结果数据。但这似乎很复杂。

How can I fix 1) or is 2) suited or is there an option 3) I am not seeing?

我应该如何修复1)或者2)合适,还是有第3个选项我没有看到的?

英文:

I have a long running process (~30sec) that calls several external apis. That process should run on the server in my next.js app.

I created an /api route and things work, I however want to give the user feedback on the process that is composed of several subtasks. E.g. the user should see:

  • processing task 1
  • processing task 2
  • ...
  • finished (with data callback)

I tried several ways to do it:

  1. Streams:

api/hello.js

export default async function handler(req, res) {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.write(JSON.stringify({ data: "Step1" }));
  res.write(JSON.stringify({ data: "Step2" }));
  res.write(JSON.stringify({ data: "Step3" }));
  res.end();
}

client

      const response = await fetch("/api/hello", {
        method: "POST",
        body: JSON.stringify(config),
        keepalive: true,
        mode: "cors",
      });

      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        console.log('Received:', decoder.decode(value));
      }

      console.log("Response fully received");

The above code does output

received {"data":"Step1"}{"data":"Step2"}{"data":"Step3"}
"Response fully received"

I was expecting that chunks are logged after each other and not at once?

  1. The other way was is putting all subprocesses it all in separate API routes and calling all sequentially with the result data from the one before. But that seems complicated.

How can I fix 1) or is 2) suited or is there an option 3) I am not seeing?

答案1

得分: 1

我认为要使用服务器发送事件,您需要使用 text/event-stream MIME 类型。我只是在本地快速进行了复制,它完全正常工作。我无法创建一个 Codesandbox 复制,因为在更新后对我来说变得非常卡顿,但如果它开始工作,我会在这里添加一个链接。

基本上,我只使用了这两个文件:

pages/api/endpoint.ts:

import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.writeHead(200, {
    Connection: "keep-alive",
    // 我在某处看到它需要在 Vercel 上设置为 'none' 才能在生产环境中工作
    "Content-Encoding": "none",
    "Cache-Control": "no-cache",
    "Content-Type": "text/event-stream",
  });

  let count = 0;
  res.write(`${JSON.stringify({ step: 0 })}`);

  const intervalId = setInterval(() => {
    count++;
    res.write(`${JSON.stringify({ step: count })}`);

    if (count === 100) {
      clearInterval(intervalId);
      res.end();
    }
  }, 5000);

  res.on("close", () => {
    clearInterval(intervalId);
    res.end();
  });
}

这是 Next.js 13 的客户端组件:

"use client";

import { useEffect, useState } from "react";

export function ClientComponent() {
  const [state, setState] = useState({ step: 0 });

  useEffect(() => {
    const callApi = async () => {
      const data = await fetch("/api/endpoint", { method: "post" });

      const stream = data.body;

      if (!stream) {
        return;
      }

      for await (const message of streamToJSON(stream)) {
        setState(JSON.parse(message));
      }
    };

    callApi();
  }, []);

  return (
    <main>
      <div>当前步骤是:{state.step}</div>
    </main>
  );
}

async function* streamToJSON(
  data: ReadableStream<Uint8Array>
): AsyncIterableIterator<string> {
  const reader = data.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { value, done } = await reader.read();
    if (done) {
      break;
    }

    if (value) {
      try {
        yield decoder.decode(value);
      } catch (error) {
        console.error(error);
      }
    }
  }
}
英文:

I think to use server-sent events you need to use text/event-stream MIME type instead. I just made a quick reproduction locally and it works just fine. I could not make a Codesandbox repro because it's extremely laggy for me after an update, but I will add a link here if it starts to work.

Basically I just used these 2 files:

pages/api/endpoint.ts:

import type { NextApiRequest, NextApiResponse } from &quot;next&quot;;

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.writeHead(200, {
    Connection: &quot;keep-alive&quot;,
    // I saw somewhere that it needs to be set to &#39;none&#39; to work in production on Vercel
    &quot;Content-Encoding&quot;: &quot;none&quot;,
    &quot;Cache-Control&quot;: &quot;no-cache&quot;,
    &quot;Content-Type&quot;: &quot;text/event-stream&quot;,
  });

  let count = 0;
  res.write(`${JSON.stringify({ step: 0 })}`);

  const intervalId = setInterval(() =&gt; {
    count++;
    res.write(`${JSON.stringify({ step: count })}`);

    if (count === 100) {
      clearInterval(intervalId);
      res.end();
    }
  }, 5000);

  res.on(&quot;close&quot;, () =&gt; {
    clearInterval(intervalId);
    res.end();
  });
}

And this is a client component for Next.js 13:

&quot;use client&quot;;

import { useEffect, useState } from &quot;react&quot;;

export function ClientComponent() {
  const [state, setState] = useState({ step: 0 });

  useEffect(() =&gt; {
    const callApi = async () =&gt; {
      const data = await fetch(&quot;/api/endpoint&quot;, { method: &quot;post&quot; });

      const stream = data.body;

      if (!stream) {
        return;
      }

      for await (const message of streamToJSON(stream)) {
        setState(JSON.parse(message));
      }
    };

    callApi();
  }, []);

  return (
    &lt;main&gt;
      &lt;div&gt;Current step is: {state.step}&lt;/div&gt;
    &lt;/main&gt;
  );
}

async function* streamToJSON(
  data: ReadableStream&lt;Uint8Array&gt;
): AsyncIterableIterator&lt;string&gt; {
  const reader = data.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { value, done } = await reader.read();
    if (done) {
      break;
    }

    if (value) {
      try {
        yield decoder.decode(value);
      } catch (error) {
        console.error(error);
      }
    }
  }
}

huangapple
  • 本文由 发表于 2023年7月5日 01:21:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76614753.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定