-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
docs(solid-start): add start-streaming-data-from-server-functions example #5837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| "name": "tanstack-solid-start-streaming-data-from-server-functions", | ||||||||||||||||||||||||||
| "private": true, | ||||||||||||||||||||||||||
| "sideEffects": false, | ||||||||||||||||||||||||||
| "type": "module", | ||||||||||||||||||||||||||
| "scripts": { | ||||||||||||||||||||||||||
| "dev": "vite dev", | ||||||||||||||||||||||||||
| "build": "vite build && tsc --noEmit", | ||||||||||||||||||||||||||
| "start": "vite start" | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| "dependencies": { | ||||||||||||||||||||||||||
| "@tanstack/solid-router": "^1.135.2", | ||||||||||||||||||||||||||
| "@tanstack/solid-router-devtools": "^1.135.2", | ||||||||||||||||||||||||||
| "@tanstack/solid-start": "^1.135.2", | ||||||||||||||||||||||||||
| "solid-js": "^1.9.10", | ||||||||||||||||||||||||||
| "zod": "^3.24.2" | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
Comment on lines
+12
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Use workspace protocol for in-repo packages. The @TanStack packages are part of this monorepo, so the dependencies here should point to - "@tanstack/solid-router": "^1.135.2",
- "@tanstack/solid-router-devtools": "^1.135.2",
- "@tanstack/solid-start": "^1.135.2",
+ "@tanstack/solid-router": "workspace:*",
+ "@tanstack/solid-router-devtools": "workspace:*",
+ "@tanstack/solid-start": "workspace:*",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| "devDependencies": { | ||||||||||||||||||||||||||
| "@types/node": "^22.5.4", | ||||||||||||||||||||||||||
| "vite-plugin-solid": "^2.11.10", | ||||||||||||||||||||||||||
| "typescript": "^5.7.2", | ||||||||||||||||||||||||||
| "vite": "^7.1.7", | ||||||||||||||||||||||||||
| "vite-tsconfig-paths": "^5.1.4" | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /* eslint-disable */ | ||
|
|
||
| // @ts-nocheck | ||
|
|
||
| // noinspection JSUnusedGlobalSymbols | ||
|
|
||
| // This file was automatically generated by TanStack Router. | ||
| // You should NOT make any changes in this file as it will be overwritten. | ||
| // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. | ||
|
|
||
| import { Route as rootRouteImport } from './routes/__root' | ||
| import { Route as IndexRouteImport } from './routes/index' | ||
|
|
||
| const IndexRoute = IndexRouteImport.update({ | ||
| id: '/', | ||
| path: '/', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
|
|
||
| export interface FileRoutesByFullPath { | ||
| '/': typeof IndexRoute | ||
| } | ||
| export interface FileRoutesByTo { | ||
| '/': typeof IndexRoute | ||
| } | ||
| export interface FileRoutesById { | ||
| __root__: typeof rootRouteImport | ||
| '/': typeof IndexRoute | ||
| } | ||
| export interface FileRouteTypes { | ||
| fileRoutesByFullPath: FileRoutesByFullPath | ||
| fullPaths: '/' | ||
| fileRoutesByTo: FileRoutesByTo | ||
| to: '/' | ||
| id: '__root__' | '/' | ||
| fileRoutesById: FileRoutesById | ||
| } | ||
| export interface RootRouteChildren { | ||
| IndexRoute: typeof IndexRoute | ||
| } | ||
|
|
||
| declare module '@tanstack/solid-router' { | ||
| interface FileRoutesByPath { | ||
| '/': { | ||
| id: '/' | ||
| path: '/' | ||
| fullPath: '/' | ||
| preLoaderRoute: typeof IndexRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const rootRouteChildren: RootRouteChildren = { | ||
| IndexRoute: IndexRoute, | ||
| } | ||
| export const routeTree = rootRouteImport | ||
| ._addFileChildren(rootRouteChildren) | ||
| ._addFileTypes<FileRouteTypes>() | ||
|
|
||
| import type { getRouter } from './router.tsx' | ||
| import type { createStart } from '@tanstack/solid-start' | ||
| declare module '@tanstack/solid-start' { | ||
| interface Register { | ||
| ssr: true | ||
| router: Awaited<ReturnType<typeof getRouter>> | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { createRouter } from '@tanstack/solid-router' | ||
| import { routeTree } from './routeTree.gen' | ||
|
|
||
| export function getRouter() { | ||
| const router = createRouter({ | ||
| routeTree, | ||
| defaultPreload: 'intent', | ||
| defaultErrorComponent: (err) => <p>{err.error.stack}</p>, | ||
| defaultNotFoundComponent: () => <p>not found</p>, | ||
| scrollRestoration: true, | ||
| }) | ||
|
|
||
| return router | ||
| } | ||
|
|
||
| declare module '@tanstack/solid-router' { | ||
| interface Register { | ||
| router: ReturnType<typeof getRouter> | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||
| /// <reference types="vite/client" /> | ||||||||||||||||||||||||||||||
| import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' | ||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||
| HeadContent, | ||||||||||||||||||||||||||||||
| Outlet, | ||||||||||||||||||||||||||||||
| Scripts, | ||||||||||||||||||||||||||||||
| createRootRoute, | ||||||||||||||||||||||||||||||
| } from '@tanstack/solid-router' | ||||||||||||||||||||||||||||||
| import { HydrationScript } from 'solid-js/web' | ||||||||||||||||||||||||||||||
| import type { JSX } from 'solid-js' | ||||||||||||||||||||||||||||||
| import appCss from '~/styles/app.css?url' | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export const Route = createRootRoute({ | ||||||||||||||||||||||||||||||
| head: () => ({ | ||||||||||||||||||||||||||||||
| links: [{ rel: 'stylesheet', href: appCss }], | ||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||
| component: RootComponent, | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function RootComponent() { | ||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <RootDocument> | ||||||||||||||||||||||||||||||
| <Outlet /> | ||||||||||||||||||||||||||||||
| </RootDocument> | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function RootDocument({ children }: { children: JSX.Element }) { | ||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <html> | ||||||||||||||||||||||||||||||
| <head> | ||||||||||||||||||||||||||||||
| <HydrationScript /> | ||||||||||||||||||||||||||||||
| </head> | ||||||||||||||||||||||||||||||
| <body> | ||||||||||||||||||||||||||||||
| <HeadContent /> | ||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||
| <TanStackRouterDevtools position="bottom-right" /> | ||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move
- <html>
- <head>
- <HydrationScript />
- </head>
- <body>
- <HeadContent />
+ <html>
+ <head>
+ <HydrationScript />
+ <HeadContent />
+ </head>
+ <body>
{children}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| <Scripts /> | ||||||||||||||||||||||||||||||
| </body> | ||||||||||||||||||||||||||||||
| </html> | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| import { createFileRoute } from '@tanstack/solid-router' | ||
| import { createServerFn } from '@tanstack/solid-start' | ||
| import { createSignal } from 'solid-js' | ||
| import { z } from 'zod' | ||
|
|
||
| /** | ||
| This schema will be used to define the type | ||
| of each chunk in the `ReadableStream`. | ||
| (It mimics OpenAI's streaming response format.) | ||
| */ | ||
| const textPartSchema = z.object({ | ||
| choices: z.array( | ||
| z.object({ | ||
| delta: z.object({ | ||
| content: z.string().optional(), | ||
| }), | ||
| index: z.number(), | ||
| finish_reason: z.string().nullable(), | ||
| }), | ||
| ), | ||
| }) | ||
|
|
||
| export type TextPart = z.infer<typeof textPartSchema> | ||
|
|
||
| /** | ||
| This helper function generates the array of messages | ||
| that we'll stream to the client. | ||
| */ | ||
| function generateMessages() { | ||
| const messages = Array.from({ length: 10 }, () => | ||
| Math.floor(Math.random() * 100), | ||
| ).map((n, i) => | ||
| textPartSchema.parse({ | ||
| choices: [ | ||
| { | ||
| delta: { content: `Number #${i + 1}: ${n}\n` }, | ||
| index: i, | ||
| finish_reason: null, | ||
| }, | ||
| ], | ||
| }), | ||
| ) | ||
| return messages | ||
| } | ||
|
|
||
| /** | ||
| This helper function is used to simulate the | ||
| delay between each message being sent. | ||
| */ | ||
| function sleep(ms: number) { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)) | ||
| } | ||
|
|
||
| /** | ||
| This server function returns a `ReadableStream` | ||
| that streams `TextPart` chunks to the client. | ||
| */ | ||
| const streamingResponseFn = createServerFn().handler(async () => { | ||
| const messages = generateMessages() | ||
| // This `ReadableStream` is typed, so each | ||
| // will be of type `TextPart`. | ||
| const stream = new ReadableStream<TextPart>({ | ||
| async start(controller) { | ||
| for (const message of messages) { | ||
| // simulate network latency | ||
| await sleep(500) | ||
| controller.enqueue(message) | ||
| } | ||
| controller.close() | ||
| }, | ||
| }) | ||
|
|
||
| return stream | ||
| }) | ||
|
|
||
| /** | ||
| You can also use an async generator function to stream | ||
| typed chunks to the client. | ||
| */ | ||
| const streamingWithAnAsyncGeneratorFn = createServerFn().handler( | ||
| async function* () { | ||
| const messages = generateMessages() | ||
| for (const msg of messages) { | ||
| await sleep(500) | ||
| // The streamed chunks are still typed as `TextPart` | ||
| yield msg | ||
| } | ||
| }, | ||
| ) | ||
|
|
||
| export const Route = createFileRoute('/')({ | ||
| component: RouteComponent, | ||
| }) | ||
|
|
||
| function RouteComponent() { | ||
| const [readableStreamMessages, setReadableStreamMessages] = createSignal('') | ||
|
|
||
| const [asyncGeneratorFuncMessages, setAsyncGeneratorFuncMessages] = | ||
| createSignal('') | ||
|
|
||
| const getTypedReadableStreamResponse = async () => { | ||
| const response = await streamingResponseFn() | ||
|
|
||
| if (!response) { | ||
| return | ||
| } | ||
|
|
||
| const reader = response.getReader() | ||
| let done = false | ||
| setReadableStreamMessages('') | ||
| while (!done) { | ||
| const { value, done: doneReading } = await reader.read() | ||
| done = doneReading | ||
| if (value) { | ||
| // Notice how we know the value of `chunk` (`TextPart | undefined`) | ||
| // here, because it's coming from the typed `ReadableStream` | ||
| const chunk = value?.choices[0].delta.content | ||
| if (chunk) { | ||
| setReadableStreamMessages((prev) => prev + chunk) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const getResponseFromTheAsyncGenerator = async () => { | ||
| setAsyncGeneratorFuncMessages('') | ||
| for await (const msg of await streamingWithAnAsyncGeneratorFn()) { | ||
| const chunk = msg?.choices[0].delta.content | ||
| if (chunk) { | ||
| setAsyncGeneratorFuncMessages((prev) => prev + chunk) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <main> | ||
| <h1>Typed Readable Stream</h1> | ||
| <div id="streamed-results"> | ||
| <button onClick={() => getTypedReadableStreamResponse()}> | ||
| Get 10 random numbers (ReadableStream) | ||
| </button> | ||
| <button onClick={() => getResponseFromTheAsyncGenerator()}> | ||
| Get 10 random numbers (Async Generator Function) | ||
| </button> | ||
| <pre>{readableStreamMessages()}</pre> | ||
| <pre>{asyncGeneratorFuncMessages()}</pre> | ||
| </div> | ||
| </main> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| body { | ||
| font-family: | ||
| Gordita, Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', | ||
| sans-serif; | ||
| } | ||
|
|
||
| a { | ||
| margin-right: 1rem; | ||
| } | ||
|
|
||
| main { | ||
| text-align: center; | ||
| padding: 1em; | ||
| margin: 0 auto; | ||
| } | ||
|
|
||
| #streamed-results { | ||
| display: grid; | ||
| grid-template-columns: 1fr 1fr; | ||
| } | ||
|
|
||
| #streamed-results > button { | ||
| margin: auto; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], | ||
| "compilerOptions": { | ||
| "strict": true, | ||
| "esModuleInterop": true, | ||
| "jsx": "preserve", | ||
| "jsxImportSource": "solid-js", | ||
| "module": "ESNext", | ||
| "moduleResolution": "Bundler", | ||
| "lib": ["DOM", "DOM.Iterable", "ES2022"], | ||
| "isolatedModules": true, | ||
| "resolveJsonModule": true, | ||
| "skipLibCheck": true, | ||
| "target": "ES2022", | ||
| "allowJs": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "baseUrl": ".", | ||
| "paths": { | ||
| "~/*": ["./src/*"] | ||
| }, | ||
| "noEmit": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { tanstackStart } from '@tanstack/solid-start/plugin/vite' | ||
| import { defineConfig } from 'vite' | ||
| import tsConfigPaths from 'vite-tsconfig-paths' | ||
| import viteSolid from 'vite-plugin-solid' | ||
|
|
||
| export default defineConfig({ | ||
| server: { | ||
| port: 3000, | ||
| }, | ||
| plugins: [ | ||
| tsConfigPaths({ | ||
| projects: ['./tsconfig.json'], | ||
| }), | ||
| tanstackStart(), | ||
| viteSolid({ ssr: true }), | ||
| ], | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the production preview script.
Vite exposes
dev,build, andpreview; there is novite start, sonpm run startwill fail at runtime. Switch tovite previewto align with the documented CLI.(vite.dev)📝 Committable suggestion
🤖 Prompt for AI Agents