@@ -5,12 +5,14 @@ import type * as Cause from "effect/Cause";
55import * as Context from "effect/Context" ;
66import * as Effect from "effect/Effect" ;
77import * as Layer from "effect/Layer" ;
8+ import type * as PlatformError from "effect/PlatformError" ;
89import * as Predicate from "effect/Predicate" ;
910import type { HttpMiddleware } from "effect/unstable/http" ;
1011import { HttpRouter } from "effect/unstable/http" ;
12+ import { Readable } from "node:stream" ;
1113import { getEventSource } from "./internal/index.js" ;
1214import * as internal from "./internal/lambdaHandler.js" ;
13- import { pipeTo } from "./internal/stream.js" ;
15+ import { pipeline , pipeTo } from "./internal/stream.js" ;
1416import type { EventSource , ResponseValues } from "./internal/types.js" ;
1517import { encodeBase64 , isContentEncodingBinary , isContentTypeBinary } from "./internal/utils.js" ;
1618import * as LambdaRuntime from "./LambdaRuntime.js" ;
@@ -207,6 +209,47 @@ type WebHandler = (
207209) => Promise < globalThis . Response > ;
208210const WebHandler = Context . Service < WebHandler > ( "@effect-aws/lambda/WebHandler" ) ;
209211
212+ type EffectStreamifyHandler < T , R , E = never > = (
213+ event : T ,
214+ responseStream : awslambda . HttpResponseStream ,
215+ context : LambdaContext ,
216+ ) => Effect . Effect < void , E , R > ;
217+
218+ const requestFromEvent = ( event : LambdaHandler . Event ) : Request => {
219+ const eventSource = getEventSource ( event ) as EventSource < LambdaHandler . Event , LambdaHandler . Result > ;
220+ const requestValues = eventSource . getRequest ( event ) ;
221+
222+ return new Request (
223+ `https://${ requestValues . remoteAddress } ${ requestValues . path } ` ,
224+ {
225+ method : requestValues . method ,
226+ headers : requestValues . headers ,
227+ body : requestValues . body ,
228+ } ,
229+ ) ;
230+ } ;
231+
232+ const responseHeaders = ( res : Response ) => {
233+ const headers : Record < string , string > = { } ;
234+ const cookies = res . headers . has ( "set-cookie" )
235+ ? res . headers . getSetCookie
236+ ? res . headers . getSetCookie ( )
237+ : Array . from ( ( res . headers as any ) . entries ( ) )
238+ . filter ( ( [ k ] : any ) => k === "set-cookie" )
239+ . map ( ( [ , v ] : any ) => v )
240+ : [ ] ;
241+
242+ if ( Array . isArray ( cookies ) && cookies . length > 0 ) {
243+ res . headers . delete ( "set-cookie" ) ;
244+ }
245+
246+ res . headers . forEach ( ( value , key ) => {
247+ headers [ key ] = value ;
248+ } ) ;
249+
250+ return { headers, cookies } ;
251+ } ;
252+
210253/**
211254 * Construct an `WebHandler` from an `HttpRouter` instance.
212255 *
@@ -238,16 +281,7 @@ export const httpApiHandler: EffectHandler<
238281> = ( event , context ) =>
239282 Effect . gen ( function * ( ) {
240283 const eventSource = getEventSource ( event ) as EventSource < LambdaHandler . Event , LambdaHandler . Result > ;
241- const requestValues = eventSource . getRequest ( event ) ;
242-
243- const req = new Request (
244- `https://${ requestValues . remoteAddress } ${ requestValues . path } ` ,
245- {
246- method : requestValues . method ,
247- headers : requestValues . headers ,
248- body : requestValues . body ,
249- } ,
250- ) ;
284+ const req = requestFromEvent ( event ) ;
251285
252286 const handler = yield * Effect . service ( WebHandler ) ;
253287
@@ -270,34 +304,59 @@ export const httpApiHandler: EffectHandler<
270304 ? encodeBase64 ( yield * Effect . promise ( ( ) => res . arrayBuffer ( ) ) )
271305 : yield * Effect . promise ( ( ) => res . text ( ) ) ;
272306
273- const headers : ResponseValues < LambdaHandler . Event > [ "headers" ] = { } ;
307+ const { cookies, headers } = responseHeaders ( res ) ;
308+ const responseHeadersMap : ResponseValues < LambdaHandler . Event > [ "headers" ] = headers ;
274309
275- if ( res . headers . has ( "set-cookie" ) ) {
276- const cookies = res . headers . getSetCookie
277- ? res . headers . getSetCookie ( )
278- : Array . from ( ( res . headers as any ) . entries ( ) )
279- . filter ( ( [ k ] : any ) => k === "set-cookie" )
280- . map ( ( [ , v ] : any ) => v ) ;
281-
282- if ( Array . isArray ( cookies ) ) {
283- headers [ "set-cookie" ] = cookies ;
284- res . headers . delete ( "set-cookie" ) ;
285- }
310+ if ( cookies . length > 0 ) {
311+ responseHeadersMap [ "set-cookie" ] = cookies ;
286312 }
287313
288- res . headers . forEach ( ( value , key ) => {
289- headers [ key ] = value ;
290- } ) ;
291-
292314 return eventSource . getResponse ( {
293315 event,
294316 statusCode : res . status ,
295317 body,
296- headers,
318+ headers : responseHeadersMap ,
297319 isBase64Encoded,
298320 } ) ;
299321 } ) ;
300322
323+ /**
324+ * Construct a streaming `StreamifyHandler` from an `HttpApi` instance for Lambda Function URLs.
325+ *
326+ * @since 1.7.0
327+ * @category constructors
328+ */
329+ export const httpApiStreamHandler : EffectStreamifyHandler <
330+ LambdaHandler . Event ,
331+ WebHandler ,
332+ Cause . UnknownError | PlatformError . PlatformError
333+ > = ( event , responseStream , context ) =>
334+ Effect . gen ( function * ( ) {
335+ const req = requestFromEvent ( event ) ;
336+
337+ const handler = yield * Effect . service ( WebHandler ) ;
338+
339+ const ctx = Context . empty ( ) . pipe (
340+ Context . add ( internal . lambdaEventTag , event ) ,
341+ Context . add ( internal . lambdaContextTag , context ) ,
342+ ) ;
343+
344+ const res : globalThis . Response = yield * Effect . tryPromise ( ( ) => handler ( req , ctx ) ) ;
345+
346+ const { cookies, headers } = responseHeaders ( res ) ;
347+ const httpResponseStream = global . awslambda . HttpResponseStream . from ( responseStream , {
348+ statusCode : res . status ,
349+ headers,
350+ cookies,
351+ } ) ;
352+
353+ if ( res . body ) {
354+ yield * pipeline ( Readable . fromWeb ( res . body as any ) , httpResponseStream ) ;
355+ } else {
356+ httpResponseStream . end ( ) ;
357+ }
358+ } ) ;
359+
301360/**
302361 * Construct a lambda handler from an `HttpApi` instance.
303362 *
@@ -327,3 +386,43 @@ export const fromHttpApi = <LA, LE>(
327386 const WebHandlerLive = Layer . effect ( WebHandler , makeWebHandler ( layer , options ) ) ;
328387 return make ( { handler : httpApiHandler , layer : WebHandlerLive , memoMap : options ?. memoMap } ) ;
329388} ;
389+
390+ /**
391+ * Construct a streaming lambda handler from an `HttpApi` instance for Lambda Function URLs.
392+ *
393+ * Lambda response streaming is supported by Lambda Function URLs with response streaming enabled,
394+ * not by API Gateway HTTP API integrations.
395+ *
396+ * @example
397+ * ```ts
398+ * import { LambdaHandler } from "@effect-aws/lambda"
399+ * import { HttpApi, HttpApiBuilder, HttpServer } from "@effect/platform"
400+ * import { Layer } from "effect"
401+ *
402+ * class MyApi extends HttpApi.make("api") {}
403+ *
404+ * const MyApiLive = HttpApiBuilder.api(MyApi)
405+ *
406+ * export const handler = LambdaHandler.streamFromHttpApi(
407+ * Layer.mergeAll(
408+ * MyApiLive,
409+ * HttpServer.layerContext
410+ * )
411+ * )
412+ * ```
413+ *
414+ * @since 1.7.0
415+ * @category constructors
416+ */
417+ export const streamFromHttpApi = < LA , LE > (
418+ layer : Layer . Layer < LA , LE , HttpRouter . HttpRouter > ,
419+ options ?: HttpApiOptions ,
420+ ) : Handler < LambdaHandler . Event , void > => {
421+ const WebHandlerLive = Layer . effect ( WebHandler , makeWebHandler ( layer , options ) ) ;
422+ const runtime = LambdaRuntime . fromLayer ( WebHandlerLive , { memoMap : options ?. memoMap } ) ;
423+
424+ return global . awslambda ?. streamifyResponse ( async ( event , responseStream , context ) => {
425+ context . callbackWaitsForEmptyEventLoop = false ;
426+ return httpApiStreamHandler ( event , responseStream , context ) . pipe ( runtime . runPromise ) ;
427+ } ) ;
428+ } ;
0 commit comments