Repository: h3js/h3 (TypeScript)

Issue:
Support for `AsyncGenerator` event handlers, for streaming long running responses
### Describe the feature

Provide a new utility that enables developers to write streamed, chunked or partial responses using an async generator:

```typescript

async function delay<T>(ms, result:T) : Promise<T> {
  return new Promise(resolve => setTimeout(()=>resolve(result), ms));
}

const tasks = [
  () => delay(5_000, 1),
  () => delay(5_000, 2),
  () => delay(5_000, 3),
  () => delay(5_000, 4),
];

export default defineEventHandler(async (event) => {
  return sendYielding(async function*() {
    yield `<h1>Task results</h1><ol>`;
    for (let task of tasks) {
      const result = await task();
      yield `<li>${result}</li>`;
    }
    yield `</ol><p>Job done</p>`;
  });
});
```

When making this request you would immediately see the `<h1>Task results</h1>` appear in the response (also in the browser). Then every 5 seconds a new list item would appear as well. Finally, immediately after the 4th list item you also see "Job done".

# Added value

The main offering of this feature is a very intuitive way to let developers send results in pieces.
The code a developer writes using the proposed `sendYielding` is devoid of stream-API / readable-API logic and complications.
Instead the focus lies on the business logic, with a single function body that you can easily await results in and easily send out status updates in between.

*sendYielding* provides a streaming response function where the developer gives up low-level control of the streaming, and in receives an ergonomic api in return.

## What about `sendStream`?

The `sendStream` function could already be used to build out the same example logic shown.
But it would be in a very different form, because it would have to build a stream/readable.

For me, personally, trying to read the streaming apis of nodejs is like walking into a brick wall.
The details of stream modes and states are a high barrier to entry.

Not just in having to understand it, but to implement one as well. This mixes the business-logic with implementation details of streams.
It is good that `sendStream` exists, but it is too big a step when you just want to stream parts of a response without the need for low-level control.

In all likelihood, the `sendYielding` would be built on top of `sendStream`.

# Use cases

- Send updates as progress is made on a long-running task
- Transform a streamed response (I'm not thinking media, more like LLM completion streaming)

# Considerations / Open questions

1. Pick a better name, this one is a bit crude and not entirely representative of what it does.
2. If the `sendYielding` wraps `sendStream`, then expose the AsyncGenerator-to-Readable as a utility as well.
3. During development attention must be paid that the stream is in some sort of immediate pass-through mode, and does not collect the full response first.
4. It may need to be a restriction that headers and the like cannot be sent or set from the yielding body.
5. How should errors be handled before, during and after the generator's lifetime.



### Additional information

- [X] Would you be willing to help implement this feature?


Discussion: Seems a nice thing to support!
I'd like to help out with the PR for this. The main problem I forsee in helping to implement this is that I don't have as wide a range of experience with the ways that h3 (and nitro) are commonly used. Like, the pitfalls to avoid or defend against in the code.

Regardless, I'll get started on getting an initial PR worked out. 



Fix (patch):
diff --git a/docs/2.utils/2.reponse.md b/docs/2.utils/2.reponse.md
index a1b2acb9f..8342433b8 100644
--- a/docs/2.utils/2.reponse.md
+++ b/docs/2.utils/2.reponse.md
@@ -65,6 +65,35 @@ Remove a response header by name.
 Directly send a response to the client.
 **Note:** This function should be used only when you want to send a response directly without using the `h3` event. Normaly you can directly `return` a value inside event handlers.
 
+### `sendIterable(event, iterable)`
+
+Iterate a source of chunks and send back each chunk in order. Supports mixing async work toghether with emitting chunks.
+Each chunk must be a string or a buffer.
+For generator (yielding) functions, the returned value is treated the same as yielded values.
+
+**Example:**
+
+```ts
+sendIterable(event, work());
+async function* work() {
+  // Open document body
+  yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
+  // Do work ...
+  for (let i = 0; i < 1000) {
+    await delay(1000);
+    // Report progress
+    yield `<li>Completed job #`;
+    yield i;
+    yield `</li>\n`;
+  }
+  // Close out the report
+  return `</ol></body></html>`;
+}
+async function delay(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}
+```
+
 ### `sendNoContent(event, code?)`
 
 Respond with an empty payload.<br>
diff --git a/src/utils/internal/iteratable.ts b/src/utils/internal/iteratable.ts
new file mode 100644
index 000000000..68f9aa2be
--- /dev/null
+++ b/src/utils/internal/iteratable.ts
@@ -0,0 +1,64 @@
+export type IterationSource<Val, Ret = Val> =
+  | Iterable<Val>
+  | AsyncIterable<Val>
+  | Iterator<Val, Ret | undefined>
+  | AsyncIterator<Val, Ret | undefined>
+  | (() =>
+      | Iterator<Val, Ret | undefined>
+      | AsyncIterator<Val, Ret | undefined>);
+
+type SendableValue = string | Buffer | Uint8Array;
+export type IteratorSerializer<Value> = (
+  value: Value,
+) => SendableValue | undefined;
+
+/**
+ * The default implementation for {@link sendIterable}'s `serializer` argument.
+ * It serializes values as follows:
+ * - Instances of {@link String}, {@link Uint8Array} and `undefined` are returned as-is.
+ * - Objects are serialized through {@link JSON.stringify}.
+ * - Functions are serialized as `undefined`.
+ * - Values of type boolean, number, bigint or symbol are serialized using their `toString` function.
+ *
+ * @param value - The value to serialize to either a string or Uint8Array.
+ */
+export function serializeIterableValue(
+  value: unknown,
+): SendableValue | undefined {
+  switch (typeof value) {
+    case "string": {
+      return value;
+    }
+    case "boolean":
+    case "number":
+    case "bigint":
+    case "symbol": {
+      return value.toString();
+    }
+    case "function":
+    case "undefined": {
+      return undefined;
+    }
+    case "object": {
+      if (value instanceof Uint8Array) {
+        return value;
+      }
+      return JSON.stringify(value);
+    }
+  }
+}
+
+export function coerceIterable<V, R>(
+  iterable: IterationSource<V, R>,
+): Iterator<V> | AsyncIterator<V> {
+  if (typeof iterable === "function") {
+    iterable = iterable();
+  }
+  if (Symbol.iterator in iterable) {
+    return iterable[Symbol.iterator]();
+  }
+  if (Symbol.asyncIterator in iterable) {
+    return iterable[Symbol.asyncIterator]();
+  }
+  return iterable;
+}
diff --git a/src/utils/response.ts b/src/utils/response.ts
index 48c27fe53..8ee1fa193 100644
--- a/src/utils/response.ts
+++ b/src/utils/response.ts
@@ -7,6 +7,12 @@ import { MIMES } from "./consts";
 import { sanitizeStatusCode, sanitizeStatusMessage } from "./sanitize";
 import { splitCookiesString } from "./cookie";
 import { hasProp } from "./internal/object";
+import {
+  serializeIterableValue,
+  coerceIterable,
+  IterationSource,
+  IteratorSerializer,
+} from "./internal/iteratable";
 
 const defer =
   typeof setImmediate === "undefined" ? (fn: () => any) => fn() : setImmediate;
@@ -451,3 +457,67 @@ export function sendWebResponse(
   }
   return sendStream(event, response.body);
 }
+
+/**
+ * Iterate a source of chunks and send back each chunk in order.
+ * Supports mixing async work toghether with emitting chunks.
+ *
+ * Each chunk must be a string or a buffer.
+ *
+ * For generator (yielding) functions, the returned value is treated the same as yielded values.
+ *
+ * @param event - H3 event
+ * @param iterable - Iterator that produces chunks of the response.
+ * @param serializer - Function that converts values from the iterable into stream-compatible values.
+ * @template Value - Test
+ *
+ * @example
+ * sendIterable(event, work());
+ * async function* work() {
+ *   // Open document body
+ *   yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
+ *   // Do work ...
+ *   for (let i = 0; i < 1000) {
+ *     await delay(1000);
+ *     // Report progress
+ *     yield `<li>Completed job #`;
+ *     yield i;
+ *     yield `</li>\n`;
+ *   }
+ *   // Close out the report
+ *   return `</ol></body></html>`;
+ * }
+ * async function delay(ms) {
+ *   return new Promise(resolve => setTimeout(resolve, ms));
+ * }
+ */
+export function sendIterable<Value = unknown, Return = unknown>(
+  event: H3Event,
+  iterable: IterationSource<Value, Return>,
+  options?: {
+    serializer: IteratorSerializer<Value | Return>;
+  },
+): Promise<void> {
+  const serializer = options?.serializer ?? serializeIterableValue;
+  const iterator = coerceIterable(iterable);
+  return sendStream(
+    event,
+    new ReadableStream({
+      async pull(controller) {
+        const { value, done } = await iterator.next();
+        if (value !== undefined) {
+          const chunk = serializer(value);
+          if (chunk !== undefined) {
+            controller.enqueue(chunk);
+          }
+        }
+        if (done) {
+          controller.close();
+        }
+      },
+      cancel() {
+        iterator.return?.();
+      },
+    }),
+  );
+}