@convex-dev/persistent-text-streaming 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -0
  3. package/dist/commonjs/client/index.d.ts +89 -0
  4. package/dist/commonjs/client/index.d.ts.map +1 -0
  5. package/dist/commonjs/client/index.js +133 -0
  6. package/dist/commonjs/client/index.js.map +1 -0
  7. package/dist/commonjs/component/_generated/api.d.ts +14 -0
  8. package/dist/commonjs/component/_generated/api.d.ts.map +1 -0
  9. package/dist/commonjs/component/_generated/api.js +22 -0
  10. package/dist/commonjs/component/_generated/api.js.map +1 -0
  11. package/dist/commonjs/component/_generated/server.d.ts +64 -0
  12. package/dist/commonjs/component/_generated/server.d.ts.map +1 -0
  13. package/dist/commonjs/component/_generated/server.js +74 -0
  14. package/dist/commonjs/component/_generated/server.js.map +1 -0
  15. package/dist/commonjs/component/convex.config.d.ts +3 -0
  16. package/dist/commonjs/component/convex.config.d.ts.map +1 -0
  17. package/dist/commonjs/component/convex.config.js +3 -0
  18. package/dist/commonjs/component/convex.config.js.map +1 -0
  19. package/dist/commonjs/component/crons.d.ts +3 -0
  20. package/dist/commonjs/component/crons.d.ts.map +1 -0
  21. package/dist/commonjs/component/crons.js +7 -0
  22. package/dist/commonjs/component/crons.js.map +1 -0
  23. package/dist/commonjs/component/lib.d.ts +21 -0
  24. package/dist/commonjs/component/lib.d.ts.map +1 -0
  25. package/dist/commonjs/component/lib.js +134 -0
  26. package/dist/commonjs/component/lib.js.map +1 -0
  27. package/dist/commonjs/component/schema.d.ts +23 -0
  28. package/dist/commonjs/component/schema.d.ts.map +1 -0
  29. package/dist/commonjs/component/schema.js +13 -0
  30. package/dist/commonjs/component/schema.js.map +1 -0
  31. package/dist/commonjs/package.json +3 -0
  32. package/dist/commonjs/react/index.d.ts +23 -0
  33. package/dist/commonjs/react/index.d.ts.map +1 -0
  34. package/dist/commonjs/react/index.js +131 -0
  35. package/dist/commonjs/react/index.js.map +1 -0
  36. package/dist/esm/client/index.d.ts +89 -0
  37. package/dist/esm/client/index.d.ts.map +1 -0
  38. package/dist/esm/client/index.js +133 -0
  39. package/dist/esm/client/index.js.map +1 -0
  40. package/dist/esm/component/_generated/api.d.ts +14 -0
  41. package/dist/esm/component/_generated/api.d.ts.map +1 -0
  42. package/dist/esm/component/_generated/api.js +22 -0
  43. package/dist/esm/component/_generated/api.js.map +1 -0
  44. package/dist/esm/component/_generated/server.d.ts +64 -0
  45. package/dist/esm/component/_generated/server.d.ts.map +1 -0
  46. package/dist/esm/component/_generated/server.js +74 -0
  47. package/dist/esm/component/_generated/server.js.map +1 -0
  48. package/dist/esm/component/convex.config.d.ts +3 -0
  49. package/dist/esm/component/convex.config.d.ts.map +1 -0
  50. package/dist/esm/component/convex.config.js +3 -0
  51. package/dist/esm/component/convex.config.js.map +1 -0
  52. package/dist/esm/component/crons.d.ts +3 -0
  53. package/dist/esm/component/crons.d.ts.map +1 -0
  54. package/dist/esm/component/crons.js +7 -0
  55. package/dist/esm/component/crons.js.map +1 -0
  56. package/dist/esm/component/lib.d.ts +21 -0
  57. package/dist/esm/component/lib.d.ts.map +1 -0
  58. package/dist/esm/component/lib.js +134 -0
  59. package/dist/esm/component/lib.js.map +1 -0
  60. package/dist/esm/component/schema.d.ts +23 -0
  61. package/dist/esm/component/schema.d.ts.map +1 -0
  62. package/dist/esm/component/schema.js +13 -0
  63. package/dist/esm/component/schema.js.map +1 -0
  64. package/dist/esm/package.json +3 -0
  65. package/dist/esm/react/index.d.ts +23 -0
  66. package/dist/esm/react/index.d.ts.map +1 -0
  67. package/dist/esm/react/index.js +131 -0
  68. package/dist/esm/react/index.js.map +1 -0
  69. package/package.json +89 -0
  70. package/react/package.json +5 -0
  71. package/src/client/index.ts +223 -0
  72. package/src/component/_generated/api.d.ts +80 -0
  73. package/src/component/_generated/api.js +23 -0
  74. package/src/component/_generated/dataModel.d.ts +60 -0
  75. package/src/component/_generated/server.d.ts +149 -0
  76. package/src/component/_generated/server.js +90 -0
  77. package/src/component/convex.config.ts +3 -0
  78. package/src/component/crons.ts +13 -0
  79. package/src/component/lib.test.ts +9 -0
  80. package/src/component/lib.ts +149 -0
  81. package/src/component/schema.ts +21 -0
  82. package/src/react/index.ts +158 -0
@@ -0,0 +1,23 @@
1
+ import { Infer } from "convex/values";
2
+ export declare const streamStatusValidator: import("convex/values").VUnion<"pending" | "streaming" | "done" | "error" | "timeout", [import("convex/values").VLiteral<"pending", "required">, import("convex/values").VLiteral<"streaming", "required">, import("convex/values").VLiteral<"done", "required">, import("convex/values").VLiteral<"error", "required">, import("convex/values").VLiteral<"timeout", "required">], "required", never>;
3
+ export type StreamStatus = Infer<typeof streamStatusValidator>;
4
+ declare const _default: import("convex/server").SchemaDefinition<{
5
+ streams: import("convex/server").TableDefinition<import("convex/values").VObject<{
6
+ status: "pending" | "streaming" | "done" | "error" | "timeout";
7
+ }, {
8
+ status: import("convex/values").VUnion<"pending" | "streaming" | "done" | "error" | "timeout", [import("convex/values").VLiteral<"pending", "required">, import("convex/values").VLiteral<"streaming", "required">, import("convex/values").VLiteral<"done", "required">, import("convex/values").VLiteral<"error", "required">, import("convex/values").VLiteral<"timeout", "required">], "required", never>;
9
+ }, "required", "status">, {
10
+ byStatus: ["status", "_creationTime"];
11
+ }, {}, {}>;
12
+ chunks: import("convex/server").TableDefinition<import("convex/values").VObject<{
13
+ streamId: import("convex/values").GenericId<"streams">;
14
+ text: string;
15
+ }, {
16
+ streamId: import("convex/values").VId<import("convex/values").GenericId<"streams">, "required">;
17
+ text: import("convex/values").VString<string, "required">;
18
+ }, "required", "streamId" | "text">, {
19
+ byStream: ["streamId", "_creationTime"];
20
+ }, {}, {}>;
21
+ }, true>;
22
+ export default _default;
23
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/component/schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAK,MAAM,eAAe,CAAC;AAEzC,eAAO,MAAM,qBAAqB,uYAMjC,CAAC;AACF,MAAM,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;;;;;;;;;;;;;;;;;;;AAE/D,wBAQG"}
@@ -0,0 +1,13 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+ export const streamStatusValidator = v.union(v.literal("pending"), v.literal("streaming"), v.literal("done"), v.literal("error"), v.literal("timeout"));
4
+ export default defineSchema({
5
+ streams: defineTable({
6
+ status: streamStatusValidator,
7
+ }).index("byStatus", ["status"]),
8
+ chunks: defineTable({
9
+ streamId: v.id("streams"),
10
+ text: v.string(),
11
+ }).index("byStream", ["streamId"]),
12
+ });
13
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAS,CAAC,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAC1C,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EACpB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,EACtB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EACjB,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAClB,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CACrB,CAAC;AAGF,eAAe,YAAY,CAAC;IAC1B,OAAO,EAAE,WAAW,CAAC;QACnB,MAAM,EAAE,qBAAqB;KAC9B,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,EAAE,WAAW,CAAC;QAClB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC;QACzB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;KACjB,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC;CACnC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,23 @@
1
+ import { StreamBody, StreamId } from "../client";
2
+ import { FunctionReference } from "convex/server";
3
+ /**
4
+ * React hook for persistent text streaming.
5
+ *
6
+ * @param getPersistentBody - A query function reference that returns the body
7
+ * of a stream using the component's `getStreamBody` method.
8
+ * @param streamUrl - The URL of the http action that will kick off the stream
9
+ * generation and stream the result back to the client using the component's
10
+ * `stream` method.
11
+ * @param driven - Whether this particular session is driving the stream. Set this
12
+ * to true if this is the client session that first created the stream using the
13
+ * component's `createStream` method. If you're simply reloading an existing
14
+ * stream, set this to false.
15
+ * @param streamId - The ID of the stream. If this is not provided, the return
16
+ * value will be an empty string for the stream body and the status will be
17
+ * `pending`.
18
+ * @returns The body and status of the stream.
19
+ */
20
+ export declare function useStream(getPersistentBody: FunctionReference<"query", "public", {
21
+ streamId: string;
22
+ }, StreamBody>, streamUrl: URL, driven: boolean, streamId?: StreamId): StreamBody;
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAMlD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CACvB,iBAAiB,EAAE,iBAAiB,CAClC,OAAO,EACP,QAAQ,EACR;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,EACpB,UAAU,CACX,EACD,SAAS,EAAE,GAAG,EACd,MAAM,EAAE,OAAO,EACf,QAAQ,CAAC,EAAE,QAAQ,cAgEpB"}
@@ -0,0 +1,131 @@
1
+ "use client";
2
+ import { useQuery } from "convex/react";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ if (typeof window === "undefined") {
5
+ throw new Error("this is frontend code, but it's running somewhere else!");
6
+ }
7
+ /**
8
+ * React hook for persistent text streaming.
9
+ *
10
+ * @param getPersistentBody - A query function reference that returns the body
11
+ * of a stream using the component's `getStreamBody` method.
12
+ * @param streamUrl - The URL of the http action that will kick off the stream
13
+ * generation and stream the result back to the client using the component's
14
+ * `stream` method.
15
+ * @param driven - Whether this particular session is driving the stream. Set this
16
+ * to true if this is the client session that first created the stream using the
17
+ * component's `createStream` method. If you're simply reloading an existing
18
+ * stream, set this to false.
19
+ * @param streamId - The ID of the stream. If this is not provided, the return
20
+ * value will be an empty string for the stream body and the status will be
21
+ * `pending`.
22
+ * @returns The body and status of the stream.
23
+ */
24
+ export function useStream(getPersistentBody, streamUrl, driven, streamId) {
25
+ const [streamEnded, setStreamEnded] = useState(null);
26
+ // Used to prevent strict mode from causing multiple streams to be started.
27
+ const streamStarted = useRef(false);
28
+ const usePersistence = useMemo(() => {
29
+ // Something is wrong with the stream, so we need to use the database value.
30
+ if (streamEnded === false) {
31
+ return true;
32
+ }
33
+ // If we're not driving the stream, we must use the database value.
34
+ if (!driven) {
35
+ return true;
36
+ }
37
+ // Otherwise, we'll try to drive the stream and use the HTTP response.
38
+ return false;
39
+ }, [driven, streamId, streamEnded]);
40
+ // console.log("usePersistence", usePersistence);
41
+ const persistentBody = useQuery(getPersistentBody, usePersistence && streamId ? { streamId: streamId } : "skip");
42
+ const [streamBody, setStreamBody] = useState("");
43
+ useEffect(() => {
44
+ if (driven && streamId && !streamStarted.current) {
45
+ // Kick off HTTP action.
46
+ void (async () => {
47
+ const success = await startStreaming(streamUrl, streamId, (text) => {
48
+ setStreamBody((prev) => prev + text);
49
+ });
50
+ setStreamEnded(success);
51
+ })();
52
+ // If we get remounted, we don't want to start a new stream.
53
+ return () => {
54
+ streamStarted.current = true;
55
+ };
56
+ }
57
+ }, [driven, streamId, setStreamEnded, streamStarted]);
58
+ const body = useMemo(() => {
59
+ // console.log(
60
+ // "body info p vs. s",
61
+ // persistentBody?.text?.length ?? 0,
62
+ // streamBody.length
63
+ //);
64
+ if (persistentBody) {
65
+ return persistentBody;
66
+ }
67
+ let status;
68
+ if (streamEnded === null) {
69
+ status = streamBody.length > 0 ? "streaming" : "pending";
70
+ }
71
+ else {
72
+ status = streamEnded ? "done" : "error";
73
+ }
74
+ return {
75
+ text: streamBody,
76
+ status: status,
77
+ };
78
+ }, [persistentBody, streamBody, streamEnded]);
79
+ return body;
80
+ }
81
+ /**
82
+ * Internal helper for starting a stream.
83
+ *
84
+ * @param url - The URL of the http action that will kick off the stream
85
+ * generation and stream the result back to the client using the component's
86
+ * `stream` method.
87
+ * @param streamId - The ID of the stream.
88
+ * @param onUpdate - A function that updates the stream body.
89
+ * @returns A promise that resolves to a boolean indicating whether the stream
90
+ * was started successfully. It can fail if the http action is not found, or
91
+ * CORS fails, or an exception is raised, or the stream is already running
92
+ * or finished, etc.
93
+ */
94
+ async function startStreaming(url, streamId, onUpdate) {
95
+ const response = await fetch(url, {
96
+ method: "POST",
97
+ body: JSON.stringify({
98
+ streamId: streamId,
99
+ }),
100
+ headers: { "Content-Type": "application/json" },
101
+ });
102
+ // Adapted from https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
103
+ if (response.status === 205) {
104
+ console.error("Stream already finished", response);
105
+ return false;
106
+ }
107
+ if (!response.ok) {
108
+ console.error("Failed to reach streaming endpoint", response);
109
+ return false;
110
+ }
111
+ if (!response.body) {
112
+ console.error("No body in response", response);
113
+ return false;
114
+ }
115
+ const reader = response.body.getReader();
116
+ while (true) {
117
+ try {
118
+ const { done, value } = await reader.read();
119
+ if (done) {
120
+ onUpdate(new TextDecoder().decode(value));
121
+ return true;
122
+ }
123
+ onUpdate(new TextDecoder().decode(value));
124
+ }
125
+ catch (e) {
126
+ console.error("Error reading stream", e);
127
+ return false;
128
+ }
129
+ }
130
+ }
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@convex-dev/persistent-text-streaming",
3
+ "description": "A Convex component for streaming durable text to the client, from LLMs and other sources.",
4
+ "repository": "github:get-convex/persistent-text-streaming",
5
+ "homepage": "https://github.com/get-convex/persistent-text-streaming#readme",
6
+ "bugs": {
7
+ "email": "support@convex.dev",
8
+ "url": "https://github.com/get-convex/persistent-text-streaming/issues"
9
+ },
10
+ "version": "0.0.1",
11
+ "license": "Apache-2.0",
12
+ "keywords": [
13
+ "convex",
14
+ "component"
15
+ ],
16
+ "type": "module",
17
+ "scripts": {
18
+ "build": "npm run build:esm && npm run build:cjs",
19
+ "build:esm": "tsc --project ./esm.json && echo '{\\n \"type\": \"module\"\\n}' > dist/esm/package.json",
20
+ "build:cjs": "tsc --project ./commonjs.json && echo '{\\n \"type\": \"commonjs\"\\n}' > dist/commonjs/package.json",
21
+ "dev": "cd example; npm run dev",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepare": "npm run build",
24
+ "prepack": "node node10stubs.mjs",
25
+ "postpack": "node node10stubs.mjs --cleanup",
26
+ "test": "vitest run",
27
+ "test:debug": "vitest --inspect-brk --no-file-parallelism",
28
+ "test:coverage": "vitest run --coverage --coverage.reporter=text"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "react"
34
+ ],
35
+ "exports": {
36
+ "./package.json": "./package.json",
37
+ ".": {
38
+ "import": {
39
+ "@convex-dev/component-source": "./src/client/index.ts",
40
+ "types": "./dist/esm/client/index.d.ts",
41
+ "default": "./dist/esm/client/index.js"
42
+ },
43
+ "require": {
44
+ "@convex-dev/component-source": "./src/client/index.ts",
45
+ "types": "./dist/commonjs/client/index.d.ts",
46
+ "default": "./dist/commonjs/client/index.js"
47
+ }
48
+ },
49
+ "./react": {
50
+ "import": {
51
+ "@convex-dev/component-source": "./src/react/index.ts",
52
+ "types": "./dist/esm/react/index.d.ts",
53
+ "default": "./dist/esm/react/index.js"
54
+ },
55
+ "require": {
56
+ "@convex-dev/component-source": "./src/react/index.ts",
57
+ "types": "./dist/commonjs/react/index.d.ts",
58
+ "default": "./dist/commonjs/react/index.js"
59
+ }
60
+ },
61
+ "./convex.config": {
62
+ "import": {
63
+ "@convex-dev/component-source": "./src/component/convex.config.ts",
64
+ "types": "./dist/esm/component/convex.config.d.ts",
65
+ "default": "./dist/esm/component/convex.config.js"
66
+ }
67
+ }
68
+ },
69
+ "peerDependencies": {
70
+ "convex": "~1.16.5 || ~1.17.0 || ~1.18.0 || ~1.19.0",
71
+ "react": "~18.3.1 || ~19.0.0",
72
+ "react-dom": "~18.3.1 || ~19.0.0"
73
+ },
74
+ "devDependencies": {
75
+ "@eslint/js": "^9.9.1",
76
+ "@types/node": "^18.17.0",
77
+ "@types/react": "^19.0.10",
78
+ "convex-test": "^0.0.36",
79
+ "eslint": "^9.9.1",
80
+ "globals": "^15.9.0",
81
+ "prettier": "3.2.5",
82
+ "typescript": "~5.0.3",
83
+ "typescript-eslint": "^8.4.0",
84
+ "vitest": "^2.1.4"
85
+ },
86
+ "main": "./dist/commonjs/client/index.js",
87
+ "types": "./dist/commonjs/client/index.d.ts",
88
+ "module": "./dist/esm/client/index.js"
89
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "main": "../dist/commonjs/./react/index.js",
3
+ "module": "../dist/esm/./react/index.js",
4
+ "types": "../dist/commonjs/./react/index.d.ts"
5
+ }
@@ -0,0 +1,223 @@
1
+ import {
2
+ Expand,
3
+ FunctionReference,
4
+ GenericActionCtx,
5
+ GenericDataModel,
6
+ GenericMutationCtx,
7
+ GenericQueryCtx,
8
+ } from "convex/server";
9
+ import { GenericId, v } from "convex/values";
10
+ import { api } from "../component/_generated/api";
11
+ import { StreamStatus } from "../component/schema";
12
+
13
+ export type StreamId = string & { __isStreamId: true };
14
+ export const StreamIdValidator = v.string();
15
+ export type StreamBody = {
16
+ text: string;
17
+ status: StreamStatus;
18
+ };
19
+
20
+ export type ChunkAppender = (text: string) => Promise<void>;
21
+ export type StreamWriter<A extends GenericActionCtx<GenericDataModel>> = (
22
+ ctx: A,
23
+ request: Request,
24
+ streamId: StreamId,
25
+ chunkAppender: ChunkAppender
26
+ ) => Promise<void>;
27
+
28
+ // TODO -- make more flexible. # of bytes, etc?
29
+ const hasDelimeter = (text: string) => {
30
+ return text.includes(".") || text.includes("!") || text.includes("?");
31
+ };
32
+
33
+ // TODO -- some sort of wrapper with easy ergonomics for working with LLMs?
34
+ export class PersistentTextStreaming {
35
+ constructor(
36
+ public component: UseApi<typeof api>,
37
+ public options?: object
38
+ ) {}
39
+
40
+ /**
41
+ * Create a new stream. This will return a stream ID that can be used
42
+ * in an HTTP action to stream data back out to the client while also
43
+ * permanently persisting the final stream in the database.
44
+ *
45
+ * @param ctx - A convex context capable of running mutations.
46
+ * @returns The ID of the new stream.
47
+ * @example
48
+ * ```ts
49
+ * const streaming = new PersistentTextStreaming(api);
50
+ * const streamId = await streaming.createStream(ctx);
51
+ * await streaming.stream(ctx, request, streamId, async (ctx, req, id, append) => {
52
+ * await append("Hello ");
53
+ * await append("World!");
54
+ * });
55
+ * ```
56
+ */
57
+
58
+ async createStream(ctx: RunMutationCtx): Promise<StreamId> {
59
+ const id = await ctx.runMutation(this.component.lib.createStream);
60
+ return id as StreamId;
61
+ }
62
+
63
+ /**
64
+ * Get the body of a stream. This will return the full text of the stream
65
+ * and the status of the stream.
66
+ *
67
+ * @param ctx - A convex context capable of running queries.
68
+ * @param streamId - The ID of the stream to get the body of.
69
+ * @returns The body of the stream and the status of the stream.
70
+ * @example
71
+ * ```ts
72
+ * const streaming = new PersistentTextStreaming(api);
73
+ * const { text, status } = await streaming.getStreamBody(ctx, streamId);
74
+ * ```
75
+ */
76
+ async getStreamBody(
77
+ ctx: RunQueryCtx,
78
+ streamId: StreamId
79
+ ): Promise<StreamBody> {
80
+ const { text, status } = await ctx.runQuery(
81
+ this.component.lib.getStreamText,
82
+ { streamId }
83
+ );
84
+ return { text, status: status as StreamStatus };
85
+ }
86
+
87
+ /**
88
+ * Inside an HTTP action, this will stream data back to the client while
89
+ * also persisting the final stream in the database.
90
+ *
91
+ * @param ctx - A convex context capable of running actions.
92
+ * @param request - The HTTP request object.
93
+ * @param streamId - The ID of the stream.
94
+ * @param streamWriter - A function that generates chunks and writes them
95
+ * to the stream with the given `StreamWriter`.
96
+ * @returns A promise that resolves to an HTTP response. You may need to adjust
97
+ * the headers of this response for CORS, etc.
98
+ * @example
99
+ * ```ts
100
+ * const streaming = new PersistentTextStreaming(api);
101
+ * const streamId = await streaming.createStream(ctx);
102
+ * const response = await streaming.stream(ctx, request, streamId, async (ctx, req, id, append) => {
103
+ * await append("Hello ");
104
+ * await append("World!");
105
+ * });
106
+ * ```
107
+ */
108
+ async stream<A extends GenericActionCtx<GenericDataModel>>(
109
+ ctx: A,
110
+ request: Request,
111
+ streamId: StreamId,
112
+ streamWriter: StreamWriter<A>
113
+ ) {
114
+ const streamState = await ctx.runQuery(this.component.lib.getStreamStatus, {
115
+ streamId,
116
+ });
117
+ if (streamState !== "pending") {
118
+ console.log("Stream was already started");
119
+ return new Response("", {
120
+ status: 205,
121
+ });
122
+ }
123
+ // Create a TransformStream to handle streaming data
124
+ const { readable, writable } = new TransformStream();
125
+ const writer = writable.getWriter();
126
+ const textEncoder = new TextEncoder();
127
+ let pending = "";
128
+
129
+ const doStream = async () => {
130
+ const chunkAppender: ChunkAppender = async (text) => {
131
+ // write to this handler's response stream on every update
132
+ await writer.write(textEncoder.encode(text));
133
+ pending += text;
134
+ // write to the database periodically, like at the end of sentences
135
+ if (hasDelimeter(text)) {
136
+ await this.addChunk(ctx, streamId, pending, false);
137
+ pending = "";
138
+ }
139
+ };
140
+ try {
141
+ await streamWriter(ctx, request, streamId, chunkAppender);
142
+ } catch (e) {
143
+ await this.setStreamStatus(ctx, streamId, "error");
144
+ await writer.close();
145
+ throw e;
146
+ }
147
+
148
+ // Success? Flush any last updates
149
+ await this.addChunk(ctx, streamId, pending, true);
150
+
151
+ await writer.close();
152
+ };
153
+
154
+ // Kick off the streaming, but don't await it.
155
+ void doStream();
156
+
157
+ // Send the readable back to the browser
158
+ return new Response(readable);
159
+ }
160
+
161
+ // Internal helper -- add a chunk to the stream.
162
+ private async addChunk(
163
+ ctx: RunMutationCtx,
164
+ streamId: StreamId,
165
+ text: string,
166
+ final: boolean
167
+ ) {
168
+ await ctx.runMutation(this.component.lib.addChunk, {
169
+ streamId,
170
+ text,
171
+ final,
172
+ });
173
+ }
174
+
175
+ // Internal helper -- set the status of a stream.
176
+ private async setStreamStatus(
177
+ ctx: RunMutationCtx,
178
+ streamId: StreamId,
179
+ status: StreamStatus
180
+ ) {
181
+ await ctx.runMutation(this.component.lib.setStreamStatus, {
182
+ streamId,
183
+ status,
184
+ });
185
+ }
186
+ }
187
+
188
+ /* Type utils follow */
189
+
190
+ type RunQueryCtx = {
191
+ runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
192
+ };
193
+ type RunMutationCtx = {
194
+ runMutation: GenericMutationCtx<GenericDataModel>["runMutation"];
195
+ };
196
+
197
+ export type OpaqueIds<T> = T extends GenericId<infer _T> | string
198
+ ? string
199
+ : T extends (infer U)[]
200
+ ? OpaqueIds<U>[]
201
+ : T extends ArrayBuffer
202
+ ? ArrayBuffer
203
+ : T extends object
204
+ ? { [K in keyof T]: OpaqueIds<T[K]> }
205
+ : T;
206
+
207
+ export type UseApi<API> = Expand<{
208
+ [mod in keyof API]: API[mod] extends FunctionReference<
209
+ infer FType,
210
+ "public",
211
+ infer FArgs,
212
+ infer FReturnType,
213
+ infer FComponentPath
214
+ >
215
+ ? FunctionReference<
216
+ FType,
217
+ "internal",
218
+ OpaqueIds<FArgs>,
219
+ OpaqueIds<FReturnType>,
220
+ FComponentPath
221
+ >
222
+ : UseApi<API[mod]>;
223
+ }>;
@@ -0,0 +1,80 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type * as crons from "../crons.js";
12
+ import type * as lib from "../lib.js";
13
+
14
+ import type {
15
+ ApiFromModules,
16
+ FilterApi,
17
+ FunctionReference,
18
+ } from "convex/server";
19
+ /**
20
+ * A utility for referencing Convex functions in your app's API.
21
+ *
22
+ * Usage:
23
+ * ```js
24
+ * const myFunctionReference = api.myModule.myFunction;
25
+ * ```
26
+ */
27
+ declare const fullApi: ApiFromModules<{
28
+ crons: typeof crons;
29
+ lib: typeof lib;
30
+ }>;
31
+ export type Mounts = {
32
+ lib: {
33
+ addChunk: FunctionReference<
34
+ "mutation",
35
+ "public",
36
+ { final: boolean; streamId: string; text: string },
37
+ any
38
+ >;
39
+ createStream: FunctionReference<"mutation", "public", {}, any>;
40
+ getStreamStatus: FunctionReference<
41
+ "query",
42
+ "public",
43
+ { streamId: string },
44
+ "pending" | "streaming" | "done" | "error" | "timeout"
45
+ >;
46
+ getStreamText: FunctionReference<
47
+ "query",
48
+ "public",
49
+ { streamId: string },
50
+ {
51
+ status: "pending" | "streaming" | "done" | "error" | "timeout";
52
+ text: string;
53
+ }
54
+ >;
55
+ setStreamStatus: FunctionReference<
56
+ "mutation",
57
+ "public",
58
+ {
59
+ status: "pending" | "streaming" | "done" | "error" | "timeout";
60
+ streamId: string;
61
+ },
62
+ any
63
+ >;
64
+ };
65
+ };
66
+ // For now fullApiWithMounts is only fullApi which provides
67
+ // jump-to-definition in component client code.
68
+ // Use Mounts for the same type without the inference.
69
+ declare const fullApiWithMounts: typeof fullApi;
70
+
71
+ export declare const api: FilterApi<
72
+ typeof fullApiWithMounts,
73
+ FunctionReference<any, "public">
74
+ >;
75
+ export declare const internal: FilterApi<
76
+ typeof fullApiWithMounts,
77
+ FunctionReference<any, "internal">
78
+ >;
79
+
80
+ export declare const components: {};
@@ -0,0 +1,23 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import { anyApi, componentsGeneric } from "convex/server";
12
+
13
+ /**
14
+ * A utility for referencing Convex functions in your app's API.
15
+ *
16
+ * Usage:
17
+ * ```js
18
+ * const myFunctionReference = api.myModule.myFunction;
19
+ * ```
20
+ */
21
+ export const api = anyApi;
22
+ export const internal = anyApi;
23
+ export const components = componentsGeneric();