@convex-dev/persistent-text-streaming 0.3.1 → 0.3.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;;;;;;;;;;;;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,EAAE,QAAQ,GAAG,SAAS,EAC9B,IAAI,CAAC,EAAE;IAEL,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,cAkFF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;;;;;;;;;;;;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,EAAE,QAAQ,GAAG,SAAS,EAC9B,IAAI,CAAC,EAAE;IAEL,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,cAuHF"}
@@ -19,9 +19,11 @@ import { useEffect, useMemo, useRef, useState } from "react";
19
19
  * @returns The body and status of the stream.
20
20
  */
21
21
  export function useStream(getPersistentBody, streamUrl, driven, streamId, opts) {
22
+ const [streamBody, setStreamBody] = useState("");
22
23
  const [streamEnded, setStreamEnded] = useState(null);
23
- // Used to prevent strict mode from causing multiple streams to be started.
24
- const streamStarted = useRef(false);
24
+ // Track the active streamId to handle multiple streams and serve as a
25
+ // Strict Mode guard (prevents double-firing when the same streamId is seen).
26
+ const activeStreamRef = useRef(undefined);
25
27
  const usePersistence = useMemo(() => {
26
28
  // Something is wrong with the stream, so we need to use the database value.
27
29
  if (streamEnded === false) {
@@ -34,43 +36,75 @@ export function useStream(getPersistentBody, streamUrl, driven, streamId, opts)
34
36
  // Otherwise, we'll try to drive the stream and use the HTTP response.
35
37
  return false;
36
38
  }, [driven, streamEnded]);
37
- // console.log("usePersistence", usePersistence);
38
39
  const persistentBody = useQuery(getPersistentBody, usePersistence && streamId ? { streamId } : "skip");
39
- const [streamBody, setStreamBody] = useState("");
40
40
  useEffect(() => {
41
- if (driven && streamId && !streamStarted.current) {
42
- // Kick off HTTP action.
43
- void (async () => {
44
- const success = await startStreaming(streamUrl, streamId, (text) => {
45
- setStreamBody((prev) => prev + text);
46
- }, {
47
- ...opts?.headers,
48
- ...(opts?.authToken
49
- ? { Authorization: `Bearer ${opts.authToken}` }
50
- : {}),
51
- });
52
- setStreamEnded(success);
53
- })();
54
- // If we get remounted, we don't want to start a new stream.
55
- return () => {
56
- streamStarted.current = true;
57
- };
41
+ if (!driven || !streamId) {
42
+ return;
58
43
  }
59
- // eslint-disable-next-line react-hooks/exhaustive-deps
60
- }, [
61
- driven,
62
- streamUrl,
63
- streamId,
64
- setStreamEnded,
65
- streamStarted,
66
- opts?.authToken,
67
- ]);
44
+ // Strict Mode guard: don't restart streaming for the same streamId
45
+ if (streamId === activeStreamRef.current) {
46
+ return;
47
+ }
48
+ // New stream: reset state and track the new streamId
49
+ activeStreamRef.current = streamId;
50
+ setStreamBody("");
51
+ setStreamEnded(null);
52
+ const controller = new AbortController();
53
+ void (async () => {
54
+ try {
55
+ const response = await fetch(streamUrl, {
56
+ method: "POST",
57
+ body: JSON.stringify({ streamId }),
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ ...opts?.headers,
61
+ ...(opts?.authToken
62
+ ? { Authorization: `Bearer ${opts.authToken}` }
63
+ : {}),
64
+ },
65
+ signal: controller.signal,
66
+ });
67
+ if (response.status === 205) {
68
+ console.error("Stream already finished", response);
69
+ setStreamEnded(false);
70
+ return;
71
+ }
72
+ if (!response.ok) {
73
+ console.error("Failed to reach streaming endpoint", response);
74
+ setStreamEnded(false);
75
+ return;
76
+ }
77
+ if (!response.body) {
78
+ console.error("No body in response", response);
79
+ setStreamEnded(false);
80
+ return;
81
+ }
82
+ const reader = response.body.getReader();
83
+ const decoder = new TextDecoder();
84
+ for (;;) {
85
+ const { done, value } = await reader.read();
86
+ const text = decoder.decode(value, { stream: !done });
87
+ if (text) {
88
+ setStreamBody((prev) => prev + text);
89
+ }
90
+ if (done) {
91
+ setStreamEnded(true);
92
+ return;
93
+ }
94
+ }
95
+ }
96
+ catch (e) {
97
+ if (!controller.signal.aborted) {
98
+ console.error("Error reading stream", e);
99
+ setStreamEnded(false);
100
+ }
101
+ }
102
+ })();
103
+ return () => {
104
+ controller.abort();
105
+ };
106
+ }, [driven, streamId, streamUrl, opts?.authToken, opts?.headers]);
68
107
  const body = useMemo(() => {
69
- // console.log(
70
- // "body info p vs. s",
71
- // persistentBody?.text?.length ?? 0,
72
- // streamBody.length
73
- //);
74
108
  if (persistentBody) {
75
109
  return persistentBody;
76
110
  }
@@ -88,54 +122,4 @@ export function useStream(getPersistentBody, streamUrl, driven, streamId, opts)
88
122
  }, [persistentBody, streamBody, streamEnded]);
89
123
  return body;
90
124
  }
91
- /**
92
- * Internal helper for starting a stream.
93
- *
94
- * @param url - The URL of the http action that will kick off the stream
95
- * generation and stream the result back to the client using the component's
96
- * `stream` method.
97
- * @param streamId - The ID of the stream.
98
- * @param onUpdate - A function that updates the stream body.
99
- * @returns A promise that resolves to a boolean indicating whether the stream
100
- * was started successfully. It can fail if the http action is not found, or
101
- * CORS fails, or an exception is raised, or the stream is already running
102
- * or finished, etc.
103
- */
104
- async function startStreaming(url, streamId, onUpdate, headers) {
105
- const response = await fetch(url, {
106
- method: "POST",
107
- body: JSON.stringify({
108
- streamId: streamId,
109
- }),
110
- headers: { "Content-Type": "application/json", ...headers },
111
- });
112
- // Adapted from https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
113
- if (response.status === 205) {
114
- console.error("Stream already finished", response);
115
- return false;
116
- }
117
- if (!response.ok) {
118
- console.error("Failed to reach streaming endpoint", response);
119
- return false;
120
- }
121
- if (!response.body) {
122
- console.error("No body in response", response);
123
- return false;
124
- }
125
- const reader = response.body.getReader();
126
- while (true) {
127
- try {
128
- const { done, value } = await reader.read();
129
- if (done) {
130
- onUpdate(new TextDecoder().decode(value));
131
- return true;
132
- }
133
- onUpdate(new TextDecoder().decode(value));
134
- }
135
- catch (e) {
136
- console.error("Error reading stream", e);
137
- return false;
138
- }
139
- }
140
- }
141
125
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "email": "support@convex.dev",
8
8
  "url": "https://github.com/get-convex/persistent-text-streaming/issues"
9
9
  },
10
- "version": "0.3.1",
10
+ "version": "0.3.2",
11
11
  "license": "Apache-2.0",
12
12
  "keywords": [
13
13
  "convex",
@@ -41,10 +41,12 @@ export function useStream(
41
41
  headers?: Record<string, string>;
42
42
  },
43
43
  ) {
44
- const [streamEnded, setStreamEnded] = useState(null as boolean | null);
44
+ const [streamBody, setStreamBody] = useState<string>("");
45
+ const [streamEnded, setStreamEnded] = useState<boolean | null>(null);
45
46
 
46
- // Used to prevent strict mode from causing multiple streams to be started.
47
- const streamStarted = useRef(false);
47
+ // Track the active streamId to handle multiple streams and serve as a
48
+ // Strict Mode guard (prevents double-firing when the same streamId is seen).
49
+ const activeStreamRef = useRef<StreamId | undefined>(undefined);
48
50
 
49
51
  const usePersistence = useMemo(() => {
50
52
  // Something is wrong with the stream, so we need to use the database value.
@@ -58,53 +60,88 @@ export function useStream(
58
60
  // Otherwise, we'll try to drive the stream and use the HTTP response.
59
61
  return false;
60
62
  }, [driven, streamEnded]);
61
- // console.log("usePersistence", usePersistence);
63
+
62
64
  const persistentBody = useQuery(
63
65
  getPersistentBody,
64
66
  usePersistence && streamId ? { streamId } : "skip",
65
67
  );
66
- const [streamBody, setStreamBody] = useState<string>("");
67
68
 
68
69
  useEffect(() => {
69
- if (driven && streamId && !streamStarted.current) {
70
- // Kick off HTTP action.
71
- void (async () => {
72
- const success = await startStreaming(
73
- streamUrl,
74
- streamId,
75
- (text) => {
76
- setStreamBody((prev) => prev + text);
77
- },
78
- {
70
+ if (!driven || !streamId) {
71
+ return;
72
+ }
73
+
74
+ // Strict Mode guard: don't restart streaming for the same streamId
75
+ if (streamId === activeStreamRef.current) {
76
+ return;
77
+ }
78
+
79
+ // New stream: reset state and track the new streamId
80
+ activeStreamRef.current = streamId;
81
+ setStreamBody("");
82
+ setStreamEnded(null);
83
+
84
+ const controller = new AbortController();
85
+
86
+ void (async () => {
87
+ try {
88
+ const response = await fetch(streamUrl, {
89
+ method: "POST",
90
+ body: JSON.stringify({ streamId }),
91
+ headers: {
92
+ "Content-Type": "application/json",
79
93
  ...opts?.headers,
80
94
  ...(opts?.authToken
81
95
  ? { Authorization: `Bearer ${opts.authToken}` }
82
96
  : {}),
83
97
  },
84
- );
85
- setStreamEnded(success);
86
- })();
87
- // If we get remounted, we don't want to start a new stream.
88
- return () => {
89
- streamStarted.current = true;
90
- };
91
- }
92
- // eslint-disable-next-line react-hooks/exhaustive-deps
93
- }, [
94
- driven,
95
- streamUrl,
96
- streamId,
97
- setStreamEnded,
98
- streamStarted,
99
- opts?.authToken,
100
- ]);
98
+ signal: controller.signal,
99
+ });
100
+
101
+ if (response.status === 205) {
102
+ console.error("Stream already finished", response);
103
+ setStreamEnded(false);
104
+ return;
105
+ }
106
+ if (!response.ok) {
107
+ console.error("Failed to reach streaming endpoint", response);
108
+ setStreamEnded(false);
109
+ return;
110
+ }
111
+ if (!response.body) {
112
+ console.error("No body in response", response);
113
+ setStreamEnded(false);
114
+ return;
115
+ }
116
+
117
+ const reader = response.body.getReader();
118
+ const decoder = new TextDecoder();
119
+
120
+ for (;;) {
121
+ const { done, value } = await reader.read();
122
+ const text = decoder.decode(value, { stream: !done });
123
+ if (text) {
124
+ setStreamBody((prev) => prev + text);
125
+ }
126
+ if (done) {
127
+ setStreamEnded(true);
128
+ return;
129
+ }
130
+ }
131
+ } catch (e) {
132
+ if (!controller.signal.aborted) {
133
+ console.error("Error reading stream", e);
134
+ setStreamEnded(false);
135
+ }
136
+ }
137
+ })();
138
+
139
+ return () => {
140
+ controller.abort();
141
+ };
142
+ }, [driven, streamId, streamUrl, opts?.authToken, opts?.headers]);
101
143
 
102
144
  const body = useMemo<StreamBody>(() => {
103
- // console.log(
104
- // "body info p vs. s",
105
- // persistentBody?.text?.length ?? 0,
106
- // streamBody.length
107
- //);
108
145
  if (persistentBody) {
109
146
  return persistentBody;
110
147
  }
@@ -123,57 +160,3 @@ export function useStream(
123
160
  return body;
124
161
  }
125
162
 
126
- /**
127
- * Internal helper for starting a stream.
128
- *
129
- * @param url - The URL of the http action that will kick off the stream
130
- * generation and stream the result back to the client using the component's
131
- * `stream` method.
132
- * @param streamId - The ID of the stream.
133
- * @param onUpdate - A function that updates the stream body.
134
- * @returns A promise that resolves to a boolean indicating whether the stream
135
- * was started successfully. It can fail if the http action is not found, or
136
- * CORS fails, or an exception is raised, or the stream is already running
137
- * or finished, etc.
138
- */
139
- async function startStreaming(
140
- url: URL,
141
- streamId: StreamId,
142
- onUpdate: (text: string) => void,
143
- headers: Record<string, string>,
144
- ) {
145
- const response = await fetch(url, {
146
- method: "POST",
147
- body: JSON.stringify({
148
- streamId: streamId,
149
- }),
150
- headers: { "Content-Type": "application/json", ...headers },
151
- });
152
- // Adapted from https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
153
- if (response.status === 205) {
154
- console.error("Stream already finished", response);
155
- return false;
156
- }
157
- if (!response.ok) {
158
- console.error("Failed to reach streaming endpoint", response);
159
- return false;
160
- }
161
- if (!response.body) {
162
- console.error("No body in response", response);
163
- return false;
164
- }
165
- const reader = response.body.getReader();
166
- while (true) {
167
- try {
168
- const { done, value } = await reader.read();
169
- if (done) {
170
- onUpdate(new TextDecoder().decode(value));
171
- return true;
172
- }
173
- onUpdate(new TextDecoder().decode(value));
174
- } catch (e) {
175
- console.error("Error reading stream", e);
176
- return false;
177
- }
178
- }
179
- }