Skip to content

Commit

Permalink
fix: isomorphic server exec
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Feb 27, 2023
1 parent 36c4124 commit 82eda8b
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 126 deletions.
11 changes: 8 additions & 3 deletions packages/bling/src/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ function transformServer({ types: t, template }) {
server$.registerHandler("${pathname}", $$server_module${serverIndex});
`)({
source: serverFn.node,
options: serverFnOpts.node,
options:
serverFnOpts?.node || t.identifier('undefined'),
})
)
} else {
Expand All @@ -221,9 +222,13 @@ function transformServer({ types: t, template }) {
process.env.TEST_ENV === 'client'
? {
source: serverFn.node,
options: serverFnOpts.node,
options:
serverFnOpts?.node || t.identifier('undefined'),
}
: {
options:
serverFnOpts?.node || t.identifier('undefined'),
}
: {}
)
)
}
Expand Down
113 changes: 68 additions & 45 deletions packages/bling/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {
createFetcher,
mergeRequestInits,
mergeServerOpts,
parseResponse,
payloadRequestInit,
resolveRequestHref,
XBlingOrigin,
XBlingResponseTypeHeader,
} from './utils/utils'

import type {
AnyServerFn,
Serializer,
ServerFnOpts,
Fetcher,
CreateFetcherFn,
FetcherFn,
FetcherMethods,
ServerFnReturn,
ServerFnCtxOptions,
ServerFnCtx,
} from './types'

export * from './utils/utils'
Expand All @@ -25,58 +28,78 @@ export function addSerializer({ apply, serialize }: Serializer) {
serializers.push({ apply, serialize })
}

export type ClientFetcherMethods = {
createFetcher(route: string, defualtOpts: ServerFnOpts): Fetcher<any>
export type CreateClientFetcherFn = <T extends AnyServerFn>(
fn: T,
opts?: ServerFnCtxOptions
) => ClientFetcher<T>

export type CreateClientFetcherMethods = {
createFetcher(
route: string,
defualtOpts: ServerFnCtxOptions
): ClientFetcher<any>
}

export type ClientFetcher<T extends AnyServerFn> = FetcherFn<T> &
FetcherMethods<T>

export type ClientFetcherMethods<T extends AnyServerFn> = FetcherMethods<T> & {
fetch: (
init: RequestInit,
opts?: ServerFnCtxOptions
) => Promise<Awaited<ServerFnReturn<T>>>
}

export type ClientServerFn = CreateFetcherFn & ClientFetcherMethods
export type ClientServerFn = CreateClientFetcherFn & CreateClientFetcherMethods

const serverImpl = (() => {
throw new Error('Should be compiled away')
}) as any

const serverMethods: ClientFetcherMethods = {
createFetcher: (pathname: string, defaultOpts?: ServerFnOpts) => {
return createFetcher(
pathname,
async (payload: any, opts?: ServerFnOpts) => {
const method = opts?.method || defaultOpts?.method || 'POST'
const baseInit: RequestInit = {
method,
headers: {
[XBlingOrigin]: 'client',
},
}

let payloadInit = payloadRequestInit(payload, serializers)

const resolvedRoute =
method === 'GET'
? payloadInit.body === 'string'
? `${pathname}?payload=${encodeURIComponent(payloadInit.body)}`
: pathname
: pathname

const request = new Request(
new URL(resolvedRoute, window.location.href).href,
mergeRequestInits(
baseInit,
payloadInit,
defaultOpts?.request,
opts?.request
)
const serverMethods: CreateClientFetcherMethods = {
createFetcher: (pathname: string, defaultOpts?: ServerFnCtxOptions) => {
const fetcherImpl = async (payload: any, opts?: ServerFnCtxOptions) => {
const method = opts?.method || defaultOpts?.method || 'POST'

const baseInit: RequestInit = {
method,
headers: {
[XBlingOrigin]: 'client',
},
}

let payloadInit = payloadRequestInit(payload, serializers)

const resolvedHref = resolveRequestHref(pathname, method, payloadInit)

const request = new Request(
resolvedHref,
mergeRequestInits(
baseInit,
payloadInit,
defaultOpts?.request,
opts?.request
)
)

const response = await fetch(request)
const response = await fetch(request)

// // throws response, error, form error, json object, string
if (response.headers.get(XBlingResponseTypeHeader) === 'throw') {
throw await parseResponse(response)
} else {
return await parseResponse(response)
}
// // throws response, error, form error, json object, string
if (response.headers.get(XBlingResponseTypeHeader) === 'throw') {
throw await parseResponse(response)
} else {
return await parseResponse(response)
}
)
}

const fetcherMethods: ClientFetcherMethods<any> = {
url: pathname,
fetch: (request: RequestInit, opts?: ServerFnCtxOptions) => {
return fetcherImpl(undefined, mergeServerOpts({ request }, opts))
},
}

return Object.assign(fetcherImpl, fetcherMethods) as ClientFetcher<any>
},
}

Expand Down
150 changes: 82 additions & 68 deletions packages/bling/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ import {
mergeRequestInits,
parseResponse,
payloadRequestInit,
resolveRequestHref,
} from './utils/utils'
import type {
AnyServerFn,
Deserializer,
ServerFnOpts,
Fetcher,
ServerFnCtx,
CreateFetcherFn,
ServerFnCtxOptions,
ServerFnCtxWithRequest,
} from './types'
import { ClientServerFn } from './client'

export * from './utils/utils'

Expand All @@ -33,9 +34,10 @@ export function addDeserializer(deserializer: Deserializer) {
export type ServerFetcherMethods = {
createHandler(
fn: AnyServerFn,
route: string,
opts: ServerFnOpts
pathame: string,
opts: ServerFnCtxOptions
): Fetcher<any>
registerHandler(pathname: string, handler: Fetcher<any>): void
}

export type ServerFn = CreateFetcherFn & ServerFetcherMethods
Expand All @@ -48,20 +50,27 @@ const serverMethods: ServerFetcherMethods = {
createHandler: (
fn: AnyServerFn,
pathname: string,
defaultOpts?: ServerFnOpts
defaultOpts?: ServerFnCtxOptions
): Fetcher<any> => {
return createFetcher(
pathname,
async (payload: any, opts?: ServerFnOpts) => {
console.log(`Executing server function: ${pathname}`)
if (payload) console.log(` Fn Payload: ${payload}`)
return createFetcher(pathname, async (payload: any, opts?: ServerFnCtx) => {
const method = opts?.method || defaultOpts?.method || 'POST'
const ssr = !opts?.request

let payloadInit = payloadRequestInit(payload, false)
console.log(`Executing server function: ${method} ${pathname}`)
if (payload) console.log(` Fn Payload: ${payload}`)

opts = opts ?? {}

if (!opts.__hasRequest) {
// This will happen if the server function is called directly during SSR
// Even though we're not crossing the network, we still need to
// create a Request object to pass to the server function
const request = new Request(
pathname,
// create a Request object to pass to the server function as if it was

let payloadInit = payloadRequestInit(payload, false)

const resolvedHref = resolveRequestHref(pathname, method, payloadInit)
opts.request = new Request(
resolvedHref,
mergeRequestInits(
{
method: 'POST',
Expand All @@ -74,40 +83,73 @@ const serverMethods: ServerFetcherMethods = {
opts?.request
)
)
}

try {
// Do the same parsing of the result as we do on the client
const response = await fn(payload, opts)

try {
// Do the same parsing of the result as we do on the client
return parseResponse(
await fn(payload, {
request: request,
})
if (!opts.__hasRequest) {
// If we're on the server during SSR, we can skip to
// parsing the response directly
return parseResponse(response)
}

// Otherwise, the client-side code will parse the response properly
return response
} catch (e) {
if (e instanceof Error && /[A-Za-z]+ is not defined/.test(e.message)) {
const error = new Error(
e.message +
'\n' +
' You probably are using a variable defined in a closure in your server function. Make sure you pass any variables needed to the server function as arguments. These arguments must be serializable.'
)
} catch (e) {
if (
e instanceof Error &&
/[A-Za-z]+ is not defined/.test(e.message)
) {
const error = new Error(
e.message +
'\n' +
' You probably are using a variable defined in a closure in your server function. Make sure you pass any variables needed to the server function as arguments. These arguments must be serializable.'
)
error.stack = e.stack ?? ''
throw error
}
throw e
error.stack = e.stack ?? ''
throw error
}
throw e
}
)
})
},
registerHandler(pathname: string, handler: Fetcher<any>): any {
console.log('Registering handler', pathname)
handlers.set(pathname, handler)
},
}

export const server$: ServerFn = Object.assign(serverImpl, serverMethods)

async function parseRequest(event: ServerFnCtx) {
export async function handleEvent(ctx: ServerFnCtxWithRequest) {
if (!ctx.request) {
throw new Error('handleEvent must be called with a request.')
}

const url = new URL(ctx.request.url)

if (hasHandler(url.pathname)) {
try {
let [pathname, payload] = await parseRequest(ctx)
let handler = getHandler(pathname)
if (!handler) {
throw {
status: 404,
message: 'Handler Not Found for ' + pathname,
}
}
const data = await handler(payload, ctx)
return respondWith(ctx, data, 'return')
} catch (error) {
return respondWith(ctx, error as Error, 'throw')
}
}

return null
}

async function parseRequest(event: ServerFnCtxWithRequest) {
let request = event.request
let contentType = request.headers.get(ContentTypeHeader)
let name = new URL(request.url).pathname,
let pathname = new URL(request.url).pathname,
payload

// Get request have their payload in the query string
Expand Down Expand Up @@ -144,18 +186,18 @@ async function parseRequest(event: ServerFnCtx) {
}
}

return [name, payload]
return [pathname, payload]
}

function respondWith(
{ request }: ServerFnCtx,
ctx: ServerFnCtxWithRequest,
data: Response | Error | string | object,
responseType: 'throw' | 'return'
) {
if (data instanceof Response) {
if (
isRedirectResponse(data) &&
request.headers.get(XBlingOrigin) === 'client'
ctx.request.headers.get(XBlingOrigin) === 'client'
) {
let headers = new Headers(data.headers)
headers.set(XBlingOrigin, 'server')
Expand Down Expand Up @@ -231,36 +273,8 @@ function respondWith(
})
}

export async function handleEvent(ctx: ServerFnCtx) {
const url = new URL(ctx.request.url)

if (hasHandler(url.pathname)) {
try {
let [name, payload] = await parseRequest(ctx)
let handler = getHandler(name)
if (!handler) {
throw {
status: 404,
message: 'Handler Not Found for ' + name,
}
}
const data = await handler(payload, ctx)
return respondWith(ctx, data, 'return')
} catch (error) {
return respondWith(ctx, error as Error, 'throw')
}
}

return null
}

const handlers = new Map<string, Fetcher<any>>()

export function registerHandler(pathname: string, handler: Fetcher<any>): any {
console.log('Registering handler', pathname)
handlers.set(pathname, handler)
}

export function getHandler(pathname: string) {
return handlers.get(pathname)
}
Expand Down
Loading

0 comments on commit 82eda8b

Please sign in to comment.