### Install Dependencies and Run Locally Source: https://github.com/get-convex/persistent-text-streaming/blob/main/CONTRIBUTING.md Install project dependencies and start the local development server. ```sh npm i npm run dev ``` -------------------------------- ### Install Dependencies and Run App Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/README.md Install project dependencies and start the Convex backend and frontend development servers. Ensure you have Node.js (v18+) and npm (v10+) installed. ```bash npm install npm run dev # in one terminal npm run dev:frontend # in another terminal ``` -------------------------------- ### Build One-Off Package Source: https://github.com/get-convex/persistent-text-streaming/blob/main/CONTRIBUTING.md Clean the project, install specific dependencies, and create a package for distribution. ```sh npm run clean npm ci npm pack ``` -------------------------------- ### Install Persistent Text Streaming Component Source: https://github.com/get-convex/persistent-text-streaming/blob/main/README.md Install the Persistent Text Streaming component using npm. This is the first step to integrating the component into your Convex project. ```bash npm install @convex-dev/persistent-text-streaming ``` -------------------------------- ### Defining an HTTP Endpoint Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md This example demonstrates how to define a basic HTTP endpoint using the `httpAction` decorator and `httpRouter` in Convex. The endpoint is registered at the specified path and handles incoming POST requests. ```APIDOC ## POST /echo ### Description An example HTTP endpoint that echoes the raw bytes of the request body back to the client. ### Method POST ### Endpoint /echo ### Request Body - **body** (bytes) - Required - The raw bytes of the request body. ### Request Example ``` [Binary data] ``` ### Response #### Success Response (200) - **body** (bytes) - The echoed raw bytes from the request body. #### Response Example ``` [Binary data] ``` ``` -------------------------------- ### Set up HTTP Router for CORS and Streaming Endpoint Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt This setup registers a POST endpoint for streaming chat and an OPTIONS handler for CORS preflight requests. This is necessary for cross-origin browser requests to the Convex HTTP endpoint. ```ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; import { streamChat } from "./chat"; const http = httpRouter(); http.route({ path: "/chat-stream", method: "POST", handler: streamChat, }); http.route({ path: "/chat-stream", method: "OPTIONS", handler: httpAction(async (_, request) => { const { headers } = request; if ( headers.get("Origin") !== null && headers.get("Access-Control-Request-Method") !== null && headers.get("Access-Control-Request-Headers") !== null ) { return new Response(null, { headers: new Headers({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "Content-Type, Digest, Authorization", "Access-Control-Max-Age": "86400", }), }); } return new Response(); }), }); export default http; ``` -------------------------------- ### HTTP Router Setup for CORS Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt This section shows how to set up the Convex HTTP router to handle POST requests for streaming and OPTIONS requests for CORS preflight, enabling cross-origin browser requests to your Convex HTTP endpoints. ```APIDOC ## HTTP Router — CORS Setup for `/chat-stream` Registers the streaming HTTP action and a companion OPTIONS handler for CORS preflight requests. This is required so browsers running your frontend on a different origin can POST to the Convex HTTP endpoint. ### Endpoint: POST /chat-stream Handles the main streaming requests. ### Endpoint: OPTIONS /chat-stream Handles CORS preflight requests, allowing cross-origin browser requests. ### Handler Details - The POST handler uses `streamChat`. - The OPTIONS handler checks for necessary CORS headers and returns appropriate response headers like `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, and `Access-Control-Allow-Headers`. ``` -------------------------------- ### Full-Text Search in Convex Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Example of performing a full-text search on the 'messages' table, filtering by 'channel' and ordering by relevance. Use `withSearchIndex` for efficient text searching. ```typescript const messages = await ctx.db .query("messages") .withSearchIndex("search_body", (q) => q.search("body", "hello hi").eq("channel", "#general"), ) .take(10); ``` -------------------------------- ### Discriminated Union Schema Example Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Define schemas with discriminated unions using `v.union()` and `v.object()` with literal kinds. This example defines a schema for results that can be either an 'error' or a 'success'. ```typescript import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ results: defineTable( v.union( v.object({ kind: v.literal("error"), errorMessage: v.string(), }), v.object({ kind: v.literal("success"), value: v.number(), }), ), ) }); ``` -------------------------------- ### Array Validator Example Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Use `v.array()` to validate arrays with elements of a specific type or union of types. This example validates an array that can contain strings or numbers. ```typescript import { mutation } from "./_generated/server"; import { v } from "convex/values"; export default mutation({ args: { simpleArray: v.array(v.union(v.string(), v.number())), }, handler: async (ctx, args) => { //... }, }); ``` -------------------------------- ### PersistentTextStreaming.deleteStream Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Deletes a stream record immediately and schedules its associated chunks for asynchronous batch deletion. Use this to clean up stored stream data, for example, when a user removes a chat message. ```APIDOC ## PersistentTextStreaming.deleteStream(ctx, streamId) ### Description This function immediately deletes a specified stream record from the database. It also schedules the associated chunks for deletion using an asynchronous batch process. This method should be used whenever stream data needs to be cleaned up, such as when a user deletes a message that included a streamed response. ### Parameters - **ctx**: The Convex context object. - **streamId** (StreamId): The unique identifier for the stream to be deleted. ### Returns `void` ``` -------------------------------- ### PersistentTextStreaming.stream Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt The core HTTP action helper for streaming text. It accepts a streamWriter callback that receives a chunkAppender function. Each call to chunkAppender immediately writes the chunk to the HTTP response stream and flushes text to the database at sentence boundaries. Returns a Response object ready to send to the browser. If the stream was already started, it returns HTTP 205. ```APIDOC ## PersistentTextStreaming.stream(ctx, request, streamId, streamWriter) ### Description This function is the core HTTP action helper for streaming text responses. It takes a `streamWriter` callback, which in turn provides a `chunkAppender` function. Users should call `chunkAppender(text)` for each piece of generated text. Each call immediately writes the chunk to the HTTP response stream, and text is flushed to the database at sentence boundaries ('.', '!', '?'). The function returns a `Response` object ready to be sent to the browser. If the stream has already been initiated (indicated by a status other than "pending"), it returns an HTTP 205 status code. ### Parameters - **ctx**: The Convex context object. - **request**: The incoming HTTP request object. - **streamId** (StreamId): The unique identifier for the stream. - **streamWriter**: A callback function that receives `ctx`, `request`, `streamId`, and an `append` function. This callback is responsible for generating and appending text chunks. ### Returns A `Response` object ready to be sent to the browser, or an HTTP 205 if the stream was already started. ``` -------------------------------- ### Delete stream and schedule chunk deletion Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt This mutation deletes a stream record and its associated chunks asynchronously. Use it to clean up stored stream data, for example, when a user deletes a chat message. ```typescript import { mutation, } from "./_generated/server"; import { v, } from "convex/values"; import { StreamId, StreamIdValidator, } from "@convex-dev/persistent-text-streaming"; import { streamingComponent, } from "./streaming"; export const deleteMessage = mutation({ args: { messageId: v.id("userMessages") }, handler: async (ctx, args) => { const message = await ctx.db.get("userMessages", args.messageId); if (!message) throw new Error("Message not found"); // Clean up the associated stream and all its chunks (async batch delete) await streamingComponent.deleteStream( ctx, message.responseStreamId as StreamId, ); // Remove the message record itself await ctx.db.delete("userMessages", args.messageId); }, }); ``` -------------------------------- ### Deploy New Version Source: https://github.com/get-convex/persistent-text-streaming/blob/main/CONTRIBUTING.md Initiate the release process for deploying a new version of the project. ```sh npm run release ``` -------------------------------- ### Run Project Tests Source: https://github.com/get-convex/persistent-text-streaming/blob/main/CONTRIBUTING.md Execute a series of commands to clean, build, typecheck, lint, and run tests for the project. ```sh npm run clean npm run build npm run typecheck npm run lint npm run test ``` -------------------------------- ### Deploy Alpha Release Source: https://github.com/get-convex/persistent-text-streaming/blob/main/CONTRIBUTING.md Initiate the release process for deploying an alpha version of the project. ```sh npm run alpha ``` -------------------------------- ### Query File Metadata from _storage System Table Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Query the `_storage` system table using `ctx.db.system.get` to retrieve file metadata. Avoid using the deprecated `ctx.storage.getMetadata`. ```typescript import { query } from "./_generated/server"; import { Id } from "./_generated/dataModel"; type FileMetadata = { _id: Id<"_storage">; _creationTime: number; contentType?: string; sha256: string; size: number; } export const exampleQuery = query({ args: { fileId: v.id("_storage") }, handler: async (ctx, args) => { const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId); console.log(metadata); return null; }, }); ``` -------------------------------- ### Implementing Pagination in Convex Queries Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Define pagination using `paginationOptsValidator` and the `.paginate()` method on a query. The `paginationOpts` argument should include `numItems` and `cursor`. ```typescript import { v } from "convex/values"; import { query, mutation } from "./_generated/server"; import { paginationOptsValidator } from "convex/server"; export const listWithExtraArg = query({ args: { paginationOpts: paginationOptsValidator, author: v.string() }, handler: async (ctx, args) => { return await ctx.db .query("messages") .withIndex("by_author", (q) => q.eq("author", args.author)) .order("desc") .paginate(args.paginationOpts); }, }); ``` -------------------------------- ### Set OpenAI API Key Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/README.md Configure your Convex backend with your OpenAI API key. This is required for the chat application to communicate with the OpenAI API. ```bash npx convex env set OPENAI_API_KEY= ``` -------------------------------- ### Instantiate PersistentTextStreaming Client Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Instantiate the component client bound to the registered Convex component. This is typically done once per file and exported for reuse. ```typescript // convex/streaming.ts import { PersistentTextStreaming, StreamId, StreamIdValidator, } from "@convex-dev/persistent-text-streaming"; import { components } from "./_generated/api"; import { query } from "./_generated/server"; // Create the component client once export const streamingComponent = new PersistentTextStreaming( components.persistentTextStreaming, ); // Expose a query so the React hook can read the persisted body export const getStreamBody = query({ args: { streamId: StreamIdValidator }, handler: async (ctx, args) => { return await streamingComponent.getStreamBody(ctx, args.streamId as StreamId); }, }); ``` -------------------------------- ### Configure Convex with Persistent Text Streaming Source: https://github.com/get-convex/persistent-text-streaming/blob/main/README.md Add the Persistent Text Streaming component to your Convex project by importing and using it in your `convex.config.ts` file. This makes the component's functionality available within your Convex backend. ```typescript import { defineApp } from "convex/server"; import persistentTextStreaming from "@convex-dev/persistent-text-streaming/convex.config.js"; const app = defineApp(); app.use(persistentTextStreaming); export default app; ``` -------------------------------- ### Registering Internal and Public Convex Functions Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Use `internalQuery`, `internalMutation`, and `internalAction` for private functions, and `query`, `mutation`, and `action` for public functions. Always include argument validators. ```typescript import { internalQuery, internalMutation, internalAction, query, mutation, action, } from "./_generated/server"; import { v } from "convex/values"; // Internal function example export const getSecretData = internalQuery({ args: {}, handler: async (ctx) => { // ... logic ... return "secret"; }, }); // Public function example export const getUserProfile = query({ args: { userId: v.id("users") }, handler: async (ctx, { userId }) => { // ... logic ... return await ctx.db.get(userId); }, }); ``` -------------------------------- ### Replacing a Document in Convex Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Shows how to completely replace an existing document using `ctx.db.replace`. This method requires the document to exist and will throw an error if it does not. ```typescript await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false }) ``` -------------------------------- ### Test Convex Functions with Vitest Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Configure Vitest with `environment: 'edge-runtime'` and use `convex-test` to test your Convex functions. Pass a module map from `import.meta.glob` to `convexTest`. ```typescript /// import { convexTest } from "convex-test"; import { expect, test } from "vitest"; import { api } from "./_generated/api"; import schema from "./schema"; const modules = import.meta.glob("./**/*.ts"); test("some behavior", async () => { const t = convexTest(schema, modules); await t.mutation(api.messages.send, { body: "Hi!", author: "Sarah" }); const messages = await t.query(api.messages.list); expect(messages).toMatchObject([{ body: "Hi!", author: "Sarah" }]); }); ``` -------------------------------- ### Registering the component for testing Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Use this helper to register the component schema and modules with a `convex-test` instance for unit and integration testing. Ensure the component is registered under its default name or a specified name. ```typescript import { convexTest } from "convex-test"; import { expect, test } from "vitest"; import { register } from "@convex-dev/persistent-text-streaming/test"; import schema from "./schema"; const modules = import.meta.glob("./**/*.ts"); test("createStream and addChunk round-trip", async () => { const t = convexTest(schema, modules); // Register the component under its default name register(t, "persistentTextStreaming"); // Create a stream via the component's public mutation const streamId = await t.mutation("persistentTextStreaming:lib:createStream", {}); expect(typeof streamId).toBe("string"); // Add a chunk to the stream await t.mutation("persistentTextStreaming:lib:addChunk", { streamId, text: "Hello, world!", final: true, }); // Read it back const body = await t.query("persistentTextStreaming:lib:getStreamText", { streamId }); expect(body.text).toBe("Hello, world!"); expect(body.status).toBe("done"); }); ``` -------------------------------- ### Typing Records with Id in Convex Queries Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Demonstrates how to use the `Record` type with `Id` for mapping document IDs to strings within a Convex query. Ensure correct type definitions for keys and values when defining records. ```typescript import { query } from "./_generated/server"; import { Doc, Id } from "./_generated/dataModel"; export const exampleQuery = query({ args: { userIds: v.array(v.id("users")) }, handler: async (ctx, args) => { const idToUsername: Record, string> = {}; for (const userId of args.userIds) { const user = await ctx.db.get("users", userId); if (user) { idToUsername[user._id] = user.username; } } return idToUsername; }, }); ``` -------------------------------- ### Convex Authentication Configuration Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Configure JWT-based authentication by defining providers and application IDs. Ensure the domain points to your auth provider's issuer URL. ```typescript export default { providers: [ { domain: "https://your-auth-provider.com", applicationID: "convex", }, ], }; ``` -------------------------------- ### Create and Stream Chat Data in Convex Source: https://github.com/get-convex/persistent-text-streaming/blob/main/README.md Defines mutations and queries for creating chat streams and retrieving their content. Includes an HTTP action to generate and stream chat chunks, saving them to the database. ```typescript const persistentTextStreaming = new PersistentTextStreaming( components.persistentTextStreaming, ); // Create a stream using the component and store the id in the database with // our chat message. export const createChat = mutation({ args: { prompt: v.string(), }, handler: async (ctx, args) => { const streamId = await persistentTextStreaming.createStream(ctx); const chatId = await ctx.db.insert("chats", { title: "...", prompt: args.prompt, stream: streamId, }); return chatId; }, }); // Create a query that returns the chat body. export const getChatBody = query({ args: { streamId: StreamIdValidator, }, handler: async (ctx, args) => { return await persistentTextStreaming.getStreamBody( ctx, args.streamId as StreamId, ); }, }); // Create an HTTP action that generates chunks of the chat body // and uses the component to stream them to the client and save them to the database. export const streamChat = httpAction(async (ctx, request) => { const body = (await request.json()) as { streamId: string }; const generateChat = async (ctx, request, streamId, chunkAppender) => { await chunkAppender("Hi there!"); await chunkAppender("How are you?"); await chunkAppender("Pretend I'm an AI or something!"); }; const response = await persistentTextStreaming.stream( ctx, request, body.streamId as StreamId, generateChat, ); // Set CORS headers appropriately. response.headers.set("Access-Control-Allow-Origin", "*"); response.headers.set("Vary", "Origin"); return response; }); ``` -------------------------------- ### Define a Convex Action Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Use this syntax to define an action that can run in the Node.js runtime. Actions cannot access the database directly. ```typescript import { action } from "./_generated/server"; export const exampleAction = action({ args: {}, handler: async (ctx, args) => { console.log("This action does not return anything"); return null; }, }); ``` -------------------------------- ### Stream chat completions with PersistentTextStreaming Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Use this helper to stream responses from models like OpenAI. It accepts a callback that receives a `chunkAppender` function. Call `chunkAppender(text)` for each piece of text; it's immediately written to the HTTP response and flushed to the database at sentence boundaries. If the stream is already active, it returns HTTP 205. ```typescript import { httpAction, } from "./_generated/server"; import { internal, } from "./_generated/api"; import { StreamId, } from "@convex-dev/persistent-text-streaming"; import { OpenAI, } from "openai"; import { streamingComponent, } from "./streaming"; const openai = new OpenAI(); export const streamChat = httpAction(async (ctx, request) => { const { streamId } = (await request.json()) as { streamId: string }; const response = await streamingComponent.stream( ctx, request, streamId as StreamId, async (ctx, _request, _streamId, append) => { // Fetch conversation history so the model has context const history = await ctx.runQuery(internal.messages.getHistory); // Open a streaming request to OpenAI const stream = await openai.chat.completions.create({ model: "gpt-4.1-mini", messages: [ { role: "system", content: "You are a helpful assistant." }, ...history, ], stream: true, }); // Forward every token to the persistent stream for await (const part of stream) { await append(part.choices[0]?.delta?.content || ""); } // On return, the component automatically flushes remaining text and marks status "done" }, ); // Add CORS headers before returning response.headers.set("Access-Control-Allow-Origin", "*"); response.headers.set("Vary", "Origin"); return response; }); ``` -------------------------------- ### Setting up automatic stream expiration (Cron) Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt The component includes a cron job that automatically marks streams older than 20 minutes as 'timeout'. This prevents resource leaks if an HTTP action is interrupted. No application-level configuration is needed for this feature. ```typescript import { cronJobs } from "convex/server"; import { internal } from "./_generated/api.js"; const crons = cronJobs(); crons.interval( "cleanup expired streams", { minutes: 1 }, internal.lib.cleanupExpiredStreams, // cleanupExpiredStreams marks streams as "timeout" if _creationTime > 20 minutes ago ); ``` -------------------------------- ### Patching a Document in Convex Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Demonstrates how to partially update an existing document using `ctx.db.patch`. This method performs a shallow merge and will throw an error if the document does not exist. ```typescript await ctx.db.patch('tasks', taskId, { completed: true }) ``` -------------------------------- ### Create a New Stream for Text Generation Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Creates a new stream record in the database with status 'pending' and returns its StreamId. Call this inside a Convex mutation when the user submits a new message. ```typescript // convex/messages.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; import { StreamId } from "@convex-dev/persistent-text-streaming"; import { streamingComponent } from "./streaming"; export const sendMessage = mutation({ args: { prompt: v.string() }, handler: async (ctx, args) => { // Allocate a new empty stream const responseStreamId = await streamingComponent.createStream(ctx); // Persist the user message with a reference to the stream const chatId = await ctx.db.insert("userMessages", { prompt: args.prompt, responseStreamId, // StreamId stored as a plain string field }); return chatId; // Return chatId so the client knows which message to drive }, }); ``` -------------------------------- ### Calling Convex Functions with TypeScript Type Annotation Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md When calling a function in the same file using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction`, provide a type annotation on the return value to resolve TypeScript circularity limitations. ```typescript import { query } from "./_generated/server"; import { api } from "./_generated/api"; import { v } from "convex/values"; export const f = query({ args: { name: v.string() }, handler: async (ctx, args) => { return "Hello " + args.name; }, }); export const g = query({ args: {}, handler: async (ctx, args) => { const result: string = await ctx.runQuery(api.example.f, { name: "Bob" }); return null; }, }); ``` -------------------------------- ### Using ConvexProviderWithAuth in React Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Integrate Convex authentication in a React application using ConvexProviderWithAuth. Provide a custom `useAuth` hook that returns loading, authentication status, and an access token fetcher. ```tsx import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react"; const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); function App({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` -------------------------------- ### Use useStream Hook for Real-time Text Streaming in React Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt The `useStream` hook from `@convex-dev/persistent-text-streaming/react` enables real-time HTTP streaming. It POSTs to a specified URL and reads the response body. When `driven` is false or the stream ends, it falls back to a Convex `useQuery` subscription. Optional `authToken` and `headers` can be passed for authenticated endpoints. ```tsx import { StreamId } from "@convex-dev/persistent-text-streaming"; import { useStream } from "@convex-dev/persistent-text-streaming/react"; import { api } from "../../convex/_generated/api"; import { Doc } from "../../convex/_generated/dataModel"; import Markdown from "react-markdown"; const CONVEX_SITE_URL = import.meta.env.VITE_CONVEX_SITE_URL as string; export function ServerMessage({ message, isDriven, }: { message: Doc<"userMessages">; isDriven: boolean; }) { const { text, status } = useStream( api.streaming.getStreamBody, new URL(`${CONVEX_SITE_URL}/chat-stream`), isDriven, message.responseStreamId as StreamId, ); if (status === "pending") return

Thinking…

; if (status === "streaming") return {text}; if (status === "done") return {text}; if (status === "error") return

Error loading response.

; if (status === "timeout") return

Stream timed out.

; } ``` -------------------------------- ### Retrieve stream body and status with Convex query Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Use this query to fetch the complete persisted text and status for a given stream ID. It's useful for clients that need the full content without originating the stream request. The status can be one of `"pending"`, `"streaming"`, `"done"`, `"error"`, or `"timeout"`. ```typescript import { PersistentTextStreaming, StreamId, StreamIdValidator, } from "@convex-dev/persistent-text-streaming"; import { components, } from "./_generated/api"; import { query, } from "./_generated/server"; export const streamingComponent = new PersistentTextStreaming( components.persistentTextStreaming, ); export const getStreamBody = query({ args: { streamId: StreamIdValidator }, handler: async (ctx, args) => { // Returns { text: "Hello world...", status: "done" } const { text, status } = await streamingComponent.getStreamBody( ctx, args.streamId as StreamId, ); return { text, status }; }, }); ``` -------------------------------- ### Subscribe to Chat Stream in React Source: https://github.com/get-convex/persistent-text-streaming/blob/main/README.md Utilizes the `useStream` hook from `@convex-dev/persistent-text-streaming/react` to subscribe to chat data. It requires the Convex query, the HTTP streaming endpoint, and the stream ID. ```typescript // chat-message.tsx, maybe? import { useStream } from "@convex-dev/persistent-text-streaming/react"; // ... // In our component: const { text, status } = useStream( api.chat.getChatBody, // The query to call for the full stream body new URL(`${convexSiteUrl}/chat-stream`), // The HTTP endpoint for streaming driven, // True if this browser session created this chat and should generate the stream chat.streamId as StreamId, // The streamId from the chat database record ); ``` -------------------------------- ### Schedule Cron Jobs with Convex Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Define cron jobs using `crons.interval` or `crons.cron` and export a top-level `crons` object. Ensure to pass a FunctionReference, not the function directly. ```typescript import { cronJobs } from "convex/server"; import { internal } from "./_generated/api"; import { internalAction } from "./_generated/server"; const empty = internalAction({ args: {}, handler: async (ctx, args) => { console.log("empty"); }, }); const crons = cronJobs(); // Run `internal.crons.empty` every two hours. crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {}); export default crons; ``` -------------------------------- ### useStream React Hook Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt The `useStream` hook from `@convex-dev/persistent-text-streaming/react` enables real-time streaming of text data. It can drive an HTTP stream or fall back to a Convex query subscription. It returns the streamed text and its current status. ```APIDOC ## `useStream(getPersistentBody, streamUrl, driven, streamId, opts?)` React hook exported from `@convex-dev/persistent-text-streaming/react`. When `driven` is `true` (the current browser session created the stream), the hook POSTs to `streamUrl` and reads the HTTP response body in real time, accumulating text in local state. When `driven` is `false`, or when the HTTP stream ends, it falls back to a Convex `useQuery` subscription on the persisted body. Returns `{ text: string, status: StreamStatus }`. Accepts optional `authToken` and additional `headers` for authenticated endpoints. ### Parameters - **getPersistentBody**: A Convex query function to fetch the persisted body. - **streamUrl**: The URL of the HTTP streaming endpoint. - **driven**: A boolean indicating if the current session should drive the stream. - **streamId**: The identifier for the stream. - **opts?**: Optional configuration, including `authToken` and custom `headers`. ``` -------------------------------- ### Expose HTTP Endpoint in Convex Source: https://github.com/get-convex/persistent-text-streaming/blob/main/README.md Routes the POST and OPTIONS requests for the chat stream endpoint in your Convex backend. Handles CORS preflight requests to allow cross-origin access. ```typescript http.route({ path: "/chat-stream", method: "POST", handler: streamChat, }); // Handle CORS preflight requests so browsers will allow the POST above when // your app is served from a different origin than your Convex deployment. http.route({ path: "/chat-stream", method: "OPTIONS", handler: httpAction(async (_, request) => { const headers = request.headers; if ( headers.get("Origin") !== null && headers.get("Access-Control-Request-Method") !== null && headers.get("Access-Control-Request-Headers") !== null ) { return new Response(null, { headers: new Headers({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "Content-Type, Digest, Authorization", "Access-Control-Max-Age": "86400", }), }); } else { return new Response(); } }), }); ``` -------------------------------- ### Use a Convex Query Function in React Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/README.md Utilize the `useQuery` hook from Convex to call a query function in a React component. Pass the API path and arguments. ```typescript const data = useQuery(api.functions.myQueryFunction, { first: 10, second: "hello", }); ``` -------------------------------- ### PersistentTextStreaming.createStream Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Creates a new stream record in the database with status 'pending' and returns its StreamId. This should be called within a Convex mutation when a user submits new content. ```APIDOC ## PersistentTextStreaming.createStream(ctx) ### Description Creates a new stream record in the database with status `"pending"` and returns its `StreamId`. Call this inside a Convex mutation when the user submits a new message. Store the returned `streamId` alongside the message record so it can be referenced later by the HTTP action and by queries. ### Method `PersistentTextStreaming.createStream` ### Parameters #### Context - **ctx** (Convex context) - The Convex context object. ### Returns - **StreamId** - The unique identifier for the newly created stream. ### Request Example ```ts // convex/messages.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; import { StreamId } from "@convex-dev/persistent-text-streaming"; import { streamingComponent } from "./streaming"; export const sendMessage = mutation({ args: { prompt: v.string() }, handler: async (ctx, args) => { // Allocate a new empty stream const responseStreamId = await streamingComponent.createStream(ctx); // Persist the user message with a reference to the stream const chatId = await ctx.db.insert("userMessages", { prompt: args.prompt, responseStreamId, // StreamId stored as a plain string field }); return chatId; // Return chatId so the client knows which message to drive }, }); ``` ``` -------------------------------- ### Define HTTP Endpoint with httpAction Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Use the `httpAction` decorator to define HTTP endpoints in `convex/http.ts`. Endpoints are registered at the exact path specified. ```typescript import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const http = httpRouter(); http.route({ path: "/echo", method: "POST", handler: httpAction(async (ctx, req) => { const body = await req.bytes(); return new Response(body, { status: 200 }); }), }); ``` -------------------------------- ### Create Chat Mutation in React Source: https://github.com/get-convex/persistent-text-streaming/blob/main/README.md Uses the `useMutation` hook to call the `createChat` mutation defined in Convex. This is typically used in a form submission handler. ```typescript // chat-input.tsx, maybe? const createChat = useMutation(api.chat.createChat); const formSubmit = async (e: React.FormEvent) => { e.preventDefault(); const chatId = await createChat({ prompt: inputValue, }); }; ``` -------------------------------- ### Convex Type Validators Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/_generated/ai/guidelines.md Reference for Convex types and their corresponding validators for argument validation and schema definition. ```typescript v.id(tableName) ``` ```typescript v.null() ``` ```typescript v.int64() ``` ```typescript v.number() ``` ```typescript v.boolean() ``` ```typescript v.string() ``` ```typescript v.bytes() ``` ```typescript v.array(values) ``` ```typescript v.object({property: value}) ``` ```typescript v.record(keys, values) ``` -------------------------------- ### Define a Convex Query Function Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/README.md Define a query function with argument validators and implement its logic to read from the database. Use `v` for argument validation. ```typescript import { query } from "./_generated/server"; import { v } from "convex/values"; export const myQueryFunction = query({ // Validators for arguments. args: { first: v.number(), second: v.string(), }, // Function implementation. handler: async (ctx, args) => { // Read the database as many times as you need here. // See https://docs.convex.dev/database/reading-data. const documents = await ctx.db.query("tablename").collect(); // Arguments passed from the client are properties of the args object. console.log(args.first, args.second); // Write arbitrary JavaScript here: filter, aggregate, build derived data, // remove non-public properties, or create new objects. return documents; }, }); ``` -------------------------------- ### Use a Convex Mutation Function in React Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/README.md Call a mutation function using the `useMutation` hook in a React component. Mutations can be fired and forgotten or their results can be handled. ```typescript const mutation = useMutation(api.functions.myMutationFunction); function handleButtonPress() { // fire and forget, the most common way to use mutations mutation({ first: "Hello!", second: "me" }); // OR // use the result once the mutation has completed mutation({ first: "Hello!", second: "me" }).then((result) => console.log(result), ); } ``` -------------------------------- ### Define a Convex Mutation Function Source: https://github.com/get-convex/persistent-text-streaming/blob/main/example/convex/README.md Define a mutation function with argument validators and implement its logic to write to the database. Mutations can also read data. ```typescript import { mutation } from "./_generated/server"; import { v } from "convex/values"; export const myMutationFunction = mutation({ // Validators for arguments. args: { first: v.string(), second: v.string(), }, // Function implementation. handler: async (ctx, args) => { // Insert or modify documents in the database here. // Mutations can also read from the database like queries. // See https://docs.convex.dev/database/writing-data. const message = { body: args.first, author: args.second }; const id = await ctx.db.insert("messages", message); // Optionally, return a value from your mutation. return await ctx.db.get(id); }, }); ``` -------------------------------- ### PersistentTextStreaming.getStreamBody Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Reads all persisted chunks from the database for a given streamId, concatenates them, and returns the full text along with its status. This is useful for non-driving clients within Convex queries. ```APIDOC ## PersistentTextStreaming.getStreamBody(ctx, streamId) ### Description This function reads all persisted chunks from the database for a specified `streamId`. It then concatenates these chunks into a single string and returns an object containing the full `text` and the `status` of the stream. The `StreamStatus` can be one of "pending", "streaming", "done", "error", or "timeout". This method is intended for use within Convex queries to expose the complete stream body to clients that are not actively driving the stream. ### Parameters - **ctx**: The Convex context object. - **streamId** (StreamId): The unique identifier for the stream. ### Returns An object with the following properties: - **text** (string): The concatenated text content of the stream. - **status** (StreamStatus): The current status of the stream. ``` -------------------------------- ### Define Schema with StreamIdValidator and StreamId Type Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Use `StreamIdValidator` (a `v.string()`) in `defineTable` for runtime validation and `StreamId` (a branded string type) for type-safe handling of stream identifiers in TypeScript. This ensures that stream IDs are correctly validated and used throughout the codebase. ```ts // convex/schema.ts — store a StreamId in a table import { defineSchema, defineTable } from "convex/server"; import { StreamIdValidator } from "@convex-dev/persistent-text-streaming"; import { v } from "convex/values"; export default defineSchema({ userMessages: defineTable({ prompt: v.string(), responseStreamId: StreamIdValidator, }).index("by_stream", ["responseStreamId"]), }); // Usage in mutations / queries — cast when calling component methods: import { StreamId } from "@convex-dev/persistent-text-streaming"; const body = await streamingComponent.getStreamBody( ctx, message.responseStreamId as StreamId, ); ``` -------------------------------- ### StreamId Type and Validator Source: https://context7.com/get-convex/persistent-text-streaming/llms.txt Defines the `StreamIdValidator` for Convex schema validation and the `StreamId` TypeScript type for type-safe stream identifier handling within your codebase. ```APIDOC ### `StreamIdValidator` and `StreamId` type `StreamIdValidator` is a Convex value validator (`v.string()`) for use in `defineTable` and function argument schemas. `StreamId` is a branded TypeScript string type (`string & { __isStreamId: true }`) for type-safe handling of stream identifiers throughout your codebase. ### Usage in Schema ```ts // convex/schema.ts import { defineSchema, defineTable } from "convex/server"; import { StreamIdValidator } from "@convex-dev/persistent-text-streaming"; import { v } from "convex/values"; export default defineSchema({ userMessages: defineTable({ prompt: v.string(), responseStreamId: StreamIdValidator, // validated as a plain string at runtime }).index("by_stream", ["responseStreamId"]), }); ``` ### Usage in Mutations/Queries When interacting with functions or components that expect a `StreamId`, cast the stored string value to the `StreamId` type. ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.