@ai-sdk/rsc 2.0.44 → 2.0.46
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 +13 -0
- package/package.json +3 -2
- package/src/ai-state.test.ts +146 -0
- package/src/ai-state.tsx +210 -0
- package/src/index.ts +20 -0
- package/src/provider.tsx +149 -0
- package/src/rsc-client.ts +8 -0
- package/src/rsc-server.ts +5 -0
- package/src/rsc-shared.mts +11 -0
- package/src/shared-client/context.tsx +226 -0
- package/src/shared-client/index.ts +11 -0
- package/src/stream-ui/__snapshots__/render.ui.test.tsx.snap +91 -0
- package/src/stream-ui/__snapshots__/stream-ui.ui.test.tsx.snap +213 -0
- package/src/stream-ui/index.tsx +1 -0
- package/src/stream-ui/stream-ui.tsx +419 -0
- package/src/stream-ui/stream-ui.ui.test.tsx +321 -0
- package/src/streamable-ui/create-streamable-ui.tsx +148 -0
- package/src/streamable-ui/create-streamable-ui.ui.test.tsx +354 -0
- package/src/streamable-ui/create-suspended-chunk.tsx +84 -0
- package/src/streamable-value/create-streamable-value.test.tsx +179 -0
- package/src/streamable-value/create-streamable-value.ts +296 -0
- package/src/streamable-value/is-streamable-value.ts +10 -0
- package/src/streamable-value/read-streamable-value.tsx +113 -0
- package/src/streamable-value/read-streamable-value.ui.test.tsx +165 -0
- package/src/streamable-value/streamable-value.ts +37 -0
- package/src/streamable-value/use-streamable-value.tsx +91 -0
- package/src/types/index.ts +1 -0
- package/src/types.test-d.ts +17 -0
- package/src/types.ts +71 -0
- package/src/util/constants.ts +5 -0
- package/src/util/create-resolvable-promise.ts +28 -0
- package/src/util/is-async-generator.ts +7 -0
- package/src/util/is-function.ts +8 -0
- package/src/util/is-generator.ts +5 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/rsc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.46",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/rsc-client.mjs",
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/**/*",
|
|
20
|
+
"src",
|
|
20
21
|
"CHANGELOG.md",
|
|
21
22
|
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"jsondiffpatch": "0.7.3",
|
|
25
|
-
"ai": "6.0.
|
|
26
|
+
"ai": "6.0.46",
|
|
26
27
|
"@ai-sdk/provider": "3.0.4",
|
|
27
28
|
"@ai-sdk/provider-utils": "4.0.8"
|
|
28
29
|
},
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
withAIState,
|
|
4
|
+
getAIState,
|
|
5
|
+
getMutableAIState,
|
|
6
|
+
sealMutableAIState,
|
|
7
|
+
getAIStateDeltaPromise,
|
|
8
|
+
} from './ai-state';
|
|
9
|
+
|
|
10
|
+
describe('AI State Management', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should get the current AI state', () => {
|
|
16
|
+
const initialState = { foo: 'bar' };
|
|
17
|
+
const result = withAIState({ state: initialState, options: {} }, () => {
|
|
18
|
+
return getAIState();
|
|
19
|
+
});
|
|
20
|
+
expect(result).toEqual(initialState);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should get a specific key from the AI state', () => {
|
|
24
|
+
const initialState = { foo: 'bar', baz: 'qux' };
|
|
25
|
+
const result = withAIState({ state: initialState, options: {} }, () => {
|
|
26
|
+
return getAIState('foo');
|
|
27
|
+
});
|
|
28
|
+
expect(result).toBe('bar');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should update the AI state', () => {
|
|
32
|
+
const initialState = { foo: 'bar' };
|
|
33
|
+
withAIState({ state: initialState, options: {} }, () => {
|
|
34
|
+
const mutableState = getMutableAIState();
|
|
35
|
+
mutableState.update({ foo: 'baz' });
|
|
36
|
+
expect(getAIState()).toEqual({ foo: 'baz' });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should update a specific key in the AI state', () => {
|
|
41
|
+
const initialState = { foo: 'bar', baz: 'qux' };
|
|
42
|
+
withAIState({ state: initialState, options: {} }, () => {
|
|
43
|
+
const mutableState = getMutableAIState('foo');
|
|
44
|
+
mutableState.update('newValue');
|
|
45
|
+
expect(getAIState()).toEqual({ foo: 'newValue', baz: 'qux' });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should throw an error when accessing AI state outside of withAIState', () => {
|
|
50
|
+
expect(() => getAIState()).toThrow(
|
|
51
|
+
'`getAIState` must be called within an AI Action.',
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should throw an error when updating AI state after it has been sealed', () => {
|
|
56
|
+
withAIState({ state: {}, options: {} }, () => {
|
|
57
|
+
sealMutableAIState();
|
|
58
|
+
expect(() => getMutableAIState()).toThrow(
|
|
59
|
+
'`getMutableAIState` must be called before returning from an AI Action.',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should call onSetAIState when updating state', () => {
|
|
65
|
+
const onSetAIState = vi.fn();
|
|
66
|
+
const initialState = { foo: 'bar' };
|
|
67
|
+
withAIState({ state: initialState, options: { onSetAIState } }, () => {
|
|
68
|
+
const mutableState = getMutableAIState();
|
|
69
|
+
mutableState.update({ foo: 'baz' });
|
|
70
|
+
mutableState.done({ foo: 'baz' });
|
|
71
|
+
});
|
|
72
|
+
expect(onSetAIState).toHaveBeenCalledWith(
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
state: { foo: 'baz' },
|
|
75
|
+
done: true,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle updates with and without key', async () => {
|
|
81
|
+
type Message = { role: string; content: string };
|
|
82
|
+
|
|
83
|
+
type AIState = {
|
|
84
|
+
chatId: string;
|
|
85
|
+
messages: Array<Message>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const initialState: AIState = {
|
|
89
|
+
chatId: '123',
|
|
90
|
+
messages: [],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
await withAIState({ state: initialState, options: {} }, async () => {
|
|
94
|
+
// Test with getMutableState()
|
|
95
|
+
const stateWithoutKey = getMutableAIState();
|
|
96
|
+
|
|
97
|
+
stateWithoutKey.update((current: AIState) => ({
|
|
98
|
+
...current,
|
|
99
|
+
messages: [...current.messages, { role: 'user', content: 'Hello!' }],
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
stateWithoutKey.done((current: AIState) => ({
|
|
103
|
+
...current,
|
|
104
|
+
messages: [
|
|
105
|
+
...current.messages,
|
|
106
|
+
{ role: 'assistant', content: 'Hello! How can I assist you today?' },
|
|
107
|
+
],
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const deltaWithoutKey = await getAIStateDeltaPromise();
|
|
111
|
+
expect(deltaWithoutKey).toBeDefined();
|
|
112
|
+
expect(getAIState()).toEqual({
|
|
113
|
+
chatId: '123',
|
|
114
|
+
messages: [
|
|
115
|
+
{ role: 'user', content: 'Hello!' },
|
|
116
|
+
{ role: 'assistant', content: 'Hello! How can I assist you today?' },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await withAIState({ state: initialState, options: {} }, async () => {
|
|
122
|
+
// Test with getMutableState('messages')
|
|
123
|
+
const stateWithKey = getMutableAIState('messages');
|
|
124
|
+
|
|
125
|
+
stateWithKey.update((current: Array<Message>) => [
|
|
126
|
+
...current,
|
|
127
|
+
{ role: 'user', content: 'Hello!' },
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
stateWithKey.done((current: Array<Message>) => [
|
|
131
|
+
...current,
|
|
132
|
+
{ role: 'assistant', content: 'Hello! How can I assist you today?' },
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const deltaWithKey = await getAIStateDeltaPromise();
|
|
136
|
+
expect(deltaWithKey).toBeDefined();
|
|
137
|
+
expect(getAIState()).toEqual({
|
|
138
|
+
chatId: '123',
|
|
139
|
+
messages: [
|
|
140
|
+
{ role: 'user', content: 'Hello!' },
|
|
141
|
+
{ role: 'assistant', content: 'Hello! How can I assist you today?' },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/ai-state.tsx
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as jsondiffpatch from 'jsondiffpatch';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { createResolvablePromise } from './util/create-resolvable-promise';
|
|
4
|
+
import { isFunction } from './util/is-function';
|
|
5
|
+
import type {
|
|
6
|
+
AIProvider,
|
|
7
|
+
InferAIState,
|
|
8
|
+
InternalAIStateStorageOptions,
|
|
9
|
+
MutableAIState,
|
|
10
|
+
ValueOrUpdater,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
// It is possible that multiple AI requests get in concurrently, for different
|
|
14
|
+
// AI instances. So ALS is necessary here for a simpler API.
|
|
15
|
+
const asyncAIStateStorage = new AsyncLocalStorage<{
|
|
16
|
+
currentState: any;
|
|
17
|
+
originalState: any;
|
|
18
|
+
sealed: boolean;
|
|
19
|
+
options: InternalAIStateStorageOptions;
|
|
20
|
+
mutationDeltaPromise?: Promise<any>;
|
|
21
|
+
mutationDeltaResolve?: (v: any) => void;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
function getAIStateStoreOrThrow(message: string) {
|
|
25
|
+
const store = asyncAIStateStorage.getStore();
|
|
26
|
+
if (!store) {
|
|
27
|
+
throw new Error(message);
|
|
28
|
+
}
|
|
29
|
+
return store;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function withAIState<S, T>(
|
|
33
|
+
{ state, options }: { state: S; options: InternalAIStateStorageOptions },
|
|
34
|
+
fn: () => T,
|
|
35
|
+
): T {
|
|
36
|
+
return asyncAIStateStorage.run(
|
|
37
|
+
{
|
|
38
|
+
currentState: JSON.parse(JSON.stringify(state)), // deep clone object
|
|
39
|
+
originalState: state,
|
|
40
|
+
sealed: false,
|
|
41
|
+
options,
|
|
42
|
+
},
|
|
43
|
+
fn,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getAIStateDeltaPromise() {
|
|
48
|
+
const store = getAIStateStoreOrThrow('Internal error occurred.');
|
|
49
|
+
return store.mutationDeltaPromise;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Internal method. This will be called after the AI Action has been returned
|
|
53
|
+
// and you can no longer call `getMutableAIState()` inside any async callbacks
|
|
54
|
+
// created by that Action.
|
|
55
|
+
export function sealMutableAIState() {
|
|
56
|
+
const store = getAIStateStoreOrThrow('Internal error occurred.');
|
|
57
|
+
store.sealed = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the current AI state.
|
|
62
|
+
* If `key` is provided, it will return the value of the specified key in the
|
|
63
|
+
* AI state, if it's an object. If it's not an object, it will throw an error.
|
|
64
|
+
*
|
|
65
|
+
* @example const state = getAIState() // Get the entire AI state
|
|
66
|
+
* @example const field = getAIState('key') // Get the value of the key
|
|
67
|
+
*/
|
|
68
|
+
function getAIState<AI extends AIProvider = any>(): Readonly<
|
|
69
|
+
InferAIState<AI, any>
|
|
70
|
+
>;
|
|
71
|
+
function getAIState<AI extends AIProvider = any>(
|
|
72
|
+
key: keyof InferAIState<AI, any>,
|
|
73
|
+
): Readonly<InferAIState<AI, any>[typeof key]>;
|
|
74
|
+
function getAIState<AI extends AIProvider = any>(
|
|
75
|
+
...args: [] | [key: keyof InferAIState<AI, any>]
|
|
76
|
+
) {
|
|
77
|
+
const store = getAIStateStoreOrThrow(
|
|
78
|
+
'`getAIState` must be called within an AI Action.',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (args.length > 0) {
|
|
82
|
+
const key = args[0];
|
|
83
|
+
if (typeof store.currentState !== 'object') {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`You can't get the "${String(
|
|
86
|
+
key,
|
|
87
|
+
)}" field from the AI state because it's not an object.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return store.currentState[key as keyof typeof store.currentState];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return store.currentState;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the mutable AI state. Note that you must call `.done()` when finishing
|
|
98
|
+
* updating the AI state.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* const state = getMutableAIState()
|
|
103
|
+
* state.update({ ...state.get(), key: 'value' })
|
|
104
|
+
* state.update((currentState) => ({ ...currentState, key: 'value' }))
|
|
105
|
+
* state.done()
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```tsx
|
|
110
|
+
* const state = getMutableAIState()
|
|
111
|
+
* state.done({ ...state.get(), key: 'value' }) // Done with a new state
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
function getMutableAIState<AI extends AIProvider = any>(): MutableAIState<
|
|
115
|
+
InferAIState<AI, any>
|
|
116
|
+
>;
|
|
117
|
+
function getMutableAIState<AI extends AIProvider = any>(
|
|
118
|
+
key: keyof InferAIState<AI, any>,
|
|
119
|
+
): MutableAIState<InferAIState<AI, any>[typeof key]>;
|
|
120
|
+
function getMutableAIState<AI extends AIProvider = any>(
|
|
121
|
+
...args: [] | [key: keyof InferAIState<AI, any>]
|
|
122
|
+
) {
|
|
123
|
+
type AIState = InferAIState<AI, any>;
|
|
124
|
+
type AIStateWithKey = typeof args extends [key: keyof AIState]
|
|
125
|
+
? AIState[(typeof args)[0]]
|
|
126
|
+
: AIState;
|
|
127
|
+
type NewStateOrUpdater = ValueOrUpdater<AIStateWithKey>;
|
|
128
|
+
|
|
129
|
+
const store = getAIStateStoreOrThrow(
|
|
130
|
+
'`getMutableAIState` must be called within an AI Action.',
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (store.sealed) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"`getMutableAIState` must be called before returning from an AI Action. Please move it to the top level of the Action's function body.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!store.mutationDeltaPromise) {
|
|
140
|
+
const { promise, resolve } = createResolvablePromise();
|
|
141
|
+
store.mutationDeltaPromise = promise;
|
|
142
|
+
store.mutationDeltaResolve = resolve;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function doUpdate(newState: NewStateOrUpdater, done: boolean) {
|
|
146
|
+
if (args.length > 0) {
|
|
147
|
+
if (typeof store.currentState !== 'object') {
|
|
148
|
+
const key = args[0];
|
|
149
|
+
throw new Error(
|
|
150
|
+
`You can't modify the "${String(
|
|
151
|
+
key,
|
|
152
|
+
)}" field of the AI state because it's not an object.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isFunction(newState)) {
|
|
158
|
+
if (args.length > 0) {
|
|
159
|
+
store.currentState[args[0]] = newState(store.currentState[args[0]]);
|
|
160
|
+
} else {
|
|
161
|
+
store.currentState = newState(store.currentState);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
if (args.length > 0) {
|
|
165
|
+
store.currentState[args[0]] = newState;
|
|
166
|
+
} else {
|
|
167
|
+
store.currentState = newState;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
store.options.onSetAIState?.({
|
|
172
|
+
key: args.length > 0 ? args[0] : undefined,
|
|
173
|
+
state: store.currentState,
|
|
174
|
+
done,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const mutableState = {
|
|
179
|
+
get: () => {
|
|
180
|
+
if (args.length > 0) {
|
|
181
|
+
const key = args[0];
|
|
182
|
+
if (typeof store.currentState !== 'object') {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`You can't get the "${String(
|
|
185
|
+
key,
|
|
186
|
+
)}" field from the AI state because it's not an object.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return store.currentState[key] as Readonly<AIStateWithKey>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return store.currentState as Readonly<AIState>;
|
|
193
|
+
},
|
|
194
|
+
update: function update(newAIState: NewStateOrUpdater) {
|
|
195
|
+
doUpdate(newAIState, false);
|
|
196
|
+
},
|
|
197
|
+
done: function done(...doneArgs: [] | [NewStateOrUpdater]) {
|
|
198
|
+
if (doneArgs.length > 0) {
|
|
199
|
+
doUpdate(doneArgs[0] as NewStateOrUpdater, true);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const delta = jsondiffpatch.diff(store.originalState, store.currentState);
|
|
203
|
+
store.mutationDeltaResolve!(delta);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return mutableState;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { getAIState, getMutableAIState };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getAIState,
|
|
3
|
+
getMutableAIState,
|
|
4
|
+
createStreamableUI,
|
|
5
|
+
createStreamableValue,
|
|
6
|
+
streamUI,
|
|
7
|
+
createAI,
|
|
8
|
+
} from './rsc-server';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
readStreamableValue,
|
|
12
|
+
useStreamableValue,
|
|
13
|
+
useUIState,
|
|
14
|
+
useAIState,
|
|
15
|
+
useActions,
|
|
16
|
+
useSyncUIState,
|
|
17
|
+
} from './rsc-client';
|
|
18
|
+
|
|
19
|
+
export type { StreamableValue } from './streamable-value/streamable-value';
|
|
20
|
+
export * from './types';
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// This file provides the AI context to all AI Actions via AsyncLocalStorage.
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { InternalAIProvider } from './rsc-shared.mjs';
|
|
5
|
+
import {
|
|
6
|
+
withAIState,
|
|
7
|
+
getAIStateDeltaPromise,
|
|
8
|
+
sealMutableAIState,
|
|
9
|
+
} from './ai-state';
|
|
10
|
+
import type {
|
|
11
|
+
ServerWrappedActions,
|
|
12
|
+
AIAction,
|
|
13
|
+
AIActions,
|
|
14
|
+
AIProvider,
|
|
15
|
+
InternalAIStateStorageOptions,
|
|
16
|
+
OnSetAIState,
|
|
17
|
+
OnGetUIState,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
async function innerAction<T>(
|
|
21
|
+
{
|
|
22
|
+
action,
|
|
23
|
+
options,
|
|
24
|
+
}: { action: AIAction; options: InternalAIStateStorageOptions },
|
|
25
|
+
state: T,
|
|
26
|
+
...args: unknown[]
|
|
27
|
+
) {
|
|
28
|
+
'use server';
|
|
29
|
+
return await withAIState(
|
|
30
|
+
{
|
|
31
|
+
state,
|
|
32
|
+
options,
|
|
33
|
+
},
|
|
34
|
+
async () => {
|
|
35
|
+
const result = await action(...args);
|
|
36
|
+
sealMutableAIState();
|
|
37
|
+
return [getAIStateDeltaPromise() as Promise<T>, result];
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function wrapAction<T = unknown>(
|
|
43
|
+
action: AIAction,
|
|
44
|
+
options: InternalAIStateStorageOptions,
|
|
45
|
+
) {
|
|
46
|
+
return innerAction.bind(null, { action, options }) as AIAction<T>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createAI<
|
|
50
|
+
AIState = any,
|
|
51
|
+
UIState = any,
|
|
52
|
+
Actions extends AIActions = {},
|
|
53
|
+
>({
|
|
54
|
+
actions,
|
|
55
|
+
initialAIState,
|
|
56
|
+
initialUIState,
|
|
57
|
+
|
|
58
|
+
onSetAIState,
|
|
59
|
+
onGetUIState,
|
|
60
|
+
}: {
|
|
61
|
+
actions: Actions;
|
|
62
|
+
initialAIState?: AIState;
|
|
63
|
+
initialUIState?: UIState;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This function is called whenever the AI state is updated by an Action.
|
|
67
|
+
* You can use this to persist the AI state to a database, or to send it to a
|
|
68
|
+
* logging service.
|
|
69
|
+
*/
|
|
70
|
+
onSetAIState?: OnSetAIState<AIState>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* This function is used to retrieve the UI state based on the AI state.
|
|
74
|
+
* For example, to render the initial UI state based on a given AI state, or
|
|
75
|
+
* to sync the UI state when the application is already loaded.
|
|
76
|
+
*
|
|
77
|
+
* If returning `undefined`, the client side UI state will not be updated.
|
|
78
|
+
*
|
|
79
|
+
* This function must be annotated with the `"use server"` directive.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* onGetUIState: async () => {
|
|
84
|
+
* 'use server';
|
|
85
|
+
*
|
|
86
|
+
* const currentAIState = getAIState();
|
|
87
|
+
* const externalAIState = await loadAIStateFromDatabase();
|
|
88
|
+
*
|
|
89
|
+
* if (currentAIState === externalAIState) return undefined;
|
|
90
|
+
*
|
|
91
|
+
* // Update current AI state and return the new UI state
|
|
92
|
+
* const state = getMutableAIState()
|
|
93
|
+
* state.done(externalAIState)
|
|
94
|
+
*
|
|
95
|
+
* return <div>...</div>;
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
onGetUIState?: OnGetUIState<UIState>;
|
|
100
|
+
}) {
|
|
101
|
+
// Wrap all actions with our HoC.
|
|
102
|
+
const wrappedActions: ServerWrappedActions = {};
|
|
103
|
+
for (const name in actions) {
|
|
104
|
+
wrappedActions[name] = wrapAction(actions[name], {
|
|
105
|
+
onSetAIState,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const wrappedSyncUIState = onGetUIState
|
|
110
|
+
? wrapAction(onGetUIState, {})
|
|
111
|
+
: undefined;
|
|
112
|
+
|
|
113
|
+
const AI: AIProvider<AIState, UIState, Actions> = async props => {
|
|
114
|
+
if ('useState' in React) {
|
|
115
|
+
// This file must be running on the React Server layer.
|
|
116
|
+
// Ideally we should be using `import "server-only"` here but we can have a
|
|
117
|
+
// more customized error message with this implementation.
|
|
118
|
+
throw new Error(
|
|
119
|
+
'This component can only be used inside Server Components.',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let uiState = props.initialUIState ?? initialUIState;
|
|
124
|
+
let aiState = props.initialAIState ?? initialAIState;
|
|
125
|
+
let aiStateDelta = undefined;
|
|
126
|
+
|
|
127
|
+
if (wrappedSyncUIState) {
|
|
128
|
+
const [newAIStateDelta, newUIState] = await wrappedSyncUIState(aiState);
|
|
129
|
+
if (newUIState !== undefined) {
|
|
130
|
+
aiStateDelta = newAIStateDelta;
|
|
131
|
+
uiState = newUIState;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<InternalAIProvider
|
|
137
|
+
wrappedActions={wrappedActions}
|
|
138
|
+
wrappedSyncUIState={wrappedSyncUIState}
|
|
139
|
+
initialUIState={uiState}
|
|
140
|
+
initialAIState={aiState}
|
|
141
|
+
initialAIStatePatch={aiStateDelta}
|
|
142
|
+
>
|
|
143
|
+
{props.children}
|
|
144
|
+
</InternalAIProvider>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return AI;
|
|
149
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { getAIState, getMutableAIState } from './ai-state';
|
|
2
|
+
export { createAI } from './provider';
|
|
3
|
+
export { streamUI } from './stream-ui';
|
|
4
|
+
export { createStreamableUI } from './streamable-ui/create-streamable-ui';
|
|
5
|
+
export { createStreamableValue } from './streamable-value/create-streamable-value';
|