@salesforce/mrt-utilities 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +49 -45
  2. package/dist/cjs/index.d.ts +4 -0
  3. package/dist/cjs/index.js +10 -0
  4. package/dist/cjs/index.js.map +1 -0
  5. package/dist/cjs/metrics/index.d.ts +1 -0
  6. package/dist/cjs/metrics/index.js +7 -0
  7. package/dist/cjs/metrics/index.js.map +1 -0
  8. package/dist/cjs/metrics/metrics-sender.d.ts +136 -0
  9. package/dist/cjs/metrics/metrics-sender.js +240 -0
  10. package/dist/cjs/metrics/metrics-sender.js.map +1 -0
  11. package/dist/cjs/middleware/data-store.d.ts +56 -0
  12. package/dist/cjs/middleware/data-store.js +124 -0
  13. package/dist/cjs/middleware/data-store.js.map +1 -0
  14. package/dist/cjs/middleware/index.d.ts +3 -0
  15. package/dist/cjs/middleware/index.js +8 -0
  16. package/dist/cjs/middleware/index.js.map +1 -0
  17. package/dist/cjs/middleware/middleware.d.ts +126 -0
  18. package/dist/cjs/middleware/middleware.js +411 -0
  19. package/dist/cjs/middleware/middleware.js.map +1 -0
  20. package/dist/cjs/package.json +1 -0
  21. package/dist/cjs/streaming/create-lambda-adapter.d.ts +68 -0
  22. package/dist/cjs/streaming/create-lambda-adapter.js +797 -0
  23. package/dist/cjs/streaming/create-lambda-adapter.js.map +1 -0
  24. package/dist/cjs/streaming/index.d.ts +1 -0
  25. package/dist/cjs/streaming/index.js +7 -0
  26. package/dist/cjs/streaming/index.js.map +1 -0
  27. package/dist/cjs/utils/configure-proxying.d.ts +160 -0
  28. package/dist/cjs/utils/configure-proxying.js +204 -0
  29. package/dist/cjs/utils/configure-proxying.js.map +1 -0
  30. package/dist/cjs/utils/ssr-proxying.d.ts +300 -0
  31. package/dist/cjs/utils/ssr-proxying.js +713 -0
  32. package/dist/cjs/utils/ssr-proxying.js.map +1 -0
  33. package/dist/cjs/utils/utils.d.ts +27 -0
  34. package/dist/cjs/utils/utils.js +44 -0
  35. package/dist/cjs/utils/utils.js.map +1 -0
  36. package/dist/esm/index.d.ts +4 -0
  37. package/dist/esm/index.js +10 -0
  38. package/dist/esm/index.js.map +1 -0
  39. package/dist/esm/metrics/index.d.ts +1 -0
  40. package/dist/esm/metrics/index.js +7 -0
  41. package/dist/esm/metrics/index.js.map +1 -0
  42. package/dist/esm/metrics/metrics-sender.d.ts +136 -0
  43. package/dist/esm/metrics/metrics-sender.js +240 -0
  44. package/dist/esm/metrics/metrics-sender.js.map +1 -0
  45. package/dist/esm/middleware/data-store.d.ts +56 -0
  46. package/dist/esm/middleware/data-store.js +124 -0
  47. package/dist/esm/middleware/data-store.js.map +1 -0
  48. package/dist/esm/middleware/index.d.ts +3 -0
  49. package/dist/esm/middleware/index.js +9 -0
  50. package/dist/esm/middleware/index.js.map +1 -0
  51. package/dist/esm/middleware/middleware.d.ts +126 -0
  52. package/dist/esm/middleware/middleware.js +411 -0
  53. package/dist/esm/middleware/middleware.js.map +1 -0
  54. package/dist/esm/streaming/create-lambda-adapter.d.ts +68 -0
  55. package/dist/esm/streaming/create-lambda-adapter.js +797 -0
  56. package/dist/esm/streaming/create-lambda-adapter.js.map +1 -0
  57. package/dist/esm/streaming/index.d.ts +1 -0
  58. package/dist/esm/streaming/index.js +7 -0
  59. package/dist/esm/streaming/index.js.map +1 -0
  60. package/dist/esm/utils/configure-proxying.d.ts +160 -0
  61. package/dist/esm/utils/configure-proxying.js +204 -0
  62. package/dist/esm/utils/configure-proxying.js.map +1 -0
  63. package/dist/esm/utils/ssr-proxying.d.ts +300 -0
  64. package/dist/esm/utils/ssr-proxying.js +713 -0
  65. package/dist/esm/utils/ssr-proxying.js.map +1 -0
  66. package/dist/esm/utils/utils.d.ts +27 -0
  67. package/dist/esm/utils/utils.js +44 -0
  68. package/dist/esm/utils/utils.js.map +1 -0
  69. package/package.json +129 -7
@@ -0,0 +1,797 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { ServerResponse } from 'http';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import zlib from 'node:zlib';
9
+ import Negotiator from 'negotiator';
10
+ import compressible from 'compressible';
11
+ import { ServerlessRequest } from '@h4ad/serverless-adapter';
12
+ /**
13
+ * Header keys to copy from the request to the response.
14
+ * These headers are typically used for tracing, correlation, or other request/response matching purposes.
15
+ */
16
+ const REQUEST_HEADERS_TO_COPY = ['x-correlation-id'];
17
+ // Check if zstd compression is available (Node.js v22.15.0+)
18
+ let createZstdCompress;
19
+ try {
20
+ // Try to import createZstdCompress - it may not exist in older Node.js versions
21
+ if (typeof zlib.createZstdCompress === 'function') {
22
+ createZstdCompress = zlib.createZstdCompress;
23
+ }
24
+ }
25
+ catch {
26
+ // zstd not available
27
+ }
28
+ /**
29
+ * Creates a Lambda Adapter that wraps an Express app and supports response streaming
30
+ * for API Gateway v1 proxy integration using AWS Lambda response streaming
31
+ *
32
+ * @param app - Express application instance
33
+ * @param responseStream - AWS Lambda response stream
34
+ * @param compressionConfig - Optional compression configuration
35
+ * @returns Lambda handler function
36
+ */
37
+ export function createStreamingLambdaAdapter(app, responseStream, compressionConfig = { enabled: true }) {
38
+ const handler = async (event, context) => {
39
+ try {
40
+ await streamResponse(event, responseStream, context, app, compressionConfig);
41
+ }
42
+ catch (error) {
43
+ console.error('Error in streaming handler:', error);
44
+ const isStreamOpen = responseStream && responseStream.writable && !responseStream.destroyed && !responseStream.writableEnded;
45
+ if (isStreamOpen && typeof responseStream.write === 'function') {
46
+ const errorMessage = error instanceof Error ? error.message : String(error);
47
+ responseStream.write(`HTTP/1.1 500 Internal Server Error\r\n\r\nInternal Server Error: ${errorMessage}`);
48
+ }
49
+ else {
50
+ console.error('[error handler] Cannot write error - stream is closed');
51
+ }
52
+ }
53
+ finally {
54
+ const isStreamOpen = responseStream && responseStream.writable && !responseStream.destroyed && !responseStream.writableEnded;
55
+ if (isStreamOpen && typeof responseStream.end === 'function') {
56
+ responseStream.end();
57
+ }
58
+ }
59
+ };
60
+ return handler;
61
+ }
62
+ /**
63
+ * Streams the response from Express app using AWS Lambda HttpResponseStream
64
+ */
65
+ async function streamResponse(event, responseStream, context, app, compressionConfig) {
66
+ // Convert API Gateway event to Express-compatible request
67
+ const expressRequest = createExpressRequest(event, context);
68
+ const expressResponse = createExpressResponse(responseStream, event, context, expressRequest, compressionConfig);
69
+ // Process the request through Express app
70
+ return new Promise((resolve, reject) => {
71
+ let resolved = false;
72
+ const resolveOnce = () => {
73
+ if (!resolved) {
74
+ resolved = true;
75
+ resolve();
76
+ }
77
+ };
78
+ const rejectOnce = (err) => {
79
+ if (!resolved) {
80
+ resolved = true;
81
+ reject(err);
82
+ }
83
+ };
84
+ // Handle response finish
85
+ expressResponse.once('finish', () => {
86
+ resolveOnce();
87
+ });
88
+ // Handle response errors
89
+ expressResponse.once('error', (err) => {
90
+ rejectOnce(err);
91
+ });
92
+ try {
93
+ app(expressRequest, expressResponse, (err) => {
94
+ if (err) {
95
+ console.error('Express app error:', err);
96
+ rejectOnce(err);
97
+ }
98
+ else {
99
+ // If response has finished, resolveOnce will be called by the finish event
100
+ // Otherwise, resolve after a short delay to allow async operations
101
+ if (expressResponse.finished) {
102
+ resolveOnce();
103
+ }
104
+ else {
105
+ // Wait a bit for the response to finish
106
+ setTimeout(() => {
107
+ resolveOnce();
108
+ }, 10);
109
+ }
110
+ }
111
+ });
112
+ }
113
+ catch (error) {
114
+ console.error('Error in streamResponse:', error);
115
+ rejectOnce(error);
116
+ }
117
+ });
118
+ }
119
+ /**
120
+ * Builds a full URL path with query string from API Gateway event
121
+ * Merges multiValueQueryStringParameters and queryStringParameters
122
+ */
123
+ const getPathFromEvent = (event) => {
124
+ const path = event.path;
125
+ // Start with multi-value query parameters (already arrays), filtering out undefined values
126
+ const mergedParams = {};
127
+ if (event.multiValueQueryStringParameters) {
128
+ for (const [key, values] of Object.entries(event.multiValueQueryStringParameters)) {
129
+ if (values) {
130
+ mergedParams[key] = [...values];
131
+ }
132
+ }
133
+ }
134
+ // Merge in single-value query parameters, converting to arrays
135
+ if (event.queryStringParameters) {
136
+ for (const [key, value] of Object.entries(event.queryStringParameters)) {
137
+ if (value === undefined)
138
+ continue;
139
+ // Add to existing array or create new one
140
+ if (mergedParams[key]) {
141
+ if (!mergedParams[key].includes(value)) {
142
+ mergedParams[key].push(value);
143
+ }
144
+ }
145
+ else {
146
+ mergedParams[key] = [value];
147
+ }
148
+ }
149
+ }
150
+ // Build query string
151
+ const searchParams = new URLSearchParams();
152
+ for (const [key, values] of Object.entries(mergedParams)) {
153
+ for (const value of values) {
154
+ searchParams.append(key, value);
155
+ }
156
+ }
157
+ const queryString = searchParams.toString();
158
+ return queryString ? `${path}?${queryString}` : path;
159
+ };
160
+ /**
161
+ * Converts API Gateway event to Express-compatible request object
162
+ * Creates a proper IncomingMessage-like object with stream properties
163
+ */
164
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
165
+ export function createExpressRequest(event, context) {
166
+ const { httpMethod, headers, multiValueHeaders, body, isBase64Encoded, requestContext } = event;
167
+ const remoteAddress = requestContext?.identity?.sourceIp ?? undefined;
168
+ const bodyEncoding = isBase64Encoded ? 'base64' : 'utf-8';
169
+ const requestBody = body ? Buffer.from(body, bodyEncoding) : undefined;
170
+ // Normalize headers to lowercase keys for case-insensitive lookup
171
+ const normalizedHeaders = {};
172
+ if (headers) {
173
+ for (const [key, value] of Object.entries(headers)) {
174
+ const normalizedKey = key.toLowerCase();
175
+ // If value is an array, take the first one; otherwise use the value
176
+ if (value === undefined)
177
+ continue;
178
+ normalizedHeaders[normalizedKey] = value;
179
+ }
180
+ }
181
+ for (const multiValueHeaderKey of Object.keys(multiValueHeaders || {})) {
182
+ const value = multiValueHeaders[multiValueHeaderKey];
183
+ if (!value || value.length <= 1)
184
+ continue;
185
+ normalizedHeaders[multiValueHeaderKey] = value.join(',');
186
+ }
187
+ const request = new ServerlessRequest({
188
+ method: httpMethod,
189
+ url: getPathFromEvent(event),
190
+ headers: normalizedHeaders,
191
+ body: requestBody,
192
+ remoteAddress,
193
+ });
194
+ // Add Express-specific properties that aren't part of IncomingMessage
195
+ // IncomingMessage doesn't have query, params, etc. - these are added by Express
196
+ const req = request;
197
+ // Express-like methods
198
+ Object.defineProperty(req, 'get', {
199
+ value(headerName) {
200
+ return this.headers[headerName.toLowerCase()];
201
+ },
202
+ writable: true,
203
+ enumerable: true,
204
+ configurable: true,
205
+ });
206
+ Object.defineProperty(req, 'header', {
207
+ value(headerName) {
208
+ return this.get(headerName);
209
+ },
210
+ writable: true,
211
+ enumerable: true,
212
+ configurable: true,
213
+ });
214
+ return req;
215
+ }
216
+ /**
217
+ * Checks if a content type is compressible using the compressible package
218
+ *
219
+ * @param contentType - The content type to check (e.g., 'text/html', 'application/json')
220
+ * @returns true if the content type is compressible, false otherwise
221
+ */
222
+ function isCompressible(contentType) {
223
+ if (!contentType) {
224
+ return false;
225
+ }
226
+ return !!compressible(contentType);
227
+ }
228
+ const isNullOrUndefined = (value) => value == null;
229
+ /**
230
+ * Determines the best encoding based on Accept-Encoding header using the negotiator package
231
+ * Prefers encodings in order: br (brotli), zstd (if available), gzip, deflate
232
+ *
233
+ * @param acceptEncoding - The Accept-Encoding header value from the request
234
+ * @param compressionConfig - Optional compression configuration
235
+ * @returns The best available encoding or null if none are supported
236
+ */
237
+ function getBestEncoding(acceptEncoding, compressionConfig) {
238
+ // If compression is explicitly disabled, return null
239
+ if (compressionConfig?.enabled === false) {
240
+ return null;
241
+ }
242
+ // If override encoding is provided, use it regardless of Accept-Encoding header
243
+ if (compressionConfig?.encoding) {
244
+ return compressionConfig.encoding;
245
+ }
246
+ if (!acceptEncoding) {
247
+ return null;
248
+ }
249
+ const negotiator = new Negotiator({ headers: { 'accept-encoding': acceptEncoding } });
250
+ // Build available encodings list based on what's supported
251
+ // Order of preference: br (brotli), zstd (if available), gzip, deflate
252
+ const availableEncodings = ['br', 'gzip', 'deflate'];
253
+ if (createZstdCompress) {
254
+ availableEncodings.push('zstd');
255
+ }
256
+ const bestEncoding = negotiator.encoding(availableEncodings);
257
+ return bestEncoding || null;
258
+ }
259
+ /**
260
+ * Creates a compression stream based on the encoding type
261
+ *
262
+ * @param encoding - The encoding type ('br', 'zstd', 'gzip', or 'deflate')
263
+ * @param compressionConfig - The compression configuration options
264
+ * @returns A compression stream (BrotliCompress, ZstdCompress, Gzip, or Deflate)
265
+ * @throws Error if the encoding is not supported
266
+ */
267
+ function createCompressionStream(encoding, compressionConfig) {
268
+ const options = compressionConfig?.options || undefined;
269
+ switch (encoding) {
270
+ case 'br':
271
+ return zlib.createBrotliCompress(options);
272
+ case 'zstd':
273
+ if (!createZstdCompress) {
274
+ throw new Error('zstd compression is not available in this Node.js version (requires v22.15.0+)');
275
+ }
276
+ return createZstdCompress(options);
277
+ case 'gzip':
278
+ return zlib.createGzip(options);
279
+ case 'deflate':
280
+ return zlib.createDeflate(options);
281
+ default:
282
+ throw new Error(`Unsupported encoding: ${encoding}`);
283
+ }
284
+ }
285
+ /**
286
+ * Creates Express-compatible response object that properly extends ServerResponse
287
+ *
288
+ * This function creates a response object that:
289
+ * - Supports AWS Lambda response streaming via HttpResponseStream
290
+ * - Automatically compresses responses based on Accept-Encoding header
291
+ * - Uses negotiator to select the best available encoding (br, zstd if available, gzip, deflate)
292
+ * - Uses compressible package to determine if content should be compressed
293
+ *
294
+ * Compression flow:
295
+ * 1. Check Accept-Encoding header and select best encoding
296
+ * 2. Create HttpResponseStream with metadata
297
+ * 3. If compression is applicable, create compression stream and pipe to httpResponseStream
298
+ * 4. Write data to compression stream (or httpResponseStream if no compression)
299
+ * 5. End compression stream, which automatically ends httpResponseStream
300
+ *
301
+ * @param responseStream - The AWS Lambda response stream
302
+ * @param method - The HTTP method (GET, POST, etc.)
303
+ * @param request - Optional Express request object (used to check Accept-Encoding header)
304
+ * @returns Express-compatible response object
305
+ */
306
+ export function createExpressResponse(responseStream, event, context, request, compressionConfig) {
307
+ const method = event.httpMethod;
308
+ let statusCode = 200;
309
+ let statusMessage = undefined;
310
+ const headers = {};
311
+ let responseStarted = false;
312
+ let httpResponseStream = null;
313
+ // Determine if compression should be used based on Accept-Encoding header
314
+ const acceptEncoding = request?.get('accept-encoding') || 'identity';
315
+ const selectedEncoding = getBestEncoding(acceptEncoding, compressionConfig) || 'identity';
316
+ let compressionStream = null;
317
+ let shouldCompress = false;
318
+ let compressionInitialized = false;
319
+ // Helper function to check if stream is still writable
320
+ const isStreamOpen = () => {
321
+ const streamToCheck = compressionStream || httpResponseStream || responseStream;
322
+ return streamToCheck && streamToCheck.writable && !streamToCheck.destroyed && !streamToCheck.writableEnded;
323
+ };
324
+ /**
325
+ * Initializes compression stream and pipes it to httpResponseStream
326
+ * This must be called after httpResponseStream is created
327
+ *
328
+ * @param httpResponseStream - The HttpResponseStream to pipe compressed data to
329
+ * @param selectedEncoding - The encoding to use (br, gzip, or deflate)
330
+ */
331
+ const initializeCompression = () => {
332
+ if (!httpResponseStream || compressionInitialized || !selectedEncoding) {
333
+ return;
334
+ }
335
+ try {
336
+ // Create compression stream based on selected encoding
337
+ compressionStream = createCompressionStream(selectedEncoding, compressionConfig);
338
+ // Set up error handling for compression stream
339
+ compressionStream.on('error', (error) => {
340
+ console.error('Compression stream error:', error);
341
+ shouldCompress = false;
342
+ });
343
+ // Pipe compression stream to httpResponseStream
344
+ // The { end: true } option ensures httpResponseStream is ended when compressionStream ends
345
+ compressionStream.pipe(httpResponseStream, { end: true });
346
+ shouldCompress = true;
347
+ compressionInitialized = true;
348
+ }
349
+ catch (error) {
350
+ console.error('Error setting up compression:', error);
351
+ shouldCompress = false;
352
+ compressionStream = null;
353
+ }
354
+ };
355
+ /**
356
+ * Writes a chunk to the appropriate stream (compression stream if enabled, otherwise httpResponseStream)
357
+ *
358
+ * @param chunk - The data chunk to write
359
+ * @returns true if the chunk was written successfully, false otherwise
360
+ */
361
+ const writeChunk = (chunk) => {
362
+ // Don't write null, undefined
363
+ if (isNullOrUndefined(chunk) || !isStreamOpen()) {
364
+ return false;
365
+ }
366
+ try {
367
+ if (shouldCompress && compressionStream && compressionStream.writable) {
368
+ // Write to compression stream, which will compress and pipe to httpResponseStream
369
+ return compressionStream.write(chunk);
370
+ }
371
+ else if (httpResponseStream && httpResponseStream.writable) {
372
+ // No compression, write directly to httpResponseStream
373
+ return httpResponseStream.write(chunk);
374
+ }
375
+ return false;
376
+ }
377
+ catch (error) {
378
+ console.error('Error writing chunk:', error);
379
+ return false;
380
+ }
381
+ };
382
+ const isCompressionEnabled = (contentTypeStr) => {
383
+ const enabled = compressionConfig?.enabled ?? true;
384
+ return !!(selectedEncoding && selectedEncoding !== 'identity' && isCompressible(contentTypeStr) && enabled);
385
+ };
386
+ const getContentType = (response) => {
387
+ const contentType = response.getHeader('content-type');
388
+ if (Array.isArray(contentType)) {
389
+ return contentType.join(',');
390
+ }
391
+ else if (typeof contentType === 'number') {
392
+ return String(contentType);
393
+ }
394
+ return contentType;
395
+ };
396
+ /**
397
+ * Initializes the response by:
398
+ * 1. Collecting headers
399
+ * 2. Determining if compression should be used
400
+ * 3. Creating HttpResponseStream with metadata
401
+ * 4. Setting up compression stream if needed
402
+ *
403
+ * This must be called before any data is written to the response.
404
+ *
405
+ * @param response - The Express response object
406
+ */
407
+ const initializeResponse = (response) => {
408
+ if (responseStarted) {
409
+ return;
410
+ }
411
+ if (!isStreamOpen()) {
412
+ console.error('Cannot initialize response - stream is closed');
413
+ return;
414
+ }
415
+ // Collect all current headers from the response
416
+ const currentHeaders = response.getHeaders();
417
+ Object.assign(headers, currentHeaders);
418
+ for (const header of REQUEST_HEADERS_TO_COPY) {
419
+ const value = request?.get(header);
420
+ if (value) {
421
+ headers[header] = value;
422
+ }
423
+ }
424
+ const contentType = getContentType(response);
425
+ if (isCompressionEnabled(contentType)) {
426
+ headers['content-encoding'] = selectedEncoding;
427
+ response.setHeader('Content-Encoding', selectedEncoding);
428
+ }
429
+ // Remove Content-Length header when compression is enabled since the length will change
430
+ delete headers['content-length'];
431
+ response.removeHeader('content-length');
432
+ // Create HttpResponseStream with metadata
433
+ // This writes the HTTP status and headers to the stream
434
+ const metadata = {
435
+ statusCode,
436
+ headers,
437
+ };
438
+ const cookies = metadata.headers['set-cookie'];
439
+ if (cookies) {
440
+ metadata.cookies = Array.isArray(cookies) ? cookies : [cookies];
441
+ delete metadata.headers['set-cookie'];
442
+ }
443
+ metadata.headers = convertHeaders(metadata.headers);
444
+ httpResponseStream = awslambda.HttpResponseStream.from(responseStream, metadata);
445
+ // Set up compression stream if compression is enabled
446
+ // The compression stream pipes to httpResponseStream, which pipes to responseStream
447
+ // 'identity' means no encoding, so we should not initialize compression for it
448
+ if (isCompressionEnabled(contentType)) {
449
+ initializeCompression();
450
+ }
451
+ responseStarted = true;
452
+ };
453
+ // Helper function to convert headers to the expected format
454
+ const convertHeaders = (headersToConvert) => {
455
+ const converted = {};
456
+ for (const [key, value] of Object.entries(headersToConvert)) {
457
+ if (value !== undefined) {
458
+ if (Array.isArray(value)) {
459
+ converted[key] = value.join(',');
460
+ }
461
+ else if (typeof value === 'number') {
462
+ converted[key] = String(value);
463
+ }
464
+ else {
465
+ converted[key] = value;
466
+ }
467
+ }
468
+ }
469
+ return converted;
470
+ };
471
+ /**
472
+ * Pipes data from the compression stream (if enabled) or httpResponseStream to a destination
473
+ * Note: This is a simplified implementation for API compatibility
474
+ *
475
+ * @param destination - The destination stream to pipe to
476
+ * @returns true if the pipe operation was successful, false otherwise
477
+ */
478
+ const pipeToDestination = async (destination) => {
479
+ if (!isStreamOpen()) {
480
+ console.error('[pipeToDestination] Cannot pipe - stream is closed');
481
+ return false;
482
+ }
483
+ // Note: pipeToDestination is called from res.pipe() which already calls initializeResponse
484
+ // So compression should already be initialized if needed
485
+ try {
486
+ // Pipe from compression stream if available, otherwise from httpResponseStream
487
+ const sourceStream = compressionStream || httpResponseStream;
488
+ if (!sourceStream) {
489
+ console.error('[pipeToDestination] No source stream available');
490
+ return false;
491
+ }
492
+ // @ts-expect-error - Pipeline expects Readable, but compression streams are Transform streams
493
+ await pipeline(sourceStream, destination);
494
+ return true;
495
+ }
496
+ catch (error) {
497
+ console.error('[pipeToDestination] Pipeline error:', error);
498
+ return false;
499
+ }
500
+ };
501
+ // @ts-expect-error - ServerResponse constructor expects IncomingMessage, but we're creating a minimal mock
502
+ const res = new ServerResponse({
503
+ method,
504
+ });
505
+ // Override statusMessage property to track custom status messages
506
+ Object.defineProperty(res, 'statusMessage', {
507
+ get() {
508
+ return statusMessage;
509
+ },
510
+ set(value) {
511
+ statusMessage = value;
512
+ },
513
+ enumerable: true,
514
+ configurable: true,
515
+ });
516
+ // Override headersSent property to track when headers are sent
517
+ // This is readonly in the parent class, so we override it to be writable
518
+ Object.defineProperty(res, 'headersSent', {
519
+ get() {
520
+ return responseStarted;
521
+ },
522
+ enumerable: true,
523
+ configurable: true,
524
+ });
525
+ // Override the core streaming methods to work with AWS Lambda Response Streaming
526
+ // @ts-expect-error - Type signature doesn't match ServerResponse.writeHead exactly, but our implementation is compatible
527
+ res.writeHead = function (code, reasonPhrase, headerObj) {
528
+ if (typeof reasonPhrase === 'object') {
529
+ headerObj = reasonPhrase;
530
+ reasonPhrase = undefined;
531
+ }
532
+ statusCode = code || statusCode;
533
+ this.statusCode = statusCode;
534
+ // Set statusMessage if provided
535
+ if (reasonPhrase) {
536
+ statusMessage = reasonPhrase;
537
+ }
538
+ if (headerObj) {
539
+ Object.assign(headers, headerObj);
540
+ for (const [key, value] of Object.entries(headerObj)) {
541
+ if (value !== undefined) {
542
+ this.setHeader(key, value);
543
+ }
544
+ }
545
+ }
546
+ // Collect all current headers
547
+ const currentHeaders = this.getHeaders();
548
+ Object.assign(headers, currentHeaders);
549
+ initializeResponse(this);
550
+ return this;
551
+ };
552
+ res.write = function (chunk) {
553
+ if (!isStreamOpen()) {
554
+ console.error(`Cannot write - stream is closed`);
555
+ return false;
556
+ }
557
+ initializeResponse(this);
558
+ if (!isNullOrUndefined(chunk)) {
559
+ return writeChunk(chunk);
560
+ }
561
+ return true; // ServerResponse.write returns boolean
562
+ };
563
+ const _flush = () => {
564
+ if (shouldCompress &&
565
+ compressionStream &&
566
+ compressionStream.writable &&
567
+ typeof compressionStream.flush === 'function') {
568
+ compressionStream.flush();
569
+ }
570
+ else if (httpResponseStream &&
571
+ httpResponseStream.writable &&
572
+ // @ts-expect-error - flush doesn't exist on Writable, but we're adding it
573
+ typeof httpResponseStream.flush === 'function') {
574
+ // @ts-expect-error - flush doesn't exist on Writable, but we're adding it
575
+ httpResponseStream.flush();
576
+ }
577
+ };
578
+ /**
579
+ * Ends the appropriate stream(s) and emits the finish event
580
+ * If compression is enabled, ends the compression stream which will automatically
581
+ * end httpResponseStream due to the pipe with { end: true }
582
+ *
583
+ * @param response - The Express response object to emit finish event on
584
+ */
585
+ const endStream = (response) => {
586
+ if (shouldCompress && compressionStream) {
587
+ try {
588
+ // Flush compression stream to ensure all buffered data is written
589
+ _flush();
590
+ // End compression stream - this will automatically end httpResponseStream
591
+ // due to the pipe with { end: true } option
592
+ compressionStream.end(() => {
593
+ response.finished = true;
594
+ response.emit('finish');
595
+ });
596
+ }
597
+ catch (error) {
598
+ console.error(`Error ending compression stream:`, error);
599
+ // Still emit finish even if there was an error
600
+ response.finished = true;
601
+ response.emit('finish');
602
+ }
603
+ }
604
+ else if (httpResponseStream && httpResponseStream.writable) {
605
+ // No compression, end httpResponseStream directly
606
+ try {
607
+ _flush();
608
+ httpResponseStream.end(() => {
609
+ response.finished = true;
610
+ response.emit('finish');
611
+ });
612
+ }
613
+ catch (error) {
614
+ console.error(`Error ending httpResponseStream:`, error);
615
+ response.finished = true;
616
+ response.emit('finish');
617
+ }
618
+ }
619
+ else {
620
+ console.error(`Cannot call end() - stream is closed`);
621
+ // Still emit finish to prevent hanging
622
+ response.finished = true;
623
+ response.emit('finish');
624
+ }
625
+ };
626
+ // @ts-expect-error - Type signature doesn't match ServerResponse.end exactly, but our implementation is compatible
627
+ res.end = function (chunk) {
628
+ if (!isStreamOpen()) {
629
+ console.error(`Cannot end - stream is already closed`);
630
+ return this;
631
+ }
632
+ initializeResponse(this);
633
+ // Chunks can be falsy ('', 0, etc.) but not null or undefined
634
+ if (!isNullOrUndefined(chunk)) {
635
+ const result = writeChunk(chunk);
636
+ if (!result) {
637
+ // Backpressure - wait for drain event before ending
638
+ const streamToWait = compressionStream || httpResponseStream;
639
+ if (streamToWait) {
640
+ streamToWait.once('drain', () => {
641
+ endStream(this);
642
+ });
643
+ }
644
+ else {
645
+ endStream(this);
646
+ }
647
+ return this;
648
+ }
649
+ }
650
+ // End the stream(s) and emit finish event
651
+ endStream(this);
652
+ return this;
653
+ };
654
+ // Add Express-specific methods that aren't in ServerResponse
655
+ res.status = function (code, message) {
656
+ this.statusCode = code;
657
+ statusCode = code;
658
+ if (message !== undefined) {
659
+ statusMessage = message;
660
+ }
661
+ return this;
662
+ };
663
+ res.set = function (field, value) {
664
+ if (typeof field === 'object') {
665
+ for (const [key, val] of Object.entries(field)) {
666
+ if (val !== undefined) {
667
+ this.setHeader(key, val);
668
+ }
669
+ }
670
+ }
671
+ else {
672
+ if (value !== undefined) {
673
+ this.setHeader(field, value);
674
+ }
675
+ }
676
+ return this;
677
+ };
678
+ // @ts-expect-error - Type signature doesn't match ExpressResponse.append exactly, but our implementation is compatible
679
+ res.append = function (field, value) {
680
+ const prevValue = this.getHeader(field);
681
+ if (prevValue) {
682
+ // If header already exists, append the value
683
+ if (Array.isArray(prevValue)) {
684
+ this.setHeader(field, prevValue.concat(value));
685
+ }
686
+ else if (Array.isArray(value)) {
687
+ this.setHeader(field, [prevValue].concat(value));
688
+ }
689
+ else {
690
+ this.setHeader(field, [prevValue, value]);
691
+ }
692
+ }
693
+ else {
694
+ // If header doesn't exist, just set it
695
+ this.setHeader(field, value);
696
+ }
697
+ return this;
698
+ };
699
+ res.flushHeaders = function () {
700
+ if (!responseStarted) {
701
+ if (!isStreamOpen()) {
702
+ console.error('[res.flushHeaders] Cannot flush headers - stream is closed');
703
+ return this;
704
+ }
705
+ // Collect all current headers and send them
706
+ // getHeaders() returns all headers from ServerResponse, including those set via set()/setHeader()
707
+ // This is the source of truth for all headers
708
+ const currentHeaders = this.getHeaders();
709
+ // Merge with local headers variable to include any headers set via writeHead
710
+ // currentHeaders takes precedence as it's the authoritative source from ServerResponse
711
+ Object.assign(headers, currentHeaders);
712
+ }
713
+ initializeResponse(this);
714
+ return this;
715
+ };
716
+ res.json = function (obj) {
717
+ res.setHeader('Content-Type', 'application/json');
718
+ this.end(JSON.stringify(obj));
719
+ return this;
720
+ };
721
+ res.send = function (body) {
722
+ if (typeof body === 'object' && body !== null) {
723
+ return this.json(body);
724
+ }
725
+ // Convert non-string values to string
726
+ const bodyString = typeof body === 'string' ? body : String(body);
727
+ this.end(bodyString);
728
+ return this;
729
+ };
730
+ // @ts-expect-error - Type signature doesn't match ExpressResponse.redirect exactly, but our implementation is compatible
731
+ res.redirect = function (url) {
732
+ this.status(302);
733
+ this.setHeader('Location', url);
734
+ this.end();
735
+ return this;
736
+ };
737
+ // Add flush method for streaming responses (important for RSC)
738
+ // @ts-expect-error - flush doesn't exist on ExpressResponse type, but we're adding it
739
+ res.flush = function () {
740
+ if (!isStreamOpen()) {
741
+ console.error(`Cannot flush - stream is closed`);
742
+ return this;
743
+ }
744
+ initializeResponse(this);
745
+ // Flush the compression stream if it exists and supports it
746
+ // This ensures any buffered compressed data is written immediately
747
+ try {
748
+ _flush();
749
+ }
750
+ catch (error) {
751
+ console.error(`Error flushing:`, error);
752
+ }
753
+ return this;
754
+ };
755
+ // Track piped destinations for unpipe support
756
+ const pipedDestinations = new Set();
757
+ // Add pipe method for streaming responses (commonly used in Express)
758
+ // @ts-expect-error - Type signature doesn't match ExpressResponse.pipe exactly, but our implementation is compatible
759
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
760
+ res.pipe = function (destination, options) {
761
+ if (!isStreamOpen()) {
762
+ console.error('[res.pipe] Cannot pipe - stream is closed');
763
+ return destination;
764
+ }
765
+ initializeResponse(this);
766
+ // Track the destination for unpipe support
767
+ pipedDestinations.add(destination);
768
+ // Use actual Node.js pipeline for pipe operations
769
+ pipeToDestination(destination)
770
+ .then(() => {
771
+ pipedDestinations.delete(destination);
772
+ })
773
+ .catch((error) => {
774
+ console.error('[res.pipe] Pipeline error:', error);
775
+ pipedDestinations.delete(destination);
776
+ });
777
+ return destination;
778
+ };
779
+ // Add unpipe method to remove pipe destinations
780
+ // @ts-expect-error - unpipe doesn't exist on ExpressResponse type, but we're adding it
781
+ res.unpipe = function (destination) {
782
+ if (destination) {
783
+ pipedDestinations.delete(destination);
784
+ // In a real implementation, you'd need to handle unpipe more carefully
785
+ // For now, we just track it
786
+ }
787
+ else {
788
+ // Unpipe all destinations
789
+ pipedDestinations.clear();
790
+ }
791
+ return this;
792
+ };
793
+ // Make response flushable (flag used by some frameworks)
794
+ res.flushable = true;
795
+ return res;
796
+ }
797
+ //# sourceMappingURL=create-lambda-adapter.js.map