@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.
- package/LICENSE +201 -0
- package/README.md +188 -0
- package/dist/commonjs/client/index.d.ts +89 -0
- package/dist/commonjs/client/index.d.ts.map +1 -0
- package/dist/commonjs/client/index.js +133 -0
- package/dist/commonjs/client/index.js.map +1 -0
- package/dist/commonjs/component/_generated/api.d.ts +14 -0
- package/dist/commonjs/component/_generated/api.d.ts.map +1 -0
- package/dist/commonjs/component/_generated/api.js +22 -0
- package/dist/commonjs/component/_generated/api.js.map +1 -0
- package/dist/commonjs/component/_generated/server.d.ts +64 -0
- package/dist/commonjs/component/_generated/server.d.ts.map +1 -0
- package/dist/commonjs/component/_generated/server.js +74 -0
- package/dist/commonjs/component/_generated/server.js.map +1 -0
- package/dist/commonjs/component/convex.config.d.ts +3 -0
- package/dist/commonjs/component/convex.config.d.ts.map +1 -0
- package/dist/commonjs/component/convex.config.js +3 -0
- package/dist/commonjs/component/convex.config.js.map +1 -0
- package/dist/commonjs/component/crons.d.ts +3 -0
- package/dist/commonjs/component/crons.d.ts.map +1 -0
- package/dist/commonjs/component/crons.js +7 -0
- package/dist/commonjs/component/crons.js.map +1 -0
- package/dist/commonjs/component/lib.d.ts +21 -0
- package/dist/commonjs/component/lib.d.ts.map +1 -0
- package/dist/commonjs/component/lib.js +134 -0
- package/dist/commonjs/component/lib.js.map +1 -0
- package/dist/commonjs/component/schema.d.ts +23 -0
- package/dist/commonjs/component/schema.d.ts.map +1 -0
- package/dist/commonjs/component/schema.js +13 -0
- package/dist/commonjs/component/schema.js.map +1 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/commonjs/react/index.d.ts +23 -0
- package/dist/commonjs/react/index.d.ts.map +1 -0
- package/dist/commonjs/react/index.js +131 -0
- package/dist/commonjs/react/index.js.map +1 -0
- package/dist/esm/client/index.d.ts +89 -0
- package/dist/esm/client/index.d.ts.map +1 -0
- package/dist/esm/client/index.js +133 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/component/_generated/api.d.ts +14 -0
- package/dist/esm/component/_generated/api.d.ts.map +1 -0
- package/dist/esm/component/_generated/api.js +22 -0
- package/dist/esm/component/_generated/api.js.map +1 -0
- package/dist/esm/component/_generated/server.d.ts +64 -0
- package/dist/esm/component/_generated/server.d.ts.map +1 -0
- package/dist/esm/component/_generated/server.js +74 -0
- package/dist/esm/component/_generated/server.js.map +1 -0
- package/dist/esm/component/convex.config.d.ts +3 -0
- package/dist/esm/component/convex.config.d.ts.map +1 -0
- package/dist/esm/component/convex.config.js +3 -0
- package/dist/esm/component/convex.config.js.map +1 -0
- package/dist/esm/component/crons.d.ts +3 -0
- package/dist/esm/component/crons.d.ts.map +1 -0
- package/dist/esm/component/crons.js +7 -0
- package/dist/esm/component/crons.js.map +1 -0
- package/dist/esm/component/lib.d.ts +21 -0
- package/dist/esm/component/lib.d.ts.map +1 -0
- package/dist/esm/component/lib.js +134 -0
- package/dist/esm/component/lib.js.map +1 -0
- package/dist/esm/component/schema.d.ts +23 -0
- package/dist/esm/component/schema.d.ts.map +1 -0
- package/dist/esm/component/schema.js +13 -0
- package/dist/esm/component/schema.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/react/index.d.ts +23 -0
- package/dist/esm/react/index.d.ts.map +1 -0
- package/dist/esm/react/index.js +131 -0
- package/dist/esm/react/index.js.map +1 -0
- package/package.json +89 -0
- package/react/package.json +5 -0
- package/src/client/index.ts +223 -0
- package/src/component/_generated/api.d.ts +80 -0
- package/src/component/_generated/api.js +23 -0
- package/src/component/_generated/dataModel.d.ts +60 -0
- package/src/component/_generated/server.d.ts +149 -0
- package/src/component/_generated/server.js +90 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/crons.ts +13 -0
- package/src/component/lib.test.ts +9 -0
- package/src/component/lib.ts +149 -0
- package/src/component/schema.ts +21 -0
- 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,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,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();
|