@@ -6,7 +6,10 @@ import zodToJsonSchema from 'zod-to-json-schema';
6
6
// TODO: This needs to be externalized.
7
7
import { OpenAIStream } from '../streams' ;
8
8
9
- import { STREAMABLE_VALUE_TYPE } from './constants' ;
9
+ import {
10
+ STREAMABLE_VALUE_TYPE ,
11
+ DEV_DEFAULT_STREAMABLE_WARNING_TIME ,
12
+ } from './constants' ;
10
13
import {
11
14
createResolvablePromise ,
12
15
createSuspensedChunk ,
@@ -22,26 +25,43 @@ export function createStreamableUI(initialValue?: React.ReactNode) {
22
25
let closed = false ;
23
26
let { row, resolve, reject } = createSuspensedChunk ( initialValue ) ;
24
27
25
- function assertStream ( ) {
28
+ function assertStream ( method : string ) {
26
29
if ( closed ) {
27
- throw new Error ( ' UI stream is already closed.') ;
30
+ throw new Error ( method + ': UI stream is already closed.') ;
28
31
}
29
32
}
30
33
34
+ let warningTimeout : NodeJS . Timeout | undefined ;
35
+ function warnUnclosedStream ( ) {
36
+ if ( process . env . NODE_ENV === 'development' ) {
37
+ if ( warningTimeout ) {
38
+ clearTimeout ( warningTimeout ) ;
39
+ }
40
+ warningTimeout = setTimeout ( ( ) => {
41
+ console . warn (
42
+ 'The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.' ,
43
+ ) ;
44
+ } , DEV_DEFAULT_STREAMABLE_WARNING_TIME ) ;
45
+ }
46
+ }
47
+ warnUnclosedStream ( ) ;
48
+
31
49
return {
32
50
value : row ,
33
51
update ( value : React . ReactNode ) {
34
- assertStream ( ) ;
52
+ assertStream ( '.update()' ) ;
35
53
36
54
const resolvable = createResolvablePromise ( ) ;
37
55
currentValue = value ;
38
56
39
57
resolve ( { value : currentValue , done : false , next : resolvable . promise } ) ;
40
58
resolve = resolvable . resolve ;
41
59
reject = resolvable . reject ;
60
+
61
+ warnUnclosedStream ( ) ;
42
62
} ,
43
63
append ( value : React . ReactNode ) {
44
- assertStream ( ) ;
64
+ assertStream ( '.append()' ) ;
45
65
46
66
const resolvable = createResolvablePromise ( ) ;
47
67
if ( typeof currentValue === 'string' && typeof value === 'string' ) {
@@ -58,16 +78,24 @@ export function createStreamableUI(initialValue?: React.ReactNode) {
58
78
resolve ( { value : currentValue , done : false , next : resolvable . promise } ) ;
59
79
resolve = resolvable . resolve ;
60
80
reject = resolvable . reject ;
81
+
82
+ warnUnclosedStream ( ) ;
61
83
} ,
62
84
error ( error : any ) {
63
- assertStream ( ) ;
85
+ assertStream ( '.error()' ) ;
64
86
87
+ if ( warningTimeout ) {
88
+ clearTimeout ( warningTimeout ) ;
89
+ }
65
90
closed = true ;
66
91
reject ( error ) ;
67
92
} ,
68
93
done ( ...args : any ) {
69
- assertStream ( ) ;
94
+ assertStream ( '.done()' ) ;
70
95
96
+ if ( warningTimeout ) {
97
+ clearTimeout ( warningTimeout ) ;
98
+ }
71
99
closed = true ;
72
100
if ( args . length ) {
73
101
resolve ( { value : args [ 0 ] , done : true } ) ;
@@ -83,15 +111,29 @@ export function createStreamableUI(initialValue?: React.ReactNode) {
83
111
* On the client side, the value can be accessed via the useStreamableValue() hook.
84
112
*/
85
113
export function createStreamableValue < T = any > ( initialValue ?: T ) {
86
- // let currentValue = initialValue
87
114
let closed = false ;
88
115
let { promise, resolve, reject } = createResolvablePromise ( ) ;
89
116
90
- function assertStream ( ) {
117
+ function assertStream ( method : string ) {
91
118
if ( closed ) {
92
- throw new Error ( 'Value stream is already closed.' ) ;
119
+ throw new Error ( method + ': Value stream is already closed.' ) ;
120
+ }
121
+ }
122
+
123
+ let warningTimeout : NodeJS . Timeout | undefined ;
124
+ function warnUnclosedStream ( ) {
125
+ if ( process . env . NODE_ENV === 'development' ) {
126
+ if ( warningTimeout ) {
127
+ clearTimeout ( warningTimeout ) ;
128
+ }
129
+ warningTimeout = setTimeout ( ( ) => {
130
+ console . warn (
131
+ 'The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.' ,
132
+ ) ;
133
+ } , DEV_DEFAULT_STREAMABLE_WARNING_TIME ) ;
93
134
}
94
135
}
136
+ warnUnclosedStream ( ) ;
95
137
96
138
function createWrapped ( val : T | undefined , initial ?: boolean ) {
97
139
if ( initial ) {
@@ -111,7 +153,7 @@ export function createStreamableValue<T = any>(initialValue?: T) {
111
153
return {
112
154
value : createWrapped ( initialValue , true ) ,
113
155
update ( value : T ) {
114
- assertStream ( ) ;
156
+ assertStream ( '.update()' ) ;
115
157
116
158
const resolvePrevious = resolve ;
117
159
const resolvable = createResolvablePromise ( ) ;
@@ -121,17 +163,23 @@ export function createStreamableValue<T = any>(initialValue?: T) {
121
163
122
164
resolvePrevious ( createWrapped ( value ) ) ;
123
165
124
- // currentValue = value
166
+ warnUnclosedStream ( ) ;
125
167
} ,
126
168
error ( error : any ) {
127
- assertStream ( ) ;
169
+ assertStream ( '.error()' ) ;
128
170
171
+ if ( warningTimeout ) {
172
+ clearTimeout ( warningTimeout ) ;
173
+ }
129
174
closed = true ;
130
175
reject ( error ) ;
131
176
} ,
132
177
done ( ...args : any ) {
133
- assertStream ( ) ;
178
+ assertStream ( '.done()' ) ;
134
179
180
+ if ( warningTimeout ) {
181
+ clearTimeout ( warningTimeout ) ;
182
+ }
135
183
closed = true ;
136
184
137
185
if ( args . length ) {
0 commit comments