diff --git a/.changeset/fifty-radios-heal.md b/.changeset/fifty-radios-heal.md new file mode 100644 index 00000000..13aecaf7 --- /dev/null +++ b/.changeset/fifty-radios-heal.md @@ -0,0 +1,5 @@ +--- +"@effect-aws/lambda": patch +--- + +Refactor `fromHttpApi` function in more effectful way diff --git a/packages/lambda/src/LambdaHandler.ts b/packages/lambda/src/LambdaHandler.ts index f4718e0f..431bfe49 100644 --- a/packages/lambda/src/LambdaHandler.ts +++ b/packages/lambda/src/LambdaHandler.ts @@ -1,11 +1,10 @@ /** * @since 1.4.0 */ -import type { HttpApi, HttpRouter } from "@effect/platform"; +import type { HttpApi, HttpRouter, HttpServerError } from "@effect/platform"; import { HttpApiBuilder, HttpApp } from "@effect/platform"; -import type { Context as LambdaContext } from "aws-lambda"; -import type { Context } from "effect"; -import { Effect, Function, Layer } from "effect"; +import type { Cause } from "effect"; +import { Context, Effect, Function, Layer } from "effect"; import { getEventSource } from "./internal/index.js"; import type { EventSource, LambdaEvent, LambdaResult } from "./internal/types.js"; import { encodeBase64, isContentEncodingBinary, isContentTypeBinary } from "./internal/utils.js"; @@ -75,164 +74,67 @@ export const make: { // Deprecated case if (globalLayer) { const runtime = LambdaRuntime.fromLayer(globalLayer); - return async (event: T, context: LambdaContext) => handlerOrOptions(event, context).pipe(runtime.runPromise); + return async (event, context) => handlerOrOptions(event, context).pipe(runtime.runPromise); } - return async (event: T, context: LambdaContext) => + return async (event, context) => handlerOrOptions(event, context).pipe(Effect.runPromise as (effect: Effect.Effect) => Promise); } - const runtime = LambdaRuntime.fromLayer(handlerOrOptions.layer); - return async (event: T, context: LambdaContext) => handlerOrOptions.handler(event, context).pipe(runtime.runPromise); + const runtime = LambdaRuntime.fromLayer(handlerOrOptions.layer, { memoMap: handlerOrOptions.memoMap }); + return async (event, context) => handlerOrOptions.handler(event, context).pipe(runtime.runPromise); }; -// const apiHandler = (options?: { -// readonly middleware?: ( -// httpApp: HttpApp.Default, -// ) => HttpApp.Default< -// never, -// HttpApi.Api | HttpApiBuilder.Router | HttpRouter.HttpRouter.DefaultServices -// >; -// }): EffectHandler< -// LambdaEvent, -// HttpApi.Api | HttpApiBuilder.Router | HttpApiBuilder.Middleware | HttpRouter.HttpRouter.DefaultServices, -// HttpServerError.ResponseError, -// LambdaResult -// > => -// (event) => -// Effect.gen(function*() { -// const eventSource = getEventSource(event) as EventSource; -// const requestValues = eventSource.getRequest(event); +interface HttpApiOptions { + readonly middleware?: ( + httpApp: HttpApp.Default, + ) => HttpApp.Default< + never, + HttpApi.Api | HttpApiBuilder.Router | HttpRouter.HttpRouter.DefaultServices + >; + readonly memoMap?: Layer.MemoMap; +} -// const req = new Request( -// `https://${requestValues.remoteAddress}${requestValues.path}`, -// { -// method: requestValues.method, -// headers: requestValues.headers, -// body: requestValues.body, -// }, -// ); - -// const app = yield* HttpApiBuilder.httpApp; - -// const appWithMiddleware = options?.middleware ? options.middleware(app as any) : app; - -// const request = HttpServerRequest.fromWeb(req); -// const response = yield* appWithMiddleware.pipe( -// Effect.provideService(HttpServerRequest.HttpServerRequest, request), -// ); - -// const handler = yield* FiberRef.get(HttpApp.currentPreResponseHandlers); - -// const resp = Option.isSome(handler) ? yield* handler.value(request, response) : response; - -// const res = HttpServerResponse.toWeb(resp, { runtime: yield* Effect.runtime() }); - -// const contentType = res.headers.get("content-type"); -// let isBase64Encoded = contentType && isContentTypeBinary(contentType) ? true : false; - -// if (!isBase64Encoded) { -// const contentEncoding = res.headers.get("content-encoding"); -// isBase64Encoded = isContentEncodingBinary(contentEncoding); -// } - -// const body = isBase64Encoded -// ? encodeBase64(yield* Effect.promise(() => res.arrayBuffer())) -// : yield* Effect.promise(() => res.text()); - -// const headers: Record = {}; - -// if (res.headers.has("set-cookie")) { -// const cookies = res.headers.getSetCookie -// ? res.headers.getSetCookie() -// : Array.from((res.headers as any).entries()) -// .filter(([k]: any) => k === "set-cookie") -// .map(([, v]: any) => v); - -// if (Array.isArray(cookies)) { -// headers["set-cookie"] = cookies.join(", "); -// res.headers.delete("set-cookie"); -// } -// } - -// res.headers.forEach((value, key) => { -// headers[key] = value; -// }); - -// return eventSource.getResponse({ -// event, -// statusCode: res.status, -// body, -// headers, -// isBase64Encoded, -// }); -// }); +type WebHandler = ReturnType; +const WebHandler = Context.GenericTag("@effect-aws/lambda/WebHandler"); /** - * Construct a lambda handler from an `HttpApi` instance. - * - * @example - * ```ts - * import { LambdaHandler } from "@effect-aws/lambda" - * import { HttpApi, HttpApiBuilder, HttpServer } from "@effect/platform" - * import { Layer } from "effect" - * - * class MyApi extends HttpApi.make("api") {} - * - * const MyApiLive = HttpApiBuilder.api(MyApi) - * - * export const handler = LambdaHandler.fromHttpApi( - * Layer.mergeAll( - * MyApiLive, - * // you could also use NodeHttpServer.layerContext, depending on your - * // server's platform - * HttpServer.layerContext - * ) - * ) - * ``` + * Construct an `WebHandler` from an `HttpApi` instance. * * @since 1.4.0 * @category constructors */ -export const fromHttpApi = ( - layer: Layer.Layer, - options?: { - readonly middleware?: ( - httpApp: HttpApp.Default, - ) => HttpApp.Default< - never, - HttpApi.Api | HttpApiBuilder.Router | HttpRouter.HttpRouter.DefaultServices - >; - readonly memoMap?: Layer.MemoMap; - }, -): Handler => { - const runtime = LambdaRuntime.fromLayer( - Layer.mergeAll(layer, HttpApiBuilder.Router.Live, HttpApiBuilder.Middleware.layer), - options, - ); - // // Alternative implementation (I keep it commented here to understand the differences) - // const handler = apiHandler(options); - // return async (event: LambdaEvent, context: LambdaContext) => handler(event, context).pipe(runtime.runPromise); - let handlerCached: - | ((request: Request, context?: Context.Context | undefined) => Promise) - | undefined; - - const handlerPromise = Effect.gen(function*() { +export const makeWebHandler = (options?: Pick): Effect.Effect< + WebHandler, + never, + HttpApiBuilder.Router | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices | HttpApiBuilder.Middleware +> => + Effect.gen(function*() { const app = yield* HttpApiBuilder.httpApp; - const rt = yield* runtime.runtimeEffect; - const handler = HttpApp.toWebHandlerRuntime(rt)( + const rt = yield* Effect.runtime(); + return HttpApp.toWebHandlerRuntime(rt)( options?.middleware ? options.middleware(app as any) as any : app, ); - handlerCached = handler; - return handler; - }).pipe(runtime.runPromise); + }); - async function handler(event: LambdaEvent) { +/** + * Construct an `EffectHandler` from an `HttpApi` instance. + * + * @since 1.4.0 + * @category constructors + */ +export const httpApiHandler: EffectHandler< + LambdaEvent, + WebHandler, + HttpServerError.ResponseError | Cause.UnknownException, + LambdaResult +> = (event) => + Effect.gen(function*() { const eventSource = getEventSource(event) as EventSource; const requestValues = eventSource.getRequest(event); - const request = new Request( - `http://${requestValues.remoteAddress}${requestValues.path}`, + const req = new Request( + `https://${requestValues.remoteAddress}${requestValues.path}`, { method: requestValues.method, headers: requestValues.headers, @@ -240,9 +142,7 @@ export const fromHttpApi = ( }, ); - const res = handlerCached !== undefined - ? await handlerCached(request) - : await handlerPromise.then((handler) => handler(request)); + const res = yield* WebHandler.pipe(Effect.andThen((handler) => handler(req))); const contentType = res.headers.get("content-type"); let isBase64Encoded = contentType && isContentTypeBinary(contentType) ? true : false; @@ -252,7 +152,9 @@ export const fromHttpApi = ( isBase64Encoded = isContentEncodingBinary(contentEncoding); } - const body = isBase64Encoded ? encodeBase64(await res.arrayBuffer()) : await res.text(); + const body = isBase64Encoded + ? encodeBase64(yield* Effect.promise(() => res.arrayBuffer())) + : yield* Effect.promise(() => res.text()); const headers: Record = {}; @@ -280,7 +182,40 @@ export const fromHttpApi = ( headers, isBase64Encoded, }); - } + }); - return handler; +/** + * Construct a lambda handler from an `HttpApi` instance. + * + * @example + * ```ts + * import { LambdaHandler } from "@effect-aws/lambda" + * import { HttpApi, HttpApiBuilder, HttpServer } from "@effect/platform" + * import { Layer } from "effect" + * + * class MyApi extends HttpApi.make("api") {} + * + * const MyApiLive = HttpApiBuilder.api(MyApi) + * + * export const handler = LambdaHandler.fromHttpApi( + * Layer.mergeAll( + * MyApiLive, + * // you could also use NodeHttpServer.layerContext, depending on your + * // server's platform + * HttpServer.layerContext + * ) + * ) + * ``` + * + * @since 1.4.0 + * @category constructors + */ +export const fromHttpApi = ( + layer: Layer.Layer, + options?: HttpApiOptions, +): Handler => { + const httpApiLayer = Layer.effect(WebHandler, makeWebHandler(options)).pipe( + Layer.provide(Layer.mergeAll(layer, HttpApiBuilder.Router.Live, HttpApiBuilder.Middleware.layer)), + ); + return make({ handler: httpApiHandler, layer: httpApiLayer, memoMap: options?.memoMap }); }; diff --git a/packages/lambda/src/Types.ts b/packages/lambda/src/Types.ts index 93d91c1d..70372d8f 100644 --- a/packages/lambda/src/Types.ts +++ b/packages/lambda/src/Types.ts @@ -37,6 +37,7 @@ export type EffectHandler = ( * @category model */ export type EffectHandlerWithLayer = { - handler: EffectHandler; - layer: Layer.Layer; + readonly handler: EffectHandler; + readonly layer: Layer.Layer; + readonly memoMap?: Layer.MemoMap; };