Skip to content

Commit f9dee8a

Browse files
shudinglgrammel
andauthoredMay 29, 2024··
fix(ai/rsc): Fix types for createStreamableValue and createStreamableUI (#1735)
Co-authored-by: Lars Grammel <lars.grammel@gmail.com>
1 parent 171bced commit f9dee8a

File tree

4 files changed

+110
-74
lines changed

4 files changed

+110
-74
lines changed
 

‎.changeset/kind-islands-watch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'ai': patch
3+
---
4+
5+
fix(ai/rsc): Fix types for createStreamableValue and createStreamableUI

‎packages/core/rsc/streamable.tsx

+101-71
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,52 @@ import {
1717
} from './utils';
1818
import type { StreamablePatch, StreamableValue } from './types';
1919

20+
// It's necessary to define the type manually here, otherwise TypeScript compiler
21+
// will not be able to infer the correct return type as it's circular.
22+
type StreamableUIWrapper = {
23+
/**
24+
* The value of the streamable UI. This can be returned from a Server Action and received by the client.
25+
*/
26+
readonly value: React.ReactNode;
27+
28+
/**
29+
* This method updates the current UI node. It takes a new UI node and replaces the old one.
30+
*/
31+
update(value: React.ReactNode): StreamableUIWrapper;
32+
33+
/**
34+
* This method is used to append a new UI node to the end of the old one.
35+
* Once appended a new UI node, the previous UI node cannot be updated anymore.
36+
*
37+
* @example
38+
* ```jsx
39+
* const ui = createStreamableUI(<div>hello</div>)
40+
* ui.append(<div>world</div>)
41+
*
42+
* // The UI node will be:
43+
* // <>
44+
* // <div>hello</div>
45+
* // <div>world</div>
46+
* // </>
47+
* ```
48+
*/
49+
append(value: React.ReactNode): StreamableUIWrapper;
50+
51+
/**
52+
* This method is used to signal that there is an error in the UI stream.
53+
* It will be thrown on the client side and caught by the nearest error boundary component.
54+
*/
55+
error(error: any): StreamableUIWrapper;
56+
57+
/**
58+
* This method marks the UI node as finalized. You can either call it without any parameters or with a new UI node as the final state.
59+
* Once called, the UI node cannot be updated or appended anymore.
60+
*
61+
* This method is always **required** to be called, otherwise the response will be stuck in a loading state.
62+
*/
63+
done(...args: [React.ReactNode] | []): StreamableUIWrapper;
64+
};
65+
2066
/**
2167
* Create a piece of changable UI that can be streamed to the client.
2268
* On the client side, it can be rendered as a normal React node.
@@ -47,14 +93,8 @@ function createStreamableUI(initialValue?: React.ReactNode) {
4793
}
4894
warnUnclosedStream();
4995

50-
const streamable = {
51-
/**
52-
* The value of the streamable UI. This can be returned from a Server Action and received by the client.
53-
*/
96+
const streamable: StreamableUIWrapper = {
5497
value: row,
55-
/**
56-
* This method updates the current UI node. It takes a new UI node and replaces the old one.
57-
*/
5898
update(value: React.ReactNode) {
5999
assertStream('.update()');
60100

@@ -75,22 +115,6 @@ function createStreamableUI(initialValue?: React.ReactNode) {
75115

76116
return streamable;
77117
},
78-
/**
79-
* This method is used to append a new UI node to the end of the old one.
80-
* Once appended a new UI node, the previous UI node cannot be updated anymore.
81-
*
82-
* @example
83-
* ```jsx
84-
* const ui = createStreamableUI(<div>hello</div>)
85-
* ui.append(<div>world</div>)
86-
*
87-
* // The UI node will be:
88-
* // <>
89-
* // <div>hello</div>
90-
* // <div>world</div>
91-
* // </>
92-
* ```
93-
*/
94118
append(value: React.ReactNode) {
95119
assertStream('.append()');
96120

@@ -105,10 +129,6 @@ function createStreamableUI(initialValue?: React.ReactNode) {
105129

106130
return streamable;
107131
},
108-
/**
109-
* This method is used to signal that there is an error in the UI stream.
110-
* It will be thrown on the client side and caught by the nearest error boundary component.
111-
*/
112132
error(error: any) {
113133
assertStream('.error()');
114134

@@ -120,12 +140,6 @@ function createStreamableUI(initialValue?: React.ReactNode) {
120140

121141
return streamable;
122142
},
123-
/**
124-
* This method marks the UI node as finalized. You can either call it without any parameters or with a new UI node as the final state.
125-
* Once called, the UI node cannot be updated or appended anymore.
126-
*
127-
* This method is always **required** to be called, otherwise the response will be stuck in a loading state.
128-
*/
129143
done(...args: [] | [React.ReactNode]) {
130144
assertStream('.done()');
131145

@@ -209,6 +223,59 @@ function createStreamableValue<T = any, E = any>(
209223
return streamableValue;
210224
}
211225

226+
// It's necessary to define the type manually here, otherwise TypeScript compiler
227+
// will not be able to infer the correct return type as it's circular.
228+
type StreamableValueWrapper<T, E> = {
229+
/**
230+
* The value of the streamable. This can be returned from a Server Action and
231+
* received by the client. To read the streamed values, use the
232+
* `readStreamableValue` or `useStreamableValue` APIs.
233+
*/
234+
readonly value: StreamableValue<T, E>;
235+
236+
/**
237+
* This method updates the current value with a new one.
238+
*/
239+
update(value: T): StreamableValueWrapper<T, E>;
240+
241+
/**
242+
* This method is used to append a delta string to the current value. It
243+
* requires the current value of the streamable to be a string.
244+
*
245+
* @example
246+
* ```jsx
247+
* const streamable = createStreamableValue('hello');
248+
* streamable.append(' world');
249+
*
250+
* // The value will be 'hello world'
251+
* ```
252+
*/
253+
append(value: T): StreamableValueWrapper<T, E>;
254+
255+
/**
256+
* This method is used to signal that there is an error in the value stream.
257+
* It will be thrown on the client side when consumed via
258+
* `readStreamableValue` or `useStreamableValue`.
259+
*/
260+
error(error: any): StreamableValueWrapper<T, E>;
261+
262+
/**
263+
* This method marks the value as finalized. You can either call it without
264+
* any parameters or with a new value as the final state.
265+
* Once called, the value cannot be updated or appended anymore.
266+
*
267+
* This method is always **required** to be called, otherwise the response
268+
* will be stuck in a loading state.
269+
*/
270+
done(...args: [T] | []): StreamableValueWrapper<T, E>;
271+
272+
/**
273+
* @internal This is an internal lock to prevent the value from being
274+
* updated by the user.
275+
*/
276+
[STREAMABLE_VALUE_INTERNAL_LOCK]: boolean;
277+
};
278+
212279
function createStreamableValueImpl<T = any, E = any>(initialValue?: T) {
213280
let closed = false;
214281
let locked = false;
@@ -286,25 +353,13 @@ function createStreamableValueImpl<T = any, E = any>(initialValue?: T) {
286353
currentValue = value;
287354
}
288355

289-
const streamable = {
290-
/**
291-
* @internal This is an internal lock to prevent the value from being
292-
* updated by the user.
293-
*/
356+
const streamable: StreamableValueWrapper<T, E> = {
294357
set [STREAMABLE_VALUE_INTERNAL_LOCK](state: boolean) {
295358
locked = state;
296359
},
297-
/**
298-
* The value of the streamable. This can be returned from a Server Action and
299-
* received by the client. To read the streamed values, use the
300-
* `readStreamableValue` or `useStreamableValue` APIs.
301-
*/
302360
get value() {
303361
return createWrapped(true);
304362
},
305-
/**
306-
* This method updates the current value with a new one.
307-
*/
308363
update(value: T) {
309364
assertStream('.update()');
310365

@@ -319,18 +374,6 @@ function createStreamableValueImpl<T = any, E = any>(initialValue?: T) {
319374

320375
return streamable;
321376
},
322-
/**
323-
* This method is used to append a delta string to the current value. It
324-
* requires the current value of the streamable to be a string.
325-
*
326-
* @example
327-
* ```jsx
328-
* const streamable = createStreamableValue('hello');
329-
* streamable.append(' world');
330-
*
331-
* // The value will be 'hello world'
332-
* ```
333-
*/
334377
append(value: T) {
335378
assertStream('.append()');
336379

@@ -366,11 +409,6 @@ function createStreamableValueImpl<T = any, E = any>(initialValue?: T) {
366409

367410
return streamable;
368411
},
369-
/**
370-
* This method is used to signal that there is an error in the value stream.
371-
* It will be thrown on the client side when consumed via
372-
* `readStreamableValue` or `useStreamableValue`.
373-
*/
374412
error(error: any) {
375413
assertStream('.error()');
376414

@@ -385,14 +423,6 @@ function createStreamableValueImpl<T = any, E = any>(initialValue?: T) {
385423

386424
return streamable;
387425
},
388-
/**
389-
* This method marks the value as finalized. You can either call it without
390-
* any parameters or with a new value as the final state.
391-
* Once called, the value cannot be updated or appended anymore.
392-
*
393-
* This method is always **required** to be called, otherwise the response
394-
* will be stuck in a loading state.
395-
*/
396426
done(...args: [] | [T]) {
397427
assertStream('.done()');
398428

‎packages/core/rsc/streamable.ui.test.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ async function recursiveResolve(val: any): Promise<any> {
103103
return val;
104104
}
105105

106-
async function simulateFlightServerRender(node: React.ReactElement) {
106+
async function simulateFlightServerRender(node: React.ReactNode) {
107107
async function traverse(node: any): Promise<any> {
108108
if (!node) return {};
109109

@@ -318,7 +318,8 @@ describe('rsc - createStreamableUI()', () => {
318318
ui.append(<div>2</div>);
319319
ui.append(<div>3</div>);
320320

321-
const currentRsolved = ui.value.props.children.props.n;
321+
const currentRsolved = (ui.value as React.ReactElement).props.children.props
322+
.n;
322323
const tryResolve1 = await Promise.race([currentRsolved, nextTick()]);
323324
expect(tryResolve1).toBeDefined();
324325
const tryResolve2 = await Promise.race([tryResolve1.next, nextTick()]);

‎packages/core/rsc/utils.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function createSuspensedChunk(initialValue: React.ReactNode) {
5757
<Suspense fallback={initialValue}>
5858
<R c={initialValue} n={promise} />
5959
</Suspense>
60-
),
60+
) as React.ReactNode,
6161
resolve,
6262
reject,
6363
};

0 commit comments

Comments
 (0)
Please sign in to comment.