@ai-sdk/rsc 2.0.47 → 2.0.49
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/CHANGELOG.md +17 -0
- package/package.json +9 -4
- package/src/ai-state.test.ts +0 -146
- package/src/stream-ui/__snapshots__/render.ui.test.tsx.snap +0 -91
- package/src/stream-ui/__snapshots__/stream-ui.ui.test.tsx.snap +0 -213
- package/src/stream-ui/stream-ui.ui.test.tsx +0 -321
- package/src/streamable-ui/create-streamable-ui.ui.test.tsx +0 -354
- package/src/streamable-value/create-streamable-value.test.tsx +0 -179
- package/src/streamable-value/read-streamable-value.ui.test.tsx +0 -165
- package/src/types.test-d.ts +0 -17
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
import { LanguageModelV3Usage } from '@ai-sdk/provider';
|
|
2
|
-
import { delay } from '@ai-sdk/provider-utils';
|
|
3
|
-
import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test';
|
|
4
|
-
import { asLanguageModelUsage } from 'ai/internal';
|
|
5
|
-
import { MockLanguageModelV3 } from 'ai/test';
|
|
6
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
-
import { z } from 'zod/v4';
|
|
8
|
-
import { streamUI } from './stream-ui';
|
|
9
|
-
|
|
10
|
-
async function recursiveResolve(val: any): Promise<any> {
|
|
11
|
-
if (val && typeof val === 'object' && typeof val.then === 'function') {
|
|
12
|
-
return await recursiveResolve(await val);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (Array.isArray(val)) {
|
|
16
|
-
return await Promise.all(val.map(recursiveResolve));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (val && typeof val === 'object') {
|
|
20
|
-
const result: any = {};
|
|
21
|
-
for (const key in val) {
|
|
22
|
-
result[key] = await recursiveResolve(val[key]);
|
|
23
|
-
}
|
|
24
|
-
return result;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return val;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function simulateFlightServerRender(node: React.ReactNode) {
|
|
31
|
-
async function traverse(node: any): Promise<any> {
|
|
32
|
-
if (!node) return {};
|
|
33
|
-
|
|
34
|
-
// Let's only do one level of promise resolution here. As it's only for testing purposes.
|
|
35
|
-
const props = await recursiveResolve({ ...node.props });
|
|
36
|
-
|
|
37
|
-
const { type } = node;
|
|
38
|
-
const { children, ...otherProps } = props;
|
|
39
|
-
const typeName = typeof type === 'function' ? type.name : String(type);
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
type: typeName,
|
|
43
|
-
props: otherProps,
|
|
44
|
-
children:
|
|
45
|
-
typeof children === 'string'
|
|
46
|
-
? children
|
|
47
|
-
: Array.isArray(children)
|
|
48
|
-
? children.map(traverse)
|
|
49
|
-
: await traverse(children),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return traverse(node);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const testUsage: LanguageModelV3Usage = {
|
|
57
|
-
inputTokens: {
|
|
58
|
-
total: 3,
|
|
59
|
-
noCache: 3,
|
|
60
|
-
cacheRead: 0,
|
|
61
|
-
cacheWrite: 0,
|
|
62
|
-
},
|
|
63
|
-
outputTokens: {
|
|
64
|
-
total: 10,
|
|
65
|
-
text: 10,
|
|
66
|
-
reasoning: 0,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const mockTextModel = new MockLanguageModelV3({
|
|
71
|
-
doStream: async () => {
|
|
72
|
-
return {
|
|
73
|
-
stream: convertArrayToReadableStream([
|
|
74
|
-
{ type: 'text-start', id: '0' },
|
|
75
|
-
{ type: 'text-delta', id: '0', delta: '{ ' },
|
|
76
|
-
{ type: 'text-delta', id: '0', delta: '"content": ' },
|
|
77
|
-
{ type: 'text-delta', id: '0', delta: `"Hello, ` },
|
|
78
|
-
{ type: 'text-delta', id: '0', delta: `world` },
|
|
79
|
-
{ type: 'text-delta', id: '0', delta: `!"` },
|
|
80
|
-
{ type: 'text-delta', id: '0', delta: ' }' },
|
|
81
|
-
{ type: 'text-end', id: '0' },
|
|
82
|
-
{
|
|
83
|
-
type: 'finish',
|
|
84
|
-
finishReason: { unified: 'stop', raw: 'stop' },
|
|
85
|
-
usage: testUsage,
|
|
86
|
-
},
|
|
87
|
-
]),
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const mockToolModel = new MockLanguageModelV3({
|
|
93
|
-
doStream: async () => {
|
|
94
|
-
return {
|
|
95
|
-
stream: convertArrayToReadableStream([
|
|
96
|
-
{
|
|
97
|
-
type: 'tool-call',
|
|
98
|
-
toolCallType: 'function',
|
|
99
|
-
toolCallId: 'call-1',
|
|
100
|
-
toolName: 'tool1',
|
|
101
|
-
input: `{ "value": "value" }`,
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
type: 'finish',
|
|
105
|
-
finishReason: { unified: 'stop', raw: 'stop' },
|
|
106
|
-
usage: testUsage,
|
|
107
|
-
},
|
|
108
|
-
]),
|
|
109
|
-
};
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('result.value', () => {
|
|
114
|
-
it('should render text', async () => {
|
|
115
|
-
const result = await streamUI({
|
|
116
|
-
model: mockTextModel,
|
|
117
|
-
prompt: '',
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const rendered = await simulateFlightServerRender(result.value);
|
|
121
|
-
expect(rendered).toMatchSnapshot();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should render text function returned ui', async () => {
|
|
125
|
-
const result = await streamUI({
|
|
126
|
-
model: mockTextModel,
|
|
127
|
-
prompt: '',
|
|
128
|
-
text: ({ content }) => <h1>{content}</h1>,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const rendered = await simulateFlightServerRender(result.value);
|
|
132
|
-
expect(rendered).toMatchSnapshot();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should render tool call results', async () => {
|
|
136
|
-
const result = await streamUI({
|
|
137
|
-
model: mockToolModel,
|
|
138
|
-
prompt: '',
|
|
139
|
-
tools: {
|
|
140
|
-
tool1: {
|
|
141
|
-
description: 'test tool 1',
|
|
142
|
-
inputSchema: z.object({
|
|
143
|
-
value: z.string(),
|
|
144
|
-
}),
|
|
145
|
-
generate: async ({ value }) => {
|
|
146
|
-
await delay(100);
|
|
147
|
-
return <div>tool1: {value}</div>;
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const rendered = await simulateFlightServerRender(result.value);
|
|
154
|
-
expect(rendered).toMatchSnapshot();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should render tool call results with generator render function', async () => {
|
|
158
|
-
const result = await streamUI({
|
|
159
|
-
model: mockToolModel,
|
|
160
|
-
prompt: '',
|
|
161
|
-
tools: {
|
|
162
|
-
tool1: {
|
|
163
|
-
description: 'test tool 1',
|
|
164
|
-
inputSchema: z.object({
|
|
165
|
-
value: z.string(),
|
|
166
|
-
}),
|
|
167
|
-
generate: async function* ({ value }) {
|
|
168
|
-
yield <div>Loading...</div>;
|
|
169
|
-
await delay(100);
|
|
170
|
-
return <div>tool: {value}</div>;
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const rendered = await simulateFlightServerRender(result.value);
|
|
177
|
-
expect(rendered).toMatchSnapshot();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should show better error messages if legacy options are passed', async () => {
|
|
181
|
-
try {
|
|
182
|
-
await streamUI({
|
|
183
|
-
model: mockToolModel,
|
|
184
|
-
prompt: '',
|
|
185
|
-
tools: {
|
|
186
|
-
tool1: {
|
|
187
|
-
description: 'test tool 1',
|
|
188
|
-
inputSchema: z.object({
|
|
189
|
-
value: z.string(),
|
|
190
|
-
}),
|
|
191
|
-
// @ts-expect-error
|
|
192
|
-
render: async function* () {},
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
});
|
|
196
|
-
} catch (e) {
|
|
197
|
-
expect(e).toMatchSnapshot();
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe('rsc - streamUI() onFinish callback', () => {
|
|
203
|
-
let result: Parameters<
|
|
204
|
-
Required<Parameters<typeof streamUI>[0]>['onFinish']
|
|
205
|
-
>[0];
|
|
206
|
-
|
|
207
|
-
beforeEach(async () => {
|
|
208
|
-
const ui = await streamUI({
|
|
209
|
-
model: mockToolModel,
|
|
210
|
-
prompt: '',
|
|
211
|
-
tools: {
|
|
212
|
-
tool1: {
|
|
213
|
-
description: 'test tool 1',
|
|
214
|
-
inputSchema: z.object({
|
|
215
|
-
value: z.string(),
|
|
216
|
-
}),
|
|
217
|
-
generate: async ({ value }) => {
|
|
218
|
-
await delay(100);
|
|
219
|
-
return <div>tool1: {value}</div>;
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
onFinish: event => {
|
|
224
|
-
result = event;
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// consume stream
|
|
229
|
-
await simulateFlightServerRender(ui.value);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should contain token usage', () => {
|
|
233
|
-
expect(result.usage).toStrictEqual(asLanguageModelUsage(testUsage));
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('should contain finish reason', async () => {
|
|
237
|
-
expect(result.finishReason).toBe('stop');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should contain final React node', async () => {
|
|
241
|
-
expect(result.value).toMatchSnapshot();
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe('options.headers', () => {
|
|
246
|
-
it('should pass headers to model', async () => {
|
|
247
|
-
const result = await streamUI({
|
|
248
|
-
model: new MockLanguageModelV3({
|
|
249
|
-
doStream: async ({ headers }) => {
|
|
250
|
-
expect(headers).toStrictEqual({
|
|
251
|
-
'custom-request-header': 'request-header-value',
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
stream: convertArrayToReadableStream([
|
|
256
|
-
{ type: 'text-start', id: '0' },
|
|
257
|
-
{
|
|
258
|
-
type: 'text-delta',
|
|
259
|
-
id: '0',
|
|
260
|
-
delta: '{ "content": "headers test" }',
|
|
261
|
-
},
|
|
262
|
-
{ type: 'text-end', id: '0' },
|
|
263
|
-
{
|
|
264
|
-
type: 'finish',
|
|
265
|
-
finishReason: {
|
|
266
|
-
unified: 'stop',
|
|
267
|
-
raw: 'stop',
|
|
268
|
-
},
|
|
269
|
-
usage: testUsage,
|
|
270
|
-
},
|
|
271
|
-
]),
|
|
272
|
-
};
|
|
273
|
-
},
|
|
274
|
-
}),
|
|
275
|
-
prompt: '',
|
|
276
|
-
headers: { 'custom-request-header': 'request-header-value' },
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
expect(await simulateFlightServerRender(result.value)).toMatchSnapshot();
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe('options.providerMetadata', () => {
|
|
284
|
-
it('should pass provider metadata to model', async () => {
|
|
285
|
-
const result = await streamUI({
|
|
286
|
-
model: new MockLanguageModelV3({
|
|
287
|
-
doStream: async ({ providerOptions }) => {
|
|
288
|
-
expect(providerOptions).toStrictEqual({
|
|
289
|
-
aProvider: { someKey: 'someValue' },
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
stream: convertArrayToReadableStream([
|
|
294
|
-
{ type: 'text-start', id: '0' },
|
|
295
|
-
{
|
|
296
|
-
type: 'text-delta',
|
|
297
|
-
id: '0',
|
|
298
|
-
delta: '{ "content": "provider metadata test" }',
|
|
299
|
-
},
|
|
300
|
-
{ type: 'text-end', id: '0' },
|
|
301
|
-
{
|
|
302
|
-
type: 'finish',
|
|
303
|
-
usage: testUsage,
|
|
304
|
-
finishReason: {
|
|
305
|
-
unified: 'stop',
|
|
306
|
-
raw: 'stop',
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
]),
|
|
310
|
-
};
|
|
311
|
-
},
|
|
312
|
-
}),
|
|
313
|
-
prompt: '',
|
|
314
|
-
providerOptions: {
|
|
315
|
-
aProvider: { someKey: 'someValue' },
|
|
316
|
-
},
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
expect(await simulateFlightServerRender(result.value)).toMatchSnapshot();
|
|
320
|
-
});
|
|
321
|
-
});
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
import { delay } from '@ai-sdk/provider-utils';
|
|
2
|
-
import { createStreamableUI } from './create-streamable-ui';
|
|
3
|
-
import { describe, it, expect } from 'vitest';
|
|
4
|
-
|
|
5
|
-
// This is a workaround to render the Flight response in a test environment.
|
|
6
|
-
async function flightRender(node: React.ReactNode, byChunk?: boolean) {
|
|
7
|
-
const ReactDOM = require('react-dom');
|
|
8
|
-
ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactDOMCurrentDispatcher =
|
|
9
|
-
{ current: {} };
|
|
10
|
-
|
|
11
|
-
const React = require('react');
|
|
12
|
-
React.__SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
|
|
13
|
-
ReactSharedServerInternals: {},
|
|
14
|
-
ReactCurrentCache: {
|
|
15
|
-
current: null,
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const {
|
|
20
|
-
renderToReadableStream,
|
|
21
|
-
} = require('react-server-dom-webpack/server.edge');
|
|
22
|
-
|
|
23
|
-
const stream = renderToReadableStream(node);
|
|
24
|
-
const reader = stream.getReader();
|
|
25
|
-
|
|
26
|
-
const chunks = [];
|
|
27
|
-
let result = '';
|
|
28
|
-
while (true) {
|
|
29
|
-
const { done, value } = await reader.read();
|
|
30
|
-
if (done) {
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const decoded = new TextDecoder().decode(value);
|
|
35
|
-
if (byChunk) {
|
|
36
|
-
chunks.push(decoded);
|
|
37
|
-
} else {
|
|
38
|
-
result += decoded;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return byChunk ? chunks : result;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function recursiveResolve(val: any): Promise<any> {
|
|
46
|
-
if (val && typeof val === 'object' && typeof val.then === 'function') {
|
|
47
|
-
return await recursiveResolve(await val);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (Array.isArray(val)) {
|
|
51
|
-
return await Promise.all(val.map(recursiveResolve));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (val && typeof val === 'object') {
|
|
55
|
-
const result: any = {};
|
|
56
|
-
for (const key in val) {
|
|
57
|
-
result[key] = await recursiveResolve(val[key]);
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return val;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function simulateFlightServerRender(node: React.ReactNode) {
|
|
66
|
-
async function traverse(node: any): Promise<any> {
|
|
67
|
-
if (!node) return {};
|
|
68
|
-
|
|
69
|
-
// Let's only do one level of promise resolution here. As it's only for testing purposes.
|
|
70
|
-
const props = await recursiveResolve({ ...node.props });
|
|
71
|
-
|
|
72
|
-
const { type } = node;
|
|
73
|
-
const { children, ...otherProps } = props;
|
|
74
|
-
const typeName = typeof type === 'function' ? type.name : String(type);
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
type: typeName,
|
|
78
|
-
props: otherProps,
|
|
79
|
-
children:
|
|
80
|
-
typeof children === 'string'
|
|
81
|
-
? children
|
|
82
|
-
: Array.isArray(children)
|
|
83
|
-
? children.map(traverse)
|
|
84
|
-
: await traverse(children),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return traverse(node);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getFinalValueFromResolved(node: any) {
|
|
92
|
-
if (!node) return node;
|
|
93
|
-
if (node.type === 'Symbol(react.suspense)') {
|
|
94
|
-
return getFinalValueFromResolved(node.children);
|
|
95
|
-
} else if (node.type === '') {
|
|
96
|
-
let wrapper;
|
|
97
|
-
let value = node.props.value;
|
|
98
|
-
let next = node.props.n;
|
|
99
|
-
let current = node.props.c;
|
|
100
|
-
|
|
101
|
-
while (next) {
|
|
102
|
-
if (next.append) {
|
|
103
|
-
if (wrapper === undefined) {
|
|
104
|
-
wrapper = current;
|
|
105
|
-
} else if (typeof current === 'string' && typeof wrapper === 'string') {
|
|
106
|
-
wrapper = wrapper + current;
|
|
107
|
-
} else {
|
|
108
|
-
wrapper = (
|
|
109
|
-
<>
|
|
110
|
-
{wrapper}
|
|
111
|
-
{current}
|
|
112
|
-
</>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
value = next.value;
|
|
118
|
-
next = next.next;
|
|
119
|
-
current = value;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return getFinalValueFromResolved(
|
|
123
|
-
wrapper === undefined ? (
|
|
124
|
-
value
|
|
125
|
-
) : typeof value === 'string' && typeof wrapper === 'string' ? (
|
|
126
|
-
wrapper + value
|
|
127
|
-
) : (
|
|
128
|
-
<>
|
|
129
|
-
{wrapper}
|
|
130
|
-
{value}
|
|
131
|
-
</>
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
return node;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
describe('rsc - createStreamableUI()', () => {
|
|
139
|
-
it('should emit React Nodes that can be updated', async () => {
|
|
140
|
-
const ui = createStreamableUI(<div>1</div>);
|
|
141
|
-
ui.update(<div>2</div>);
|
|
142
|
-
ui.update(<div>3</div>);
|
|
143
|
-
ui.done();
|
|
144
|
-
|
|
145
|
-
const final = getFinalValueFromResolved(
|
|
146
|
-
await simulateFlightServerRender(ui.value),
|
|
147
|
-
);
|
|
148
|
-
expect(final).toMatchInlineSnapshot(`
|
|
149
|
-
<div>
|
|
150
|
-
3
|
|
151
|
-
</div>
|
|
152
|
-
`);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should emit React Nodes that can be updated with .done()', async () => {
|
|
156
|
-
const ui = createStreamableUI(<div>1</div>);
|
|
157
|
-
ui.update(<div>2</div>);
|
|
158
|
-
ui.update(<div>3</div>);
|
|
159
|
-
ui.done(<div>4</div>);
|
|
160
|
-
|
|
161
|
-
const final = getFinalValueFromResolved(
|
|
162
|
-
await simulateFlightServerRender(ui.value),
|
|
163
|
-
);
|
|
164
|
-
expect(final).toMatchInlineSnapshot(`
|
|
165
|
-
<div>
|
|
166
|
-
4
|
|
167
|
-
</div>
|
|
168
|
-
`);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('should support .append()', async () => {
|
|
172
|
-
const ui = createStreamableUI(<div>1</div>);
|
|
173
|
-
ui.update(<div>2</div>);
|
|
174
|
-
ui.append(<div>3</div>);
|
|
175
|
-
ui.append(<div>4</div>);
|
|
176
|
-
ui.done();
|
|
177
|
-
|
|
178
|
-
const final = getFinalValueFromResolved(
|
|
179
|
-
await simulateFlightServerRender(ui.value),
|
|
180
|
-
);
|
|
181
|
-
expect(final).toMatchInlineSnapshot(`
|
|
182
|
-
<React.Fragment>
|
|
183
|
-
<React.Fragment>
|
|
184
|
-
<div>
|
|
185
|
-
2
|
|
186
|
-
</div>
|
|
187
|
-
<div>
|
|
188
|
-
3
|
|
189
|
-
</div>
|
|
190
|
-
</React.Fragment>
|
|
191
|
-
<div>
|
|
192
|
-
4
|
|
193
|
-
</div>
|
|
194
|
-
</React.Fragment>
|
|
195
|
-
`);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should support streaming .append() result before .done()', async () => {
|
|
199
|
-
const ui = createStreamableUI(<div>1</div>);
|
|
200
|
-
ui.append(<div>2</div>);
|
|
201
|
-
ui.append(<div>3</div>);
|
|
202
|
-
|
|
203
|
-
const currentResolved = (ui.value as React.ReactElement).props.children
|
|
204
|
-
.props.n;
|
|
205
|
-
const tryResolve1 = await Promise.race([currentResolved, delay()]);
|
|
206
|
-
expect(tryResolve1).toBeDefined();
|
|
207
|
-
const tryResolve2 = await Promise.race([tryResolve1.next, delay()]);
|
|
208
|
-
expect(tryResolve2).toBeDefined();
|
|
209
|
-
expect(getFinalValueFromResolved(tryResolve2.value)).toMatchInlineSnapshot(`
|
|
210
|
-
<div>
|
|
211
|
-
3
|
|
212
|
-
</div>
|
|
213
|
-
`);
|
|
214
|
-
|
|
215
|
-
ui.append(<div>4</div>);
|
|
216
|
-
ui.done();
|
|
217
|
-
|
|
218
|
-
const final = getFinalValueFromResolved(
|
|
219
|
-
await simulateFlightServerRender(ui.value),
|
|
220
|
-
);
|
|
221
|
-
expect(final).toMatchInlineSnapshot(`
|
|
222
|
-
<React.Fragment>
|
|
223
|
-
<React.Fragment>
|
|
224
|
-
<React.Fragment>
|
|
225
|
-
<div>
|
|
226
|
-
1
|
|
227
|
-
</div>
|
|
228
|
-
<div>
|
|
229
|
-
2
|
|
230
|
-
</div>
|
|
231
|
-
</React.Fragment>
|
|
232
|
-
<div>
|
|
233
|
-
3
|
|
234
|
-
</div>
|
|
235
|
-
</React.Fragment>
|
|
236
|
-
<div>
|
|
237
|
-
4
|
|
238
|
-
</div>
|
|
239
|
-
</React.Fragment>
|
|
240
|
-
`);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should support updating the appended ui', async () => {
|
|
244
|
-
const ui = createStreamableUI(<div>1</div>);
|
|
245
|
-
ui.update(<div>2</div>);
|
|
246
|
-
ui.append(<div>3</div>);
|
|
247
|
-
ui.done(<div>4</div>);
|
|
248
|
-
|
|
249
|
-
const final = getFinalValueFromResolved(
|
|
250
|
-
await simulateFlightServerRender(ui.value),
|
|
251
|
-
);
|
|
252
|
-
expect(final).toMatchInlineSnapshot(`
|
|
253
|
-
<React.Fragment>
|
|
254
|
-
<div>
|
|
255
|
-
2
|
|
256
|
-
</div>
|
|
257
|
-
<div>
|
|
258
|
-
4
|
|
259
|
-
</div>
|
|
260
|
-
</React.Fragment>
|
|
261
|
-
`);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('should re-use the text node when appending strings', async () => {
|
|
265
|
-
const ui = createStreamableUI('hello');
|
|
266
|
-
ui.append(' world');
|
|
267
|
-
ui.append('!');
|
|
268
|
-
ui.done();
|
|
269
|
-
|
|
270
|
-
const final = getFinalValueFromResolved(
|
|
271
|
-
await simulateFlightServerRender(ui.value),
|
|
272
|
-
);
|
|
273
|
-
expect(final).toMatchInlineSnapshot('"hello world!"');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should send minimal incremental diffs when appending strings', async () => {
|
|
277
|
-
const ui = createStreamableUI('hello');
|
|
278
|
-
ui.append(' world');
|
|
279
|
-
ui.append(' and');
|
|
280
|
-
ui.append(' universe');
|
|
281
|
-
ui.done();
|
|
282
|
-
|
|
283
|
-
expect(await flightRender(ui.value)).toMatchInlineSnapshot(`
|
|
284
|
-
"1:"$Sreact.suspense"
|
|
285
|
-
2:D{"name":"","env":"Server"}
|
|
286
|
-
0:["$","$1",null,{"fallback":"hello","children":"$L2"}]
|
|
287
|
-
3:D{"name":"","env":"Server"}
|
|
288
|
-
2:["hello",["$","$1",null,{"fallback":" world","children":"$L3"}]]
|
|
289
|
-
4:D{"name":"","env":"Server"}
|
|
290
|
-
3:[" world",["$","$1",null,{"fallback":" and","children":"$L4"}]]
|
|
291
|
-
5:D{"name":"","env":"Server"}
|
|
292
|
-
4:[" and",["$","$1",null,{"fallback":" universe","children":"$L5"}]]
|
|
293
|
-
5:" universe"
|
|
294
|
-
"
|
|
295
|
-
`);
|
|
296
|
-
|
|
297
|
-
const final = getFinalValueFromResolved(
|
|
298
|
-
await simulateFlightServerRender(ui.value),
|
|
299
|
-
);
|
|
300
|
-
expect(final).toStrictEqual('hello world and universe');
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('should error when updating a closed streamable', async () => {
|
|
304
|
-
const ui = createStreamableUI(<div>1</div>);
|
|
305
|
-
ui.done(<div>2</div>);
|
|
306
|
-
|
|
307
|
-
expect(() => {
|
|
308
|
-
ui.update(<div>3</div>);
|
|
309
|
-
}).toThrowErrorMatchingInlineSnapshot(
|
|
310
|
-
'[Error: .update(): UI stream is already closed.]',
|
|
311
|
-
);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('should avoid sending data again if the same UI is passed', async () => {
|
|
315
|
-
const node = <div>1</div>;
|
|
316
|
-
const ui = createStreamableUI(node);
|
|
317
|
-
ui.update(node);
|
|
318
|
-
ui.update(node);
|
|
319
|
-
ui.update(node);
|
|
320
|
-
ui.update(node);
|
|
321
|
-
ui.update(node);
|
|
322
|
-
ui.update(node);
|
|
323
|
-
ui.done();
|
|
324
|
-
|
|
325
|
-
expect(await flightRender(ui.value)).toMatchInlineSnapshot(`
|
|
326
|
-
"1:"$Sreact.suspense"
|
|
327
|
-
2:D{"name":"","env":"Server"}
|
|
328
|
-
0:["$","$1",null,{"fallback":["$","div",null,{"children":"1"}],"children":"$L2"}]
|
|
329
|
-
4:{"children":"1"}
|
|
330
|
-
3:["$","div",null,"$4"]
|
|
331
|
-
2:"$3"
|
|
332
|
-
"
|
|
333
|
-
`);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('should return self', async () => {
|
|
337
|
-
const ui = createStreamableUI(<div>1</div>)
|
|
338
|
-
.update(<div>2</div>)
|
|
339
|
-
.update(<div>3</div>)
|
|
340
|
-
.done(<div>4</div>);
|
|
341
|
-
|
|
342
|
-
expect(await flightRender(ui.value)).toMatchInlineSnapshot(`
|
|
343
|
-
"1:"$Sreact.suspense"
|
|
344
|
-
2:D{"name":"","env":"Server"}
|
|
345
|
-
0:["$","$1",null,{"fallback":["$","div",null,{"children":"1"}],"children":"$L2"}]
|
|
346
|
-
3:D{"name":"","env":"Server"}
|
|
347
|
-
2:["$","$1",null,{"fallback":["$","div",null,{"children":"2"}],"children":"$L3"}]
|
|
348
|
-
4:D{"name":"","env":"Server"}
|
|
349
|
-
3:["$","$1",null,{"fallback":["$","div",null,{"children":"3"}],"children":"$L4"}]
|
|
350
|
-
4:["$","div",null,{"children":"4"}]
|
|
351
|
-
"
|
|
352
|
-
`);
|
|
353
|
-
});
|
|
354
|
-
});
|