Skip to content

Commit 2229139

Browse files
authored
refactor: improve code ergonomics (#153)
* refactor: improve code ergonomics * docs: add changeset
1 parent 212e6d7 commit 2229139

File tree

3 files changed

+90
-149
lines changed

3 files changed

+90
-149
lines changed

.changeset/fifty-radios-heal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect-aws/lambda": patch
3+
---
4+
5+
Refactor `fromHttpApi` function in more effectful way
Lines changed: 82 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/**
22
* @since 1.4.0
33
*/
4-
import type { HttpApi, HttpRouter } from "@effect/platform";
4+
import type { HttpApi, HttpRouter, HttpServerError } from "@effect/platform";
55
import { HttpApiBuilder, HttpApp } from "@effect/platform";
6-
import type { Context as LambdaContext } from "aws-lambda";
7-
import type { Context } from "effect";
8-
import { Effect, Function, Layer } from "effect";
6+
import type { Cause } from "effect";
7+
import { Context, Effect, Function, Layer } from "effect";
98
import { getEventSource } from "./internal/index.js";
109
import type { EventSource, LambdaEvent, LambdaResult } from "./internal/types.js";
1110
import { encodeBase64, isContentEncodingBinary, isContentTypeBinary } from "./internal/utils.js";
@@ -75,174 +74,75 @@ export const make: {
7574
// Deprecated case
7675
if (globalLayer) {
7776
const runtime = LambdaRuntime.fromLayer(globalLayer);
78-
return async (event: T, context: LambdaContext) => handlerOrOptions(event, context).pipe(runtime.runPromise);
77+
return async (event, context) => handlerOrOptions(event, context).pipe(runtime.runPromise);
7978
}
8079

81-
return async (event: T, context: LambdaContext) =>
80+
return async (event, context) =>
8281
handlerOrOptions(event, context).pipe(Effect.runPromise as <A, E>(effect: Effect.Effect<A, E, R>) => Promise<A>);
8382
}
8483

85-
const runtime = LambdaRuntime.fromLayer(handlerOrOptions.layer);
86-
return async (event: T, context: LambdaContext) => handlerOrOptions.handler(event, context).pipe(runtime.runPromise);
84+
const runtime = LambdaRuntime.fromLayer(handlerOrOptions.layer, { memoMap: handlerOrOptions.memoMap });
85+
return async (event, context) => handlerOrOptions.handler(event, context).pipe(runtime.runPromise);
8786
};
8887

89-
// const apiHandler = (options?: {
90-
// readonly middleware?: (
91-
// httpApp: HttpApp.Default,
92-
// ) => HttpApp.Default<
93-
// never,
94-
// HttpApi.Api | HttpApiBuilder.Router | HttpRouter.HttpRouter.DefaultServices
95-
// >;
96-
// }): EffectHandler<
97-
// LambdaEvent,
98-
// HttpApi.Api | HttpApiBuilder.Router | HttpApiBuilder.Middleware | HttpRouter.HttpRouter.DefaultServices,
99-
// HttpServerError.ResponseError,
100-
// LambdaResult
101-
// > =>
102-
// (event) =>
103-
// Effect.gen(function*() {
104-
// const eventSource = getEventSource(event) as EventSource<LambdaEvent, LambdaResult>;
105-
// const requestValues = eventSource.getRequest(event);
88+
interface HttpApiOptions {
89+
readonly middleware?: (
90+
httpApp: HttpApp.Default,
91+
) => HttpApp.Default<
92+
never,
93+
HttpApi.Api | HttpApiBuilder.Router | HttpRouter.HttpRouter.DefaultServices
94+
>;
95+
readonly memoMap?: Layer.MemoMap;
96+
}
10697

107-
// const req = new Request(
108-
// `https://${requestValues.remoteAddress}${requestValues.path}`,
109-
// {
110-
// method: requestValues.method,
111-
// headers: requestValues.headers,
112-
// body: requestValues.body,
113-
// },
114-
// );
115-
116-
// const app = yield* HttpApiBuilder.httpApp;
117-
118-
// const appWithMiddleware = options?.middleware ? options.middleware(app as any) : app;
119-
120-
// const request = HttpServerRequest.fromWeb(req);
121-
// const response = yield* appWithMiddleware.pipe(
122-
// Effect.provideService(HttpServerRequest.HttpServerRequest, request),
123-
// );
124-
125-
// const handler = yield* FiberRef.get(HttpApp.currentPreResponseHandlers);
126-
127-
// const resp = Option.isSome(handler) ? yield* handler.value(request, response) : response;
128-
129-
// const res = HttpServerResponse.toWeb(resp, { runtime: yield* Effect.runtime() });
130-
131-
// const contentType = res.headers.get("content-type");
132-
// let isBase64Encoded = contentType && isContentTypeBinary(contentType) ? true : false;
133-
134-
// if (!isBase64Encoded) {
135-
// const contentEncoding = res.headers.get("content-encoding");
136-
// isBase64Encoded = isContentEncodingBinary(contentEncoding);
137-
// }
138-
139-
// const body = isBase64Encoded
140-
// ? encodeBase64(yield* Effect.promise(() => res.arrayBuffer()))
141-
// : yield* Effect.promise(() => res.text());
142-
143-
// const headers: Record<string, string> = {};
144-
145-
// if (res.headers.has("set-cookie")) {
146-
// const cookies = res.headers.getSetCookie
147-
// ? res.headers.getSetCookie()
148-
// : Array.from((res.headers as any).entries())
149-
// .filter(([k]: any) => k === "set-cookie")
150-
// .map(([, v]: any) => v);
151-
152-
// if (Array.isArray(cookies)) {
153-
// headers["set-cookie"] = cookies.join(", ");
154-
// res.headers.delete("set-cookie");
155-
// }
156-
// }
157-
158-
// res.headers.forEach((value, key) => {
159-
// headers[key] = value;
160-
// });
161-
162-
// return eventSource.getResponse({
163-
// event,
164-
// statusCode: res.status,
165-
// body,
166-
// headers,
167-
// isBase64Encoded,
168-
// });
169-
// });
98+
type WebHandler = ReturnType<typeof HttpApp.toWebHandler>;
99+
const WebHandler = Context.GenericTag<WebHandler>("@effect-aws/lambda/WebHandler");
170100

171101
/**
172-
* Construct a lambda handler from an `HttpApi` instance.
173-
*
174-
* @example
175-
* ```ts
176-
* import { LambdaHandler } from "@effect-aws/lambda"
177-
* import { HttpApi, HttpApiBuilder, HttpServer } from "@effect/platform"
178-
* import { Layer } from "effect"
179-
*
180-
* class MyApi extends HttpApi.make("api") {}
181-
*
182-
* const MyApiLive = HttpApiBuilder.api(MyApi)
183-
*
184-
* export const handler = LambdaHandler.fromHttpApi(
185-
* Layer.mergeAll(
186-
* MyApiLive,
187-
* // you could also use NodeHttpServer.layerContext, depending on your
188-
* // server's platform
189-
* HttpServer.layerContext
190-
* )
191-
* )
192-
* ```
102+
* Construct an `WebHandler` from an `HttpApi` instance.
193103
*
194104
* @since 1.4.0
195105
* @category constructors
196106
*/
197-
export const fromHttpApi = <LA, LE>(
198-
layer: Layer.Layer<LA | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices, LE>,
199-
options?: {
200-
readonly middleware?: (
201-
httpApp: HttpApp.Default,
202-
) => HttpApp.Default<
203-
never,
204-
HttpApi.Api | HttpApiBuilder.Router | HttpRouter.HttpRouter.DefaultServices
205-
>;
206-
readonly memoMap?: Layer.MemoMap;
207-
},
208-
): Handler<LambdaEvent, LambdaResult> => {
209-
const runtime = LambdaRuntime.fromLayer(
210-
Layer.mergeAll(layer, HttpApiBuilder.Router.Live, HttpApiBuilder.Middleware.layer),
211-
options,
212-
);
213-
// // Alternative implementation (I keep it commented here to understand the differences)
214-
// const handler = apiHandler(options);
215-
// return async (event: LambdaEvent, context: LambdaContext) => handler(event, context).pipe(runtime.runPromise);
216-
let handlerCached:
217-
| ((request: Request, context?: Context.Context<never> | undefined) => Promise<Response>)
218-
| undefined;
219-
220-
const handlerPromise = Effect.gen(function*() {
107+
export const makeWebHandler = (options?: Pick<HttpApiOptions, "middleware">): Effect.Effect<
108+
WebHandler,
109+
never,
110+
HttpApiBuilder.Router | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices | HttpApiBuilder.Middleware
111+
> =>
112+
Effect.gen(function*() {
221113
const app = yield* HttpApiBuilder.httpApp;
222-
const rt = yield* runtime.runtimeEffect;
223-
const handler = HttpApp.toWebHandlerRuntime(rt)(
114+
const rt = yield* Effect.runtime<HttpRouter.HttpRouter.DefaultServices>();
115+
return HttpApp.toWebHandlerRuntime(rt)(
224116
options?.middleware ? options.middleware(app as any) as any : app,
225117
);
226-
handlerCached = handler;
227-
return handler;
228-
}).pipe(runtime.runPromise);
118+
});
229119

230-
async function handler(event: LambdaEvent) {
120+
/**
121+
* Construct an `EffectHandler` from an `HttpApi` instance.
122+
*
123+
* @since 1.4.0
124+
* @category constructors
125+
*/
126+
export const httpApiHandler: EffectHandler<
127+
LambdaEvent,
128+
WebHandler,
129+
HttpServerError.ResponseError | Cause.UnknownException,
130+
LambdaResult
131+
> = (event) =>
132+
Effect.gen(function*() {
231133
const eventSource = getEventSource(event) as EventSource<LambdaEvent, LambdaResult>;
232134
const requestValues = eventSource.getRequest(event);
233135

234-
const request = new Request(
235-
`http://${requestValues.remoteAddress}${requestValues.path}`,
136+
const req = new Request(
137+
`https://${requestValues.remoteAddress}${requestValues.path}`,
236138
{
237139
method: requestValues.method,
238140
headers: requestValues.headers,
239141
body: requestValues.body,
240142
},
241143
);
242144

243-
const res = handlerCached !== undefined
244-
? await handlerCached(request)
245-
: await handlerPromise.then((handler) => handler(request));
145+
const res = yield* WebHandler.pipe(Effect.andThen((handler) => handler(req)));
246146

247147
const contentType = res.headers.get("content-type");
248148
let isBase64Encoded = contentType && isContentTypeBinary(contentType) ? true : false;
@@ -252,7 +152,9 @@ export const fromHttpApi = <LA, LE>(
252152
isBase64Encoded = isContentEncodingBinary(contentEncoding);
253153
}
254154

255-
const body = isBase64Encoded ? encodeBase64(await res.arrayBuffer()) : await res.text();
155+
const body = isBase64Encoded
156+
? encodeBase64(yield* Effect.promise(() => res.arrayBuffer()))
157+
: yield* Effect.promise(() => res.text());
256158

257159
const headers: Record<string, string> = {};
258160

@@ -280,7 +182,40 @@ export const fromHttpApi = <LA, LE>(
280182
headers,
281183
isBase64Encoded,
282184
});
283-
}
185+
});
284186

285-
return handler;
187+
/**
188+
* Construct a lambda handler from an `HttpApi` instance.
189+
*
190+
* @example
191+
* ```ts
192+
* import { LambdaHandler } from "@effect-aws/lambda"
193+
* import { HttpApi, HttpApiBuilder, HttpServer } from "@effect/platform"
194+
* import { Layer } from "effect"
195+
*
196+
* class MyApi extends HttpApi.make("api") {}
197+
*
198+
* const MyApiLive = HttpApiBuilder.api(MyApi)
199+
*
200+
* export const handler = LambdaHandler.fromHttpApi(
201+
* Layer.mergeAll(
202+
* MyApiLive,
203+
* // you could also use NodeHttpServer.layerContext, depending on your
204+
* // server's platform
205+
* HttpServer.layerContext
206+
* )
207+
* )
208+
* ```
209+
*
210+
* @since 1.4.0
211+
* @category constructors
212+
*/
213+
export const fromHttpApi = <LA, LE>(
214+
layer: Layer.Layer<LA | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices, LE>,
215+
options?: HttpApiOptions,
216+
): Handler<LambdaEvent, LambdaResult> => {
217+
const httpApiLayer = Layer.effect(WebHandler, makeWebHandler(options)).pipe(
218+
Layer.provide(Layer.mergeAll(layer, HttpApiBuilder.Router.Live, HttpApiBuilder.Middleware.layer)),
219+
);
220+
return make({ handler: httpApiHandler, layer: httpApiLayer, memoMap: options?.memoMap });
286221
};

packages/lambda/src/Types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type EffectHandler<T, R, E = never, A = void> = (
3737
* @category model
3838
*/
3939
export type EffectHandlerWithLayer<T, R, E1 = never, E2 = never, A = void> = {
40-
handler: EffectHandler<T, R, E1, A>;
41-
layer: Layer.Layer<R, E2>;
40+
readonly handler: EffectHandler<T, R, E1, A>;
41+
readonly layer: Layer.Layer<R, E2>;
42+
readonly memoMap?: Layer.MemoMap;
4243
};

0 commit comments

Comments
 (0)