-
-
Notifications
You must be signed in to change notification settings - Fork 120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: edge cases for server action #745
Changes from all commits
355e558
c5934c8
0de7663
5c6b67c
6fef60b
65c6ecc
00efab2
1339070
c64150f
237e07b
a37303a
4f8aef3
409cef7
366aadc
daeb9f2
256cc41
b7b88a1
21fcbe8
a1d06b7
f79441c
3267bdf
ddb4980
0a7ff13
67bad7c
a8e579c
75e7d18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"name": "ai", | ||
"type": "module", | ||
"version": "1.0.0", | ||
"description": "Vercel AI mockup", | ||
"exports": { | ||
"./rsc": { | ||
"types": "./src/index.d.ts", | ||
"react-server": "./src/server.js", | ||
"import": "./src/client.js" | ||
} | ||
}, | ||
"devDependencies": { | ||
"react-dom": "^18", | ||
"react-server-dom-webpack": "18.3.0-canary-eb33bd747-20240312" | ||
}, | ||
"peerDependencies": { | ||
"react": "^18 || ^19" | ||
}, | ||
"private": true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
'use client'; | ||
import { useActions } from './shared.js'; | ||
|
||
export { useActions }; | ||
|
||
export function createAI() { | ||
throw new Error('You should not call createAI in the client side'); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import type { ReactNode } from 'react'; | ||
|
||
declare function createAI(options: { | ||
actions: Record<string, any>; | ||
}): (props: { children: ReactNode }) => ReactNode; | ||
|
||
declare function useActions(): Record<string, any>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
'use server'; | ||
import { InternalProvider } from './shared.js'; | ||
import { jsx } from 'react/jsx-runtime'; | ||
|
||
async function innerAction({ action }, ...args) { | ||
'use server'; | ||
return await action(...args); | ||
} | ||
|
||
function wrapAction(action) { | ||
return innerAction.bind(null, { action }); | ||
} | ||
|
||
export function createAI({ actions }) { | ||
const wrappedActions = {}; | ||
for (const name in actions) { | ||
wrappedActions[name] = wrapAction(actions[name]); | ||
} | ||
return function AI(props) { | ||
return jsx(InternalProvider, { | ||
actions: wrappedActions, | ||
children: props.children, | ||
}); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
'use client'; | ||
import { createContext, useContext } from 'react'; | ||
import { jsx } from 'react/jsx-runtime'; | ||
|
||
const ActionContext = createContext(null); | ||
|
||
export function useActions() { | ||
return useContext(ActionContext); | ||
} | ||
|
||
export function InternalProvider(props) { | ||
return jsx('div', { | ||
'data-testid': 'ai-internal-provider', | ||
children: jsx(ActionContext.Provider, { | ||
value: props.actions, | ||
children: props.children, | ||
}), | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
'use client'; | ||
|
||
import { useActions } from 'ai/rsc'; | ||
import { useEffect } from 'react'; | ||
|
||
export const ClientActionsConsumer = () => { | ||
const actions = useActions(); | ||
useEffect(() => { | ||
(globalThis as any).actions = actions; | ||
}, [actions]); | ||
return <div>{JSON.stringify(Object.keys(actions))}</div>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { ReactNode } from 'react'; | ||
import { createAI } from 'ai/rsc'; | ||
|
||
const AI = createAI({ | ||
actions: { | ||
foo: async () => { | ||
'use server'; | ||
return 0; | ||
}, | ||
}, | ||
}); | ||
|
||
export function ServerProvider({ children }: { children: ReactNode }) { | ||
return <AI>{children}</AI>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,16 @@ | |
import { streamToString } from '../utils/stream.js'; | ||
import { decodeActionId } from '../renderers/utils.js'; | ||
|
||
// HACK for react-server-dom-webpack without webpack | ||
(globalThis as any).__webpack_module_loading__ ||= new Map(); | ||
(globalThis as any).__webpack_module_cache__ ||= new Map(); | ||
(globalThis as any).__webpack_chunk_load__ ||= async (id: string) => | ||
(globalThis as any).__webpack_module_loading__.get(id); | ||
(globalThis as any).__webpack_require__ ||= (id: string) => | ||
(globalThis as any).__webpack_module_cache__.get(id); | ||
const moduleLoading = (globalThis as any).__webpack_module_loading__; | ||
const moduleCache = (globalThis as any).__webpack_module_cache__; | ||
|
||
export const SERVER_MODULE_MAP = { | ||
'rsdw-server': 'react-server-dom-webpack/server.edge', | ||
'waku-server': 'waku/server', | ||
|
@@ -189,7 +199,46 @@ | |
) { | ||
// XXX This doesn't support streaming unlike busboy | ||
const formData = parseFormData(bodyStr, contentType); | ||
args = await decodeReply(formData); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are something wrong, formData could include server references which need webpack runtime @dai-shi There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright. It's reasonable that it requires the second argument |
||
const moduleMap = new Proxy({} as Record<string, ImportManifestEntry>, { | ||
get(_target, rsfId: string): ImportManifestEntry { | ||
const [fileId, name] = rsfId.split('#') as [string, string]; | ||
// fixme: race condition, server actions are not initialized in the first time | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As said, I think we should initialize all server actions before running RSC otherwise it will crash for the first time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for next.js, they have a global server action map in the runtime There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still need to understand the issue, but it feels like we want lazy evaluation. |
||
if (!moduleLoading.has(fileId)) { | ||
if (!opts.isDev) { | ||
moduleLoading.set( | ||
fileId, | ||
import( | ||
/* @vite-ignore */ | ||
fileId | ||
).then((m: any) => { | ||
moduleCache.set( | ||
fileId, | ||
Object.fromEntries(m['__waku_serverActions']), | ||
); | ||
}), | ||
); | ||
} else { | ||
moduleLoading.set( | ||
fileId, | ||
opts | ||
.loadServerFile(filePathToFileURL(fileId)) | ||
Check failure on line 224 in packages/waku/src/lib/renderers/rsc-renderer.ts GitHub Actions / test
|
||
.then((m: any) => { | ||
moduleCache.set( | ||
fileId, | ||
Object.fromEntries(m['__waku_serverActions']), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks ugly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah... I hope we can improve it. |
||
); | ||
}), | ||
); | ||
} | ||
} | ||
return { | ||
id: fileId, | ||
chunks: [], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure? I mean waku's approach might be different from nextjs's one. |
||
name, | ||
}; | ||
}, | ||
}); | ||
args = await decodeReply(formData, moduleMap); | ||
} else if (bodyStr) { | ||
args = await decodeReply(bodyStr); | ||
} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we should finish up #707 before this PR.