Skip to content
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

add module service adapter, unify route handling to handleRequestWithEdgeSpec #23

Merged
merged 9 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Create an `entrypoint.js` file:
import { startServer } from "edgespec/adapters/node"
import bundle from "./dist"

startServer(bundle, 3000)
startServer(bundle, { port: 3000 })
```

### WinterCG (Cloudflare Workers/Vercel Edge Functions)
Expand Down
24 changes: 20 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"ts-node": "^10.9.1",
"tsup": "^7.2.0",
"tsx": "^4.5.0",
"type-fest": "^4.9.0",
"typescript": "^5.3.2"
},
"dependencies": {
Expand Down
88 changes: 88 additions & 0 deletions src/adapters/module-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
type EdgeSpecAdapter,
type EdgeSpecRouteBundle,
handleRequestWithEdgeSpec,
} from "src/types/edge-spec"
import { EdgeSpecRequest, EdgeSpecRouteFn } from "src/types/web-handler"

export interface ModuleServiceOptions {
routeParam?: string
handleRouteParamNotFound?: EdgeSpecRouteFn
allowMatchingOnAnyCatchAllRouteParam?: boolean
}

export type ModuleService = (options?: ModuleServiceOptions) => EdgeSpecRouteFn

export const createModuleService: EdgeSpecAdapter<[], ModuleService> = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐧 I kinda like

Suggested change
export const createModuleService: EdgeSpecAdapter<[], ModuleService> = (
export const createEmbeddedService: EdgeSpecAdapter<[], EmbeddedService> = (

but don't want to bikeshed too much

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seveibar

will deal with this in follow-up discussions/PRs/RFCs

moduleServiceEdgeSpec
) => {
return (options) => async (request) => {
// cascade options down the edge spec chain
const edgeSpec: EdgeSpecRouteBundle = {
...request.edgeSpec,
...moduleServiceEdgeSpec,
...(options?.handleRouteParamNotFound && {
handleModuleServiceRouteNotFound: options?.handleRouteParamNotFound,
}),
}

const pathnameOverrideResult = getPathnameOverride(request, options ?? {})

if ("failed" in pathnameOverrideResult) {
return await pathnameOverrideResult.failed(request)
}

const response = await handleRequestWithEdgeSpec(
edgeSpec,
pathnameOverrideResult.pathnameOverride
)(request)

return response
}
}

function getPathnameOverride(
request: EdgeSpecRequest,
options: ModuleServiceOptions
):
| {
pathnameOverride: string | undefined
}
| {
failed: EdgeSpecRouteFn
} {
const {
routeParam,
handleRouteParamNotFound = request.edgeSpec
.handleModuleServiceRouteNotFound ??
(() => {
throw new Error("Module service route not found!")
}),
allowMatchingOnAnyCatchAllRouteParam = true,
} = options

let paths: string[] | undefined

if (routeParam) {
const candidate = request.pathParams?.[routeParam]

if (candidate && Array.isArray(candidate)) {
paths = candidate
}
}

if (!paths && allowMatchingOnAnyCatchAllRouteParam) {
for (const routes of Object.values(request.pathParams ?? {})) {
if (Array.isArray(routes)) {
paths = routes
break
}
}
}

if (!paths) {
return { failed: handleRouteParamNotFound }
}

return { pathnameOverride: "/" + paths.join("/") }
}
24 changes: 10 additions & 14 deletions src/adapters/node.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import http from "node:http"
import { transformToNodeBuilder } from "src/edge-runtime/transform-to-node"
import { EdgeSpecAdapter } from "src/types/edge-spec"
import { EdgeSpecRequest } from "src/types/web-handler"
import { EdgeSpecAdapter, handleRequestWithEdgeSpec } from "src/types/edge-spec"

export const startServer: EdgeSpecAdapter = (edgeSpec, port) => {
export interface EdgeSpecNodeAdapterOptions {
port?: number
}

export const startServer: EdgeSpecAdapter<[EdgeSpecNodeAdapterOptions]> = (
edgeSpec,
{ port }
) => {
const transformToNode = transformToNodeBuilder({
defaultOrigin: `http://localhost${port ? `:${port}` : ""}`,
})

const server = http.createServer(
transformToNode(async (fetchRequest: EdgeSpecRequest) => {
const { matchedRoute, routeParams } = edgeSpec.routeMatcher(
new URL(fetchRequest.url).pathname
)
const handler = edgeSpec.routeMapWithHandlers[matchedRoute]
fetchRequest.pathParams = routeParams

const fetchResponse: Response = await handler(fetchRequest)

return fetchResponse
})
transformToNode(handleRequestWithEdgeSpec(edgeSpec))
)
server.listen(port)

Expand Down
10 changes: 3 additions & 7 deletions src/adapters/wintercg-minimal.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { EdgeSpecAdapter } from "src/types/edge-spec"
import { EdgeSpecAdapter, handleRequestWithEdgeSpec } from "src/types/edge-spec"
import { EdgeSpecFetchEvent } from "src/types/web-handler"

export const addFetchListener: EdgeSpecAdapter = (edgeSpec) => {
addEventListener("fetch", async (event) => {
// TODO: find a better way to cast this
const fetchEvent = event as unknown as EdgeSpecFetchEvent

const { matchedRoute, routeParams } = edgeSpec.routeMatcher(
new URL(fetchEvent.request.url).pathname
fetchEvent.respondWith(
await handleRequestWithEdgeSpec(edgeSpec)(fetchEvent.request)
)
const handler = edgeSpec.routeMapWithHandlers[matchedRoute]
fetchEvent.request.pathParams = routeParams

fetchEvent.respondWith(await handler(fetchEvent.request))
})
}
2 changes: 1 addition & 1 deletion src/create-with-edge-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EdgeSpecRouteFn } from "./types/web-handler.js"
export const createWithEdgeSpec = (globalSpec: SetupParams) => {
return (routeSpec: any) =>
(routeFn: EdgeSpecRouteFn): EdgeSpecRouteFn =>
async (req: Request) => {
async (req) => {
// Identify environment this is being executed in and convert to WinterCG-
// compatible request

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./types/edge-spec.js"
export * from "./codegen/generate-module-code.js"
export * from "./create-with-edge-spec.js"
64 changes: 30 additions & 34 deletions src/serve/create-server-from-route-map.ts
mxsdev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,44 @@ import {
transformToNodeBuilder,
} from "src/edge-runtime/transform-to-node.js"
import { EdgeSpecRouteFn } from "src/types/web-handler.js"
import {
EdgeSpecRouteBundle,
EdgeSpecOptions,
handleRequestWithEdgeSpec,
} from "src/types/edge-spec.js"

export const createServerFromRouteMap = async (
export const createEdgeSpecFromRouteMap = (
routeMap: Record<string, EdgeSpecRouteFn>,
transformToNodeOptions: TransformToNodeOptions
) => {
edgeSpecOptions?: Partial<EdgeSpecOptions>
): EdgeSpecRouteBundle => {
const formattedRoutes = normalizeRouteMap(routeMap)

const routeMatcher = getRouteMatcher(Object.keys(formattedRoutes))

const transformToNode = transformToNodeBuilder(transformToNodeOptions)
const routeMapWithHandlers = Object.fromEntries(
Object.entries(formattedRoutes).map(([routeFormatted, route]) => [
routeFormatted,
routeMap[route],
])
)

const server = createServer(
transformToNode(async (req) => {
// TODO: put routeParams on request object...
const { matchedRoute, routeParams } =
routeMatcher(new URL(req.url).pathname) ?? {}
const routeFn =
matchedRoute &&
formattedRoutes[matchedRoute] &&
routeMap[formattedRoutes[matchedRoute]]
return {
routeMatcher,
routeMapWithHandlers,
...edgeSpecOptions,
}
}

export const createNodeServerFromRouteMap = async (
routeMap: Record<string, EdgeSpecRouteFn>,
transformToNodeOptions: TransformToNodeOptions,
edgeSpecOptions?: Partial<EdgeSpecOptions>
) => {
const edgeSpec = createEdgeSpecFromRouteMap(routeMap, edgeSpecOptions)

if (!matchedRoute || !routeFn) {
console.log({
matchedRoute,
formattedRoutes,
routeMap,
url: req.url,
transformToNodeOptions,
})
return new Response("Not found", {
status: 404,
})
}
const transformToNode = transformToNodeBuilder(transformToNodeOptions)

try {
return await routeFn(req)
} catch (e: any) {
return new Response(e.toString(), {
status: 500,
})
}
})
const server = createServer(
transformToNode(handleRequestWithEdgeSpec(edgeSpec))
)

return server
Expand Down
75 changes: 62 additions & 13 deletions src/types/edge-spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,67 @@
import type { EdgeSpecRouteFn, EdgeSpecRouteParams } from "./web-handler"
import {
createEdgeSpecRequest,
type EdgeSpecRouteFn,
type EdgeSpecRouteParams,
} from "./web-handler.js"

export type EdgeSpecRouteMatcher = (pathname: string) => {
matchedRoute: string
routeParams: EdgeSpecRouteParams
}
import type { ReadonlyDeep } from "type-fest"

export type EdgeSpecRouteMatcher = (pathname: string) =>
| {
matchedRoute: string
routeParams: EdgeSpecRouteParams
}
| undefined
| null

export interface EdgeSpec {
export type EdgeSpecRouteMap = Record<string, EdgeSpecRouteFn>

export interface EdgeSpecOptions {
routeMatcher: EdgeSpecRouteMatcher
routeMapWithHandlers: {
[route: string]: EdgeSpecRouteFn
}
routeMapWithHandlers: EdgeSpecRouteMap

handleModuleServiceRouteNotFound?: EdgeSpecRouteFn
handle404?: EdgeSpecRouteFn
}

export type EdgeSpecAdapter<ReturnValue = void> = (
edgeSpec: EdgeSpec,
port?: number
) => ReturnValue
// make this deeply immutable to force usage through helper functions
export type EdgeSpecRouteBundle = ReadonlyDeep<EdgeSpecOptions>

mxsdev marked this conversation as resolved.
Show resolved Hide resolved
export type EdgeSpecAdapter<
Options extends Array<unknown> = [],
ReturnValue = void,
> = (edgeSpec: EdgeSpecRouteBundle, ...options: Options) => ReturnValue

export function handleRequestWithEdgeSpec(
edgeSpec: EdgeSpecRouteBundle,
pathnameOverride?: string
): (request: Request) => Promise<Response> {
return async (request: Request) => {
const {
routeMatcher,
routeMapWithHandlers,
handle404 = () =>
new Response("Not found", {
status: 404,
}),
} = edgeSpec

const pathname = pathnameOverride ?? new URL(request.url).pathname
const { matchedRoute, routeParams } = routeMatcher(pathname) ?? {}

const routeFn = matchedRoute && routeMapWithHandlers[matchedRoute]

const edgeSpecRequest = createEdgeSpecRequest(request, {
edgeSpec,
pathParams: routeParams,
})

if (!routeFn) {
return await handle404(edgeSpecRequest)
}

const response: Response = await routeFn(edgeSpecRequest)

return response
}
}
Loading
Loading