@tanstack/start-server-core 1.132.0-alpha.2 → 1.132.0-alpha.21

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/src/index.tsx CHANGED
@@ -58,3 +58,5 @@ export type {
58
58
  export * from './virtual-modules'
59
59
 
60
60
  export { HEADERS } from './constants'
61
+
62
+ export { createServerRpc } from './createServerRpc'
@@ -0,0 +1,24 @@
1
+ import { createSerializationAdapter } from '@tanstack/router-core'
2
+ import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core'
3
+ import { createServerRpc } from '../createServerRpc'
4
+ import { getServerFnById } from '../getServerFnById'
5
+
6
+ export const ServerFunctionSerializationAdapter = createSerializationAdapter({
7
+ key: '$TSS/serverfn',
8
+ test: (v): v is { functionId: string } => {
9
+ if (typeof v !== 'object' || v === null) return false
10
+
11
+ if (!(TSS_SERVER_FUNCTION in v)) return false
12
+
13
+ return !!v[TSS_SERVER_FUNCTION]
14
+ },
15
+ toSerializable: ({ functionId }) => ({ functionId }),
16
+ fromSerializable: ({ functionId }) => {
17
+ const fn = async (opts: any, signal: any): Promise<any> => {
18
+ const serverFn = await getServerFnById(functionId)
19
+ const result = await serverFn(opts ?? {}, signal)
20
+ return result.result
21
+ }
22
+ return createServerRpc(functionId, fn)
23
+ },
24
+ })
@@ -0,0 +1,10 @@
1
+ import { makeSerovalPlugin } from '@tanstack/router-core'
2
+ import { getDefaultSerovalPlugins } from '@tanstack/start-client-core'
3
+ import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter'
4
+
5
+ export function getSerovalPlugins() {
6
+ return [
7
+ ...getDefaultSerovalPlugins(),
8
+ makeSerovalPlugin(ServerFunctionSerializationAdapter),
9
+ ]
10
+ }
@@ -1,9 +1,13 @@
1
1
  import { isNotFound } from '@tanstack/router-core'
2
2
  import invariant from 'tiny-invariant'
3
- import { startSerializer } from '@tanstack/start-client-core'
4
- import { VIRTUAL_MODULES } from './virtual-modules'
5
- import { loadVirtualModule } from './loadVirtualModule'
3
+ import {
4
+ TSS_FORMDATA_CONTEXT,
5
+ X_TSS_SERIALIZED,
6
+ } from '@tanstack/start-client-core'
7
+ import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
6
8
  import { getResponse } from './request-response'
9
+ import { getServerFnById } from './getServerFnById'
10
+ import { getSerovalPlugins } from './serializer/getSerovalPlugins'
7
11
 
8
12
  function sanitizeBase(base: string | undefined) {
9
13
  if (!base) {
@@ -15,72 +19,6 @@ function sanitizeBase(base: string | undefined) {
15
19
  return base.replace(/^\/|\/$/g, '')
16
20
  }
17
21
 
18
- async function revive(root: any, reviver?: (key: string, value: any) => any) {
19
- async function reviveNode(holder: any, key: string) {
20
- const value = holder[key]
21
-
22
- if (value && typeof value === 'object') {
23
- await Promise.all(Object.keys(value).map((k) => reviveNode(value, k)))
24
- }
25
-
26
- if (reviver) {
27
- holder[key] = await reviver(key, holder[key])
28
- }
29
- }
30
-
31
- const holder = { '': root }
32
- await reviveNode(holder, '')
33
- return holder['']
34
- }
35
-
36
- async function reviveServerFns(key: string, value: any) {
37
- if (value && value.__serverFn === true && value.functionId) {
38
- const serverFn = await getServerFnById(value.functionId)
39
- return async (opts: any, signal: any): Promise<any> => {
40
- const result = await serverFn(opts ?? {}, signal)
41
- return result.result
42
- }
43
- }
44
- return value
45
- }
46
-
47
- async function getServerFnById(serverFnId: string) {
48
- const { default: serverFnManifest } = await loadVirtualModule(
49
- VIRTUAL_MODULES.serverFnManifest,
50
- )
51
-
52
- const serverFnInfo = serverFnManifest[serverFnId]
53
-
54
- if (!serverFnInfo) {
55
- console.info('serverFnManifest', serverFnManifest)
56
- throw new Error('Server function info not found for ' + serverFnId)
57
- }
58
-
59
- const fnModule = await serverFnInfo.importer()
60
-
61
- if (!fnModule) {
62
- console.info('serverFnInfo', serverFnInfo)
63
- throw new Error('Server function module not resolved for ' + serverFnId)
64
- }
65
-
66
- const action = fnModule[serverFnInfo.functionName]
67
-
68
- if (!action) {
69
- console.info('serverFnInfo', serverFnInfo)
70
- console.info('fnModule', fnModule)
71
- throw new Error(
72
- `Server function module export not resolved for serverFn ID: ${serverFnId}`,
73
- )
74
- }
75
- return action
76
- }
77
-
78
- async function parsePayload(payload: any) {
79
- const parsedPayload = startSerializer.parse(payload)
80
- await revive(parsedPayload, reviveServerFns)
81
- return parsedPayload
82
- }
83
-
84
22
  export const handleServerAction = async ({ request }: { request: Request }) => {
85
23
  const controller = new AbortController()
86
24
  const signal = controller.signal
@@ -104,7 +42,6 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
104
42
  }
105
43
 
106
44
  const isCreateServerFn = 'createServerFn' in search
107
- const isRaw = 'raw' in search
108
45
 
109
46
  if (typeof serverFnId !== 'string') {
110
47
  throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
@@ -118,14 +55,21 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
118
55
  'application/x-www-form-urlencoded',
119
56
  ]
120
57
 
58
+ const contentType = request.headers.get('Content-Type')
59
+ const serovalPlugins = getSerovalPlugins()
60
+
61
+ function parsePayload(payload: any) {
62
+ const parsedPayload = fromJSON(payload, { plugins: serovalPlugins })
63
+ return parsedPayload
64
+ }
65
+
121
66
  const response = await (async () => {
122
67
  try {
123
68
  let result = await (async () => {
124
69
  // FormData
125
70
  if (
126
- request.headers.get('Content-Type') &&
127
- formDataContentTypes.some((type) =>
128
- request.headers.get('Content-Type')?.includes(type),
71
+ formDataContentTypes.some(
72
+ (type) => contentType && contentType.includes(type),
129
73
  )
130
74
  ) {
131
75
  // We don't support GET requests with FormData payloads... that seems impossible
@@ -133,45 +77,59 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
133
77
  method.toLowerCase() !== 'get',
134
78
  'GET requests with FormData payloads are not supported',
135
79
  )
80
+ const formData = await request.formData()
81
+ const serializedContext = formData.get(TSS_FORMDATA_CONTEXT)
82
+ formData.delete(TSS_FORMDATA_CONTEXT)
83
+
84
+ const params = {
85
+ context: {} as any,
86
+ data: formData,
87
+ }
88
+ if (typeof serializedContext === 'string') {
89
+ try {
90
+ params.context = parsePayload(JSON.parse(serializedContext))
91
+ } catch {}
92
+ }
136
93
 
137
- return await action(await request.formData(), signal)
94
+ return await action(params, signal)
138
95
  }
139
96
 
140
97
  // Get requests use the query string
141
98
  if (method.toLowerCase() === 'get') {
99
+ invariant(
100
+ isCreateServerFn,
101
+ 'expected GET request to originate from createServerFn',
102
+ )
142
103
  // By default the payload is the search params
143
- let payload: any = search
144
-
145
- // If this GET request was created by createServerFn,
146
- // then the payload will be on the payload param
147
- if (isCreateServerFn) {
148
- payload = search.payload
149
- }
150
-
104
+ let payload: any = search.payload
151
105
  // If there's a payload, we should try to parse it
152
- payload = payload ? await parsePayload(payload) : payload
106
+ payload = payload ? await parsePayload(JSON.parse(payload)) : payload
153
107
 
154
108
  // Send it through!
155
109
  return await action(payload, signal)
156
110
  }
157
111
 
158
- // This must be a POST request, likely JSON???
159
- const jsonPayloadAsString = await request.text()
112
+ if (method.toLowerCase() !== 'post') {
113
+ throw new Error('expected POST method')
114
+ }
115
+
116
+ if (!contentType || !contentType.includes('application/json')) {
117
+ throw new Error('expected application/json content type')
118
+ }
160
119
 
161
- // We should probably try to deserialize the payload
162
- // as JSON, but we'll just pass it through for now.
163
- const payload = await parsePayload(jsonPayloadAsString)
120
+ const jsonPayload = await request.json()
164
121
 
165
122
  // If this POST request was created by createServerFn,
166
- // it's payload will be the only argument
123
+ // its payload will be the only argument
167
124
  if (isCreateServerFn) {
125
+ const payload = await parsePayload(jsonPayload)
168
126
  return await action(payload, signal)
169
127
  }
170
128
 
171
129
  // Otherwise, we'll spread the payload. Need to
172
130
  // support `use server` functions that take multiple
173
131
  // arguments.
174
- return await action(...(payload as any), signal)
132
+ return await action(...jsonPayload)
175
133
  })()
176
134
 
177
135
  // Any time we get a Response back, we should just
@@ -192,18 +150,6 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
192
150
  }
193
151
  }
194
152
 
195
- // if (!search.createServerFn) {
196
- // result = result.result
197
- // }
198
-
199
- // else if (
200
- // isPlainObject(result) &&
201
- // 'result' in result &&
202
- // result.result instanceof Response
203
- // ) {
204
- // return result.result
205
- // }
206
-
207
153
  // TODO: RSCs Where are we getting this package?
208
154
  // if (isValidElement(result)) {
209
155
  // const { renderToPipeableStream } = await import(
@@ -228,16 +174,86 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
228
174
  }
229
175
 
230
176
  const response = getResponse()
231
- return new Response(
232
- result !== undefined ? startSerializer.stringify(result) : undefined,
233
- {
177
+ let nonStreamingBody: any = undefined
178
+
179
+ if (result !== undefined) {
180
+ // first run without the stream in case `result` does not need streaming
181
+ let done = false as boolean
182
+ const callbacks: {
183
+ onParse: (value: any) => void
184
+ onDone: () => void
185
+ onError: (error: any) => void
186
+ } = {
187
+ onParse: (value) => {
188
+ nonStreamingBody = value
189
+ },
190
+ onDone: () => {
191
+ done = true
192
+ },
193
+ onError: (error) => {
194
+ throw error
195
+ },
196
+ }
197
+ toCrossJSONStream(result, {
198
+ refs: new Map(),
199
+ plugins: serovalPlugins,
200
+ onParse(value) {
201
+ callbacks.onParse(value)
202
+ },
203
+ onDone() {
204
+ callbacks.onDone()
205
+ },
206
+ onError: (error) => {
207
+ callbacks.onError(error)
208
+ },
209
+ })
210
+ if (done) {
211
+ return new Response(
212
+ nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
213
+ {
214
+ status: response?.status,
215
+ statusText: response?.statusText,
216
+ headers: {
217
+ 'Content-Type': 'application/json',
218
+ [X_TSS_SERIALIZED]: 'true',
219
+ },
220
+ },
221
+ )
222
+ }
223
+
224
+ // not done yet, we need to stream
225
+ const stream = new ReadableStream({
226
+ start(controller) {
227
+ callbacks.onParse = (value) =>
228
+ controller.enqueue(JSON.stringify(value) + '\n')
229
+ callbacks.onDone = () => {
230
+ try {
231
+ controller.close()
232
+ } catch (error) {
233
+ controller.error(error)
234
+ }
235
+ }
236
+ callbacks.onError = (error) => controller.error(error)
237
+ // stream the initial body
238
+ if (nonStreamingBody !== undefined) {
239
+ callbacks.onParse(nonStreamingBody)
240
+ }
241
+ },
242
+ })
243
+ return new Response(stream, {
234
244
  status: response?.status,
235
245
  statusText: response?.statusText,
236
246
  headers: {
237
- 'Content-Type': 'application/json',
247
+ 'Content-Type': 'application/x-ndjson',
248
+ [X_TSS_SERIALIZED]: 'true',
238
249
  },
239
- },
240
- )
250
+ })
251
+ }
252
+
253
+ return new Response(undefined, {
254
+ status: response?.status,
255
+ statusText: response?.statusText,
256
+ })
241
257
  } catch (error: any) {
242
258
  if (error instanceof Response) {
243
259
  return error
@@ -265,10 +281,21 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
265
281
  console.error(error)
266
282
  console.info()
267
283
 
268
- return new Response(startSerializer.stringify(error), {
269
- status: 500,
284
+ const serializedError = JSON.stringify(
285
+ await Promise.resolve(
286
+ toCrossJSONAsync(error, {
287
+ refs: new Map(),
288
+ plugins: serovalPlugins,
289
+ }),
290
+ ),
291
+ )
292
+ const response = getResponse()
293
+ return new Response(serializedError, {
294
+ status: response?.status ?? 500,
295
+ statusText: response?.statusText,
270
296
  headers: {
271
297
  'Content-Type': 'application/json',
298
+ [X_TSS_SERIALIZED]: 'true',
272
299
  },
273
300
  })
274
301
  }
@@ -276,10 +303,6 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
276
303
 
277
304
  request.signal.removeEventListener('abort', abort)
278
305
 
279
- if (isRaw) {
280
- return response
281
- }
282
-
283
306
  return response
284
307
  }
285
308