Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/fifty-radios-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-aws/lambda": patch
---

Refactor `fromHttpApi` function in more effectful way
229 changes: 82 additions & 147 deletions packages/lambda/src/LambdaHandler.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -75,174 +74,75 @@ 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 <A, E>(effect: Effect.Effect<A, E, R>) => Promise<A>);
}

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<LambdaEvent, LambdaResult>;
// 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<string, string> = {};

// 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<typeof HttpApp.toWebHandler>;
const WebHandler = Context.GenericTag<WebHandler>("@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 = <LA, LE>(
layer: Layer.Layer<LA | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices, LE>,
options?: {
readonly middleware?: (
httpApp: HttpApp.Default,
) => HttpApp.Default<
never,
HttpApi.Api | HttpApiBuilder.Router | HttpRouter.HttpRouter.DefaultServices
>;
readonly memoMap?: Layer.MemoMap;
},
): Handler<LambdaEvent, LambdaResult> => {
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<never> | undefined) => Promise<Response>)
| undefined;

const handlerPromise = Effect.gen(function*() {
export const makeWebHandler = (options?: Pick<HttpApiOptions, "middleware">): 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<HttpRouter.HttpRouter.DefaultServices>();
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<LambdaEvent, LambdaResult>;
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,
body: requestValues.body,
},
);

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;
Expand All @@ -252,7 +152,9 @@ export const fromHttpApi = <LA, LE>(
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<string, string> = {};

Expand Down Expand Up @@ -280,7 +182,40 @@ export const fromHttpApi = <LA, LE>(
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 = <LA, LE>(
layer: Layer.Layer<LA | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices, LE>,
options?: HttpApiOptions,
): Handler<LambdaEvent, LambdaResult> => {
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 });
};
5 changes: 3 additions & 2 deletions packages/lambda/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type EffectHandler<T, R, E = never, A = void> = (
* @category model
*/
export type EffectHandlerWithLayer<T, R, E1 = never, E2 = never, A = void> = {
handler: EffectHandler<T, R, E1, A>;
layer: Layer.Layer<R, E2>;
readonly handler: EffectHandler<T, R, E1, A>;
readonly layer: Layer.Layer<R, E2>;
readonly memoMap?: Layer.MemoMap;
};