docs: document the fetchStream method of the JS SDK (#13125)

This commit is contained in:
Shahed Nasser
2025-08-01 17:47:26 +03:00
committed by GitHub
parent 6ec530b2a5
commit f9ca2691be
5 changed files with 323 additions and 7 deletions

View File

@@ -72,7 +72,9 @@ The response of this API route has the status code `201`.
To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type.
For example, to create an API route that returns an event stream:
### Example: Server-Sent Events (SSE)
For example, to create an API route that returns server-sent events (SSE), you can set the `Content-Type` header to `text/event-stream`:
export const streamHighlights = [
["7", "writeHead", "Set the response's headers."],
@@ -97,9 +99,14 @@ export const GET = async (
})
const interval = setInterval(() => {
res.write("Streaming data...\n")
res.write("data: Streaming data...\n\n")
}, 3000)
req.on("close", () => {
clearInterval(interval)
res.end()
})
req.on("end", () => {
clearInterval(interval)
res.end()
@@ -114,6 +121,12 @@ The `writeHead` method accepts two parameters:
This API route opens a stream by setting the `Content-Type` to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds.
### Tip: Fetching Stream with JS SDK
The JS SDK has a `fetchStream` method that you can use to fetch data from an API route that returns a stream.
Learn more in the [JS SDK](!resources!/js-sdk) documentation.
---
## Do More with Responses

View File

@@ -57,7 +57,7 @@ export const generatedEditDates = {
"app/learn/debugging-and-testing/testing-tools/unit-tests/module-example/page.mdx": "2024-09-02T11:04:27.232Z",
"app/learn/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z",
"app/learn/fundamentals/modules/service-constraints/page.mdx": "2025-03-18T15:12:46.006Z",
"app/learn/fundamentals/api-routes/responses/page.mdx": "2025-07-25T15:17:39.986Z",
"app/learn/fundamentals/api-routes/responses/page.mdx": "2025-08-01T14:15:34.787Z",
"app/learn/fundamentals/api-routes/validation/page.mdx": "2025-03-24T06:52:47.896Z",
"app/learn/fundamentals/api-routes/errors/page.mdx": "2025-06-19T16:09:08.563Z",
"app/learn/fundamentals/admin/constraints/page.mdx": "2025-07-21T08:20:43.223Z",

View File

@@ -8685,7 +8685,9 @@ The response of this API route has the status code `201`.
To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type.
For example, to create an API route that returns an event stream:
### Example: Server-Sent Events (SSE)
For example, to create an API route that returns server-sent events (SSE), you can set the `Content-Type` header to `text/event-stream`:
```ts highlights={streamHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
@@ -8701,9 +8703,14 @@ export const GET = async (
})
const interval = setInterval(() => {
res.write("Streaming data...\n")
res.write("data: Streaming data...\n\n")
}, 3000)
req.on("close", () => {
clearInterval(interval)
res.end()
})
req.on("end", () => {
clearInterval(interval)
res.end()
@@ -8718,6 +8725,12 @@ The `writeHead` method accepts two parameters:
This API route opens a stream by setting the `Content-Type` to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds.
### Tip: Fetching Stream with JS SDK
The JS SDK has a `fetchStream` method that you can use to fetch data from an API route that returns a stream.
Learn more in the [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) documentation.
***
## Do More with Responses
@@ -25026,7 +25039,7 @@ Users of any actor type (admin, customer, or custom actor type) can request to r
In this guide, you'll implement a subscriber that handles the `auth.password_reset` event to send an email notification to the user with instructions on how to reset their password.
After adding the subscriber, you will have a complete reset password flow you can utilize using the Medusa admin, storefront, or API routes.
After adding the subscriber, you will have a complete reset password flow you can utilize using the Medusa Admin, storefront, or API routes.
***
@@ -43706,6 +43719,151 @@ The method returns a Promise that, when resolved, has the data returned by the r
***
## Stream Server-Sent Events
The JS SDK supports streaming server-sent events (SSE) using the `client.fetchStream` method. This method is useful when you want to receive real-time updates from the server.
For example, consider you have the following custom API route at `src/api/admin/stream/route.ts`:
```ts title="src/api/admin/stream/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
})
const interval = setInterval(() => {
res.write("data: Streaming data...\n\n")
}, 3000)
req.on("close", () => {
clearInterval(interval)
res.end()
})
req.on("end", () => {
clearInterval(interval)
res.end()
})
}
```
Then, you can use the `client.fetchStream` method in a UI route to receive the streaming data:
```tsx title="src/admin/route/stream/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button, Text } from "@medusajs/ui"
import { useState } from "react"
import { sdk } from "../../lib/sdk"
const StreamTestPage = () => {
const [messages, setMessages] = useState<string[]>([])
const [isStreaming, setIsStreaming] = useState(false)
const [abortStream, setAbortStream] = useState<(() => void) | null>(null)
const startStream = async () => {
setIsStreaming(true)
setMessages([])
const { stream, abort } = await sdk.client.fetchStream("/admin/stream")
if (!stream) {
console.error("Failed to start stream")
setIsStreaming(false)
return
}
// Store the abort function for the abort button
setAbortStream(() => abort)
try {
for await (const chunk of stream) {
// Since the server sends plain text, convert to string
const message = typeof chunk === "string" ? chunk : (chunk.data || String(chunk))
setMessages((prev) => [...prev, message.trim()])
}
} catch (error) {
// Don't log abort errors as they're expected when user clicks abort
if (error instanceof Error && error.name !== "AbortError") {
console.error("Stream error:", error)
}
} finally {
setIsStreaming(false)
setAbortStream(null)
}
}
const handleAbort = () => {
if (abortStream) {
abortStream()
setIsStreaming(false)
setAbortStream(null)
}
}
return (
<Container className="p-6">
<Heading level="h1" className="mb-6">
fetchStream Example
</Heading>
<div className="space-y-4">
<div className="flex gap-2">
<Button
onClick={startStream}
disabled={isStreaming}
variant="primary"
>
{isStreaming ? "Streaming..." : "Start Stream"}
</Button>
<Button
onClick={handleAbort}
disabled={!isStreaming}
variant="secondary"
>
Abort Stream
</Button>
</div>
<div className="border rounded p-4 h-64 overflow-y-auto bg-ui-bg-subtle">
{messages.length === 0 ? (
<Text className="text-ui-fg-muted">No messages yet...</Text>
) : (
messages.map((msg, index) => (
<div key={index} className="mb-2 text-sm">
{msg}
</div>
))
)}
</div>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Stream Test",
})
export default StreamTestPage
```
`fetchStream` accepts the same parameters as `fetch`, but it returns an object having two properties:
- `stream`: An [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) that you can use to iterate over the streaming data.
- `abort`: A function that you can call to abort the stream. This is useful when you want to stop receiving data from the server.
In this example, when the user clicks the "Start Stream" button, you start the stream and listen for incoming data. The data is received as chunks, which you can process and display in the UI.
***
## Handle Errors
If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties:

View File

@@ -398,6 +398,151 @@ The method returns a Promise that, when resolved, has the data returned by the r
---
## Stream Server-Sent Events
The JS SDK supports streaming server-sent events (SSE) using the `client.fetchStream` method. This method is useful when you want to receive real-time updates from the server.
For example, consider you have the following custom API route at `src/api/admin/stream/route.ts`:
```ts title="src/api/admin/stream/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
})
const interval = setInterval(() => {
res.write("data: Streaming data...\n\n")
}, 3000)
req.on("close", () => {
clearInterval(interval)
res.end()
})
req.on("end", () => {
clearInterval(interval)
res.end()
})
}
```
Then, you can use the `client.fetchStream` method in a UI route to receive the streaming data:
```tsx title="src/admin/route/stream/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button, Text } from "@medusajs/ui"
import { useState } from "react"
import { sdk } from "../../lib/sdk"
const StreamTestPage = () => {
const [messages, setMessages] = useState<string[]>([])
const [isStreaming, setIsStreaming] = useState(false)
const [abortStream, setAbortStream] = useState<(() => void) | null>(null)
const startStream = async () => {
setIsStreaming(true)
setMessages([])
const { stream, abort } = await sdk.client.fetchStream("/admin/stream")
if (!stream) {
console.error("Failed to start stream")
setIsStreaming(false)
return
}
// Store the abort function for the abort button
setAbortStream(() => abort)
try {
for await (const chunk of stream) {
// Since the server sends plain text, convert to string
const message = typeof chunk === "string" ? chunk : (chunk.data || String(chunk))
setMessages((prev) => [...prev, message.trim()])
}
} catch (error) {
// Don't log abort errors as they're expected when user clicks abort
if (error instanceof Error && error.name !== "AbortError") {
console.error("Stream error:", error)
}
} finally {
setIsStreaming(false)
setAbortStream(null)
}
}
const handleAbort = () => {
if (abortStream) {
abortStream()
setIsStreaming(false)
setAbortStream(null)
}
}
return (
<Container className="p-6">
<Heading level="h1" className="mb-6">
fetchStream Example
</Heading>
<div className="space-y-4">
<div className="flex gap-2">
<Button
onClick={startStream}
disabled={isStreaming}
variant="primary"
>
{isStreaming ? "Streaming..." : "Start Stream"}
</Button>
<Button
onClick={handleAbort}
disabled={!isStreaming}
variant="secondary"
>
Abort Stream
</Button>
</div>
<div className="border rounded p-4 h-64 overflow-y-auto bg-ui-bg-subtle">
{messages.length === 0 ? (
<Text className="text-ui-fg-muted">No messages yet...</Text>
) : (
messages.map((msg, index) => (
<div key={index} className="mb-2 text-sm">
{msg}
</div>
))
)}
</div>
</div>
</Container>
)
}
export const config = defineRouteConfig({
label: "Stream Test",
})
export default StreamTestPage
```
`fetchStream` accepts the same parameters as `fetch`, but it returns an object having two properties:
- `stream`: An [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) that you can use to iterate over the streaming data.
- `abort`: A function that you can call to abort the stream. This is useful when you want to stop receiving data from the server.
In this example, when the user clicks the "Start Stream" button, you start the stream and listen for incoming data. The data is received as chunks, which you can process and display in the UI.
---
## Handle Errors
If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties:

View File

@@ -2168,7 +2168,7 @@ export const generatedEditDates = {
"app/commerce-modules/store/links-to-other-modules/page.mdx": "2025-04-17T16:03:16.419Z",
"app/examples/page.mdx": "2025-07-16T09:53:26.163Z",
"app/medusa-cli/commands/build/page.mdx": "2024-11-11T11:00:49.665Z",
"app/js-sdk/page.mdx": "2025-05-26T15:08:16.590Z",
"app/js-sdk/page.mdx": "2025-08-01T14:17:07.509Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.apiKey/page.mdx": "2025-05-20T07:51:40.924Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.campaign/page.mdx": "2025-05-20T07:51:40.925Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.claim/page.mdx": "2025-06-25T10:11:46.945Z",