@@ -15,7 +15,7 @@ import {
15
15
createSuspensedChunk ,
16
16
consumeStream ,
17
17
} from './utils' ;
18
- import type { StreamableValue } from './types' ;
18
+ import type { StreamablePatch , StreamableValue } from './types' ;
19
19
20
20
/**
21
21
* Create a piece of changable UI that can be streamed to the client.
@@ -48,7 +48,13 @@ export function createStreamableUI(initialValue?: React.ReactNode) {
48
48
warnUnclosedStream ( ) ;
49
49
50
50
return {
51
+ /**
52
+ * The value of the streamable UI. This can be returned from a Server Action and received by the client.
53
+ */
51
54
value : row ,
55
+ /**
56
+ * This method updates the current UI node. It takes a new UI node and replaces the old one.
57
+ */
52
58
update ( value : React . ReactNode ) {
53
59
assertStream ( '.update()' ) ;
54
60
@@ -67,6 +73,22 @@ export function createStreamableUI(initialValue?: React.ReactNode) {
67
73
68
74
warnUnclosedStream ( ) ;
69
75
} ,
76
+ /**
77
+ * This method is used to append a new UI node to the end of the old one.
78
+ * Once appended a new UI node, the previous UI node cannot be updated anymore.
79
+ *
80
+ * @example
81
+ * ```jsx
82
+ * const ui = createStreamableUI(<div>hello</div>)
83
+ * ui.append(<div>world</div>)
84
+ *
85
+ * // The UI node will be:
86
+ * // <>
87
+ * // <div>hello</div>
88
+ * // <div>world</div>
89
+ * // </>
90
+ * ```
91
+ */
70
92
append ( value : React . ReactNode ) {
71
93
assertStream ( '.append()' ) ;
72
94
@@ -79,6 +101,10 @@ export function createStreamableUI(initialValue?: React.ReactNode) {
79
101
80
102
warnUnclosedStream ( ) ;
81
103
} ,
104
+ /**
105
+ * This method is used to signal that there is an error in the UI stream.
106
+ * It will be thrown on the client side and caught by the nearest error boundary component.
107
+ */
82
108
error ( error : any ) {
83
109
assertStream ( '.error()' ) ;
84
110
@@ -88,6 +114,12 @@ export function createStreamableUI(initialValue?: React.ReactNode) {
88
114
closed = true ;
89
115
reject ( error ) ;
90
116
} ,
117
+ /**
118
+ * 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.
119
+ * Once called, the UI node cannot be updated or appended anymore.
120
+ *
121
+ * This method is always **required** to be called, otherwise the response will be stuck in a loading state.
122
+ */
91
123
done ( ...args : [ ] | [ React . ReactNode ] ) {
92
124
assertStream ( '.done()' ) ;
93
125
@@ -116,6 +148,7 @@ export function createStreamableValue<T = any, E = any>(initialValue?: T) {
116
148
let currentError : E | undefined ;
117
149
let currentPromise : typeof resolvable . promise | undefined =
118
150
resolvable . promise ;
151
+ let currentPatchValue : StreamablePatch ;
119
152
120
153
function assertStream ( method : string ) {
121
154
if ( closed ) {
@@ -138,35 +171,63 @@ export function createStreamableValue<T = any, E = any>(initialValue?: T) {
138
171
}
139
172
warnUnclosedStream ( ) ;
140
173
141
- function createWrapped ( withType ?: boolean ) : StreamableValue < T , E > {
174
+ function createWrapped ( initialChunk ?: boolean ) : StreamableValue < T , E > {
142
175
// This makes the payload much smaller if there're mutative updates before the first read.
143
- const init : Partial < StreamableValue < T , E > > =
144
- currentError === undefined
145
- ? { curr : currentValue }
146
- : { error : currentError } ;
176
+ let init : Partial < StreamableValue < T , E > > ;
177
+
178
+ if ( currentError !== undefined ) {
179
+ init = { error : currentError } ;
180
+ } else {
181
+ if ( currentPatchValue && ! initialChunk ) {
182
+ init = { diff : currentPatchValue } ;
183
+ } else {
184
+ init = { curr : currentValue } ;
185
+ }
186
+ }
147
187
148
188
if ( currentPromise ) {
149
189
init . next = currentPromise ;
150
190
}
151
191
152
- if ( withType ) {
192
+ if ( initialChunk ) {
153
193
init . type = STREAMABLE_VALUE_TYPE ;
154
194
}
155
195
156
196
return init ;
157
197
}
158
198
199
+ // Update the internal `currentValue` and `currentPatchValue` if needed.
200
+ function updateValueStates ( value : T ) {
201
+ // If we can only send a patch over the wire, it's better to do so.
202
+ currentPatchValue = undefined ;
203
+ if ( typeof value === 'string' ) {
204
+ if ( typeof currentValue === 'string' ) {
205
+ if ( value . startsWith ( currentValue ) ) {
206
+ currentPatchValue = [ 0 , value . slice ( currentValue . length ) ] ;
207
+ }
208
+ }
209
+ }
210
+
211
+ currentValue = value ;
212
+ }
213
+
159
214
return {
215
+ /**
216
+ * The value of the streamable. This can be returned from a Server Action and received by the client.
217
+ */
160
218
get value ( ) {
161
219
return createWrapped ( true ) ;
162
220
} ,
221
+ /**
222
+ * This method updates the current value with a new one.
223
+ */
163
224
update ( value : T ) {
164
225
assertStream ( '.update()' ) ;
165
226
166
227
const resolvePrevious = resolvable . resolve ;
167
228
resolvable = createResolvablePromise ( ) ;
168
229
169
- currentValue = value ;
230
+ updateValueStates ( value ) ;
170
231
currentPromise = resolvable . promise ;
171
232
resolvePrevious ( createWrapped ( ) ) ;
172
233
@@ -194,8 +255,8 @@ export function createStreamableValue<T = any, E = any>(initialValue?: T) {
194
255
currentPromise = undefined ;
195
256
196
257
if ( args . length ) {
197
- currentValue = args [ 0 ] ;
198
- resolvable . resolve ( { curr : args [ 0 ] } ) ;
258
+ updateValueStates ( args [ 0 ] ) ;
259
+ resolvable . resolve ( createWrapped ( ) ) ;
199
260
return ;
200
261
}
201
262
0 commit comments