@middy/core 5.0.3 → 5.2.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 (3) hide show
  1. package/index.d.ts +1 -0
  2. package/index.js +225 -156
  3. package/package.json +4 -3
package/index.d.ts CHANGED
@@ -56,6 +56,7 @@ export interface MiddlewareObj<
56
56
  before?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
57
57
  after?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
58
58
  onError?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
59
+ name?: string
59
60
  }
60
61
 
61
62
  // The AWS provided Handler type uses void | Promise<TResult> so we have no choice but to follow and suppress the linter warning
package/index.js CHANGED
@@ -1,165 +1,234 @@
1
- import { Readable } from 'node:stream';
2
- import { pipeline } from 'node:stream/promises';
3
- import { setTimeout } from 'node:timers/promises';
4
- const defaultLambdaHandler = ()=>{};
1
+ /* global awslambda */
2
+ import { Readable } from 'node:stream'
3
+ import { pipeline } from 'node:stream/promises'
4
+ import { setTimeout } from 'node:timers'
5
+
6
+ const defaultLambdaHandler = () => {}
5
7
  const defaultPlugin = {
6
- timeoutEarlyInMillis: 5,
7
- timeoutEarlyResponse: ()=>{
8
- const err = new Error('[AbortError]: The operation was aborted.', {
9
- cause: {
10
- package: '@middy/core'
11
- }
12
- });
13
- err.name = 'TimeoutError';
14
- throw err;
15
- },
16
- streamifyResponse: false
17
- };
18
- const middy = (lambdaHandler = defaultLambdaHandler, plugin = {})=>{
19
- if (typeof lambdaHandler !== 'function') {
20
- plugin = lambdaHandler;
21
- lambdaHandler = defaultLambdaHandler;
8
+ timeoutEarlyInMillis: 5,
9
+ timeoutEarlyResponse: () => {
10
+ const err = new Error('[AbortError]: The operation was aborted.', {
11
+ cause: { package: '@middy/core' }
12
+ })
13
+ err.name = 'TimeoutError'
14
+ throw err
15
+ },
16
+ streamifyResponse: false // Deprecate need for this when AWS provides a flag for when it's looking for it
17
+ }
18
+
19
+ const middy = (lambdaHandler = defaultLambdaHandler, plugin = {}) => {
20
+ // Allow base handler to be set using .handler()
21
+ if (typeof lambdaHandler !== 'function') {
22
+ plugin = lambdaHandler
23
+ lambdaHandler = defaultLambdaHandler
24
+ }
25
+ plugin = { ...defaultPlugin, ...plugin }
26
+ plugin.timeoutEarly = plugin.timeoutEarlyInMillis > 0
27
+
28
+ plugin.beforePrefetch?.()
29
+ const beforeMiddlewares = []
30
+ const afterMiddlewares = []
31
+ const onErrorMiddlewares = []
32
+
33
+ const middyHandler = (event = {}, context = {}) => {
34
+ plugin.requestStart?.()
35
+ const request = {
36
+ event,
37
+ context,
38
+ response: undefined,
39
+ error: undefined,
40
+ internal: plugin.internal ?? {}
22
41
  }
23
- plugin = {
24
- ...defaultPlugin,
25
- ...plugin
26
- };
27
- plugin.timeoutEarly = plugin.timeoutEarlyInMillis > 0;
28
- plugin.beforePrefetch?.();
29
- const beforeMiddlewares = [];
30
- const afterMiddlewares = [];
31
- const onErrorMiddlewares = [];
32
- const middyHandler = (event = {}, context = {})=>{
33
- plugin.requestStart?.();
34
- const request = {
35
- event,
36
- context,
37
- response: undefined,
38
- error: undefined,
39
- internal: plugin.internal ?? {}
40
- };
41
- return runRequest(request, [
42
- ...beforeMiddlewares
43
- ], lambdaHandler, [
44
- ...afterMiddlewares
45
- ], [
46
- ...onErrorMiddlewares
47
- ], plugin);
48
- };
49
- const middy = plugin.streamifyResponse ? awslambda.streamifyResponse(async (event, responseStream, context)=>{
50
- const handlerResponse = await middyHandler(event, context);
51
- let handlerBody = handlerResponse;
52
- if (handlerResponse.statusCode) {
53
- handlerBody = handlerResponse.body ?? '';
54
- delete handlerResponse.body;
55
- responseStream = awslambda.HttpResponseStream.from(responseStream, handlerResponse);
56
- }
57
- let handlerStream;
58
- if (handlerBody._readableState) {
59
- handlerStream = handlerBody;
60
- } else if (typeof handlerBody === 'string') {
61
- function* iterator(input) {
62
- const size = 16384;
63
- let position = 0;
64
- const length = input.length;
65
- while(position < length){
66
- yield input.substring(position, position + size);
67
- position += size;
68
- }
69
- }
70
- handlerStream = Readable.from(iterator(handlerBody));
71
- }
72
- if (!handlerStream) {
73
- throw new Error('handler response not a ReadableStream');
74
- }
75
- await pipeline(handlerStream, responseStream);
76
- }) : middyHandler;
77
- middy.use = (middlewares)=>{
78
- if (!Array.isArray(middlewares)) {
79
- middlewares = [
80
- middlewares
81
- ];
42
+
43
+ return runRequest(
44
+ request,
45
+ beforeMiddlewares,
46
+ lambdaHandler,
47
+ afterMiddlewares,
48
+ onErrorMiddlewares,
49
+ plugin
50
+ )
51
+ }
52
+ const middy = plugin.streamifyResponse
53
+ ? awslambda.streamifyResponse(async (event, responseStream, context) => {
54
+ const handlerResponse = await middyHandler(event, context)
55
+
56
+ let handlerBody = handlerResponse
57
+ if (handlerResponse.statusCode) {
58
+ handlerBody = handlerResponse.body ?? ''
59
+ delete handlerResponse.body // #1137
60
+ responseStream = awslambda.HttpResponseStream.from(
61
+ responseStream,
62
+ handlerResponse
63
+ )
64
+ }
65
+
66
+ // Source @datastream/core (MIT)
67
+ let handlerStream
68
+ if (handlerBody._readableState) {
69
+ handlerStream = handlerBody
70
+ } else if (typeof handlerBody === 'string') {
71
+ function * iterator (input) {
72
+ const size = 16384 // 16 * 1024 // Node.js default
73
+ let position = 0
74
+ const length = input.length
75
+ while (position < length) {
76
+ yield input.substring(position, position + size)
77
+ position += size
78
+ }
82
79
  }
83
- for (const middleware of middlewares){
84
- const { before, after, onError } = middleware;
85
- if (!before && !after && !onError) {
86
- throw new Error('Middleware must be an object containing at least one key among "before", "after", "onError"');
80
+ handlerStream = Readable.from(iterator(handlerBody))
81
+ }
82
+
83
+ if (!handlerStream) {
84
+ throw new Error('handler response not a ReadableStream')
85
+ }
86
+
87
+ await pipeline(handlerStream, responseStream)
88
+ })
89
+ : middyHandler
90
+
91
+ middy.use = (middlewares) => {
92
+ if (!Array.isArray(middlewares)) {
93
+ middlewares = [middlewares]
94
+ }
95
+ for (const middleware of middlewares) {
96
+ const { before, after, onError } = middleware
97
+
98
+ if (before || after || onError) {
99
+ if (before) middy.before(before)
100
+ if (after) middy.after(after)
101
+ if (onError) middy.onError(onError)
102
+ } else {
103
+ throw new Error(
104
+ 'Middleware must be an object containing at least one key among "before", "after", "onError"'
105
+ )
106
+ }
107
+ }
108
+ return middy
109
+ }
110
+
111
+ // Inline Middlewares
112
+ middy.before = (beforeMiddleware) => {
113
+ beforeMiddlewares.push(beforeMiddleware)
114
+ return middy
115
+ }
116
+ middy.after = (afterMiddleware) => {
117
+ afterMiddlewares.unshift(afterMiddleware)
118
+ return middy
119
+ }
120
+ middy.onError = (onErrorMiddleware) => {
121
+ onErrorMiddlewares.unshift(onErrorMiddleware)
122
+ return middy
123
+ }
124
+ middy.handler = (replaceLambdaHandler) => {
125
+ lambdaHandler = replaceLambdaHandler
126
+ return middy
127
+ }
128
+
129
+ return middy
130
+ }
131
+
132
+ // shared AbortController, because it's slow
133
+ let handlerAbort = new AbortController()
134
+ const runRequest = async (
135
+ request,
136
+ beforeMiddlewares,
137
+ lambdaHandler,
138
+ afterMiddlewares,
139
+ onErrorMiddlewares,
140
+ plugin
141
+ ) => {
142
+ let timeoutID
143
+ // context.getRemainingTimeInMillis checked for when AWS context missing (tests, containers)
144
+ const timeoutEarly =
145
+ plugin.timeoutEarly && request.context.getRemainingTimeInMillis
146
+
147
+ try {
148
+ await runMiddlewares(request, beforeMiddlewares, plugin)
149
+
150
+ // Check if before stack hasn't exit early
151
+ if (typeof request.response === 'undefined') {
152
+ plugin.beforeHandler?.()
153
+
154
+ // Can't manually abort and timeout with same AbortSignal
155
+ // https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
156
+ if (handlerAbort.signal.aborted) {
157
+ handlerAbort = new AbortController()
158
+ }
159
+ const promises = [
160
+ lambdaHandler(request.event, request.context, {
161
+ signal: handlerAbort.signal
162
+ })
163
+ ]
164
+
165
+ // clearTimeout pattern is 10x faster than using AbortController
166
+ // Note: signal.abort is slow ~6000ns
167
+ if (timeoutEarly) {
168
+ let timeoutResolve
169
+ const timeoutPromise = new Promise((resolve, reject) => {
170
+ timeoutResolve = () => {
171
+ handlerAbort.abort()
172
+ try {
173
+ resolve(plugin.timeoutEarlyResponse())
174
+ } catch (e) {
175
+ reject(e)
87
176
  }
88
- if (before) middy.before(before);
89
- if (after) middy.after(after);
90
- if (onError) middy.onError(onError);
91
- }
92
- return middy;
93
- };
94
- middy.before = (beforeMiddleware)=>{
95
- beforeMiddlewares.push(beforeMiddleware);
96
- return middy;
97
- };
98
- middy.after = (afterMiddleware)=>{
99
- afterMiddlewares.unshift(afterMiddleware);
100
- return middy;
101
- };
102
- middy.onError = (onErrorMiddleware)=>{
103
- onErrorMiddlewares.unshift(onErrorMiddleware);
104
- return middy;
105
- };
106
- middy.handler = (replaceLambdaHandler)=>{
107
- lambdaHandler = replaceLambdaHandler;
108
- return middy;
109
- };
110
- return middy;
111
- };
112
- const runRequest = async (request, beforeMiddlewares, lambdaHandler, afterMiddlewares, onErrorMiddlewares, plugin)=>{
113
- let timeoutAbort;
114
- const timeoutEarly = plugin.timeoutEarly && request.context.getRemainingTimeInMillis;
177
+ }
178
+ })
179
+ timeoutID = setTimeout(
180
+ timeoutResolve,
181
+ request.context.getRemainingTimeInMillis() -
182
+ plugin.timeoutEarlyInMillis
183
+ )
184
+ promises.push(timeoutPromise)
185
+ }
186
+ request.response = await Promise.race(promises)
187
+ if (timeoutID) {
188
+ clearTimeout(timeoutID)
189
+ }
190
+
191
+ plugin.afterHandler?.()
192
+ await runMiddlewares(request, afterMiddlewares, plugin)
193
+ }
194
+ } catch (e) {
195
+ // timeout should be aborted when errors happen in handler
196
+ if (timeoutID) {
197
+ clearTimeout(timeoutID)
198
+ }
199
+
200
+ // Reset response changes made by after stack before error thrown
201
+ request.response = undefined
202
+ request.error = e
115
203
  try {
116
- await runMiddlewares(request, beforeMiddlewares, plugin);
117
- if (typeof request.response === 'undefined') {
118
- plugin.beforeHandler?.();
119
- const handlerAbort = new AbortController();
120
- if (timeoutEarly) timeoutAbort = new AbortController();
121
- request.response = await Promise.race([
122
- lambdaHandler(request.event, request.context, {
123
- signal: handlerAbort.signal
124
- }),
125
- timeoutEarly ? setTimeout(request.context.getRemainingTimeInMillis() - plugin.timeoutEarlyInMillis, undefined, {
126
- signal: timeoutAbort.signal
127
- }).then(()=>{
128
- handlerAbort.abort();
129
- return plugin.timeoutEarlyResponse();
130
- }) : Promise.race([])
131
- ]);
132
- timeoutAbort?.abort();
133
- plugin.afterHandler?.();
134
- await runMiddlewares(request, afterMiddlewares, plugin);
135
- }
204
+ await runMiddlewares(request, onErrorMiddlewares, plugin)
136
205
  } catch (e) {
137
- timeoutAbort?.abort();
138
- request.response = undefined;
139
- request.error = e;
140
- try {
141
- await runMiddlewares(request, onErrorMiddlewares, plugin);
142
- } catch (e) {
143
- e.originalError = request.error;
144
- request.error = e;
145
- throw request.error;
146
- }
147
- if (typeof request.response === 'undefined') throw request.error;
148
- } finally{
149
- await plugin.requestEnd?.(request);
206
+ // Save error that wasn't handled
207
+ e.originalError = request.error
208
+ request.error = e
209
+
210
+ throw request.error
150
211
  }
151
- return request.response;
152
- };
153
- const runMiddlewares = async (request, middlewares, plugin)=>{
154
- for (const nextMiddleware of middlewares){
155
- plugin.beforeMiddleware?.(nextMiddleware.name);
156
- const res = await nextMiddleware(request);
157
- plugin.afterMiddleware?.(nextMiddleware.name);
158
- if (typeof res !== 'undefined') {
159
- request.response = res;
160
- return;
161
- }
212
+ // Catch if onError stack hasn't handled the error
213
+ if (typeof request.response === 'undefined') throw request.error
214
+ } finally {
215
+ await plugin.requestEnd?.(request)
216
+ }
217
+
218
+ return request.response
219
+ }
220
+
221
+ const runMiddlewares = async (request, middlewares, plugin) => {
222
+ for (const nextMiddleware of middlewares) {
223
+ plugin.beforeMiddleware?.(nextMiddleware.name)
224
+ const res = await nextMiddleware(request)
225
+ plugin.afterMiddleware?.(nextMiddleware.name)
226
+ // short circuit chaining and respond early
227
+ if (typeof res !== 'undefined') {
228
+ request.response = res
229
+ return
162
230
  }
163
- };
164
- export default middy;
231
+ }
232
+ }
165
233
 
234
+ export default middy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@middy/core",
3
- "version": "5.0.3",
3
+ "version": "5.2.0",
4
4
  "description": "🛵 The stylish Node.js middleware engine for AWS Lambda (core package)",
5
5
  "type": "module",
6
6
  "engines": {
@@ -27,7 +27,8 @@
27
27
  "scripts": {
28
28
  "test": "npm run test:unit",
29
29
  "test:unit": "ava",
30
- "test:benchmark": "node __benchmarks__/index.js"
30
+ "test:benchmark": "node __benchmarks__/index.js",
31
+ "test:profile": "node --prof __benchmarks__/index.js && node --prof-process --preprocess -j isolate*.log | speedscope -"
31
32
  },
32
33
  "license": "MIT",
33
34
  "keywords": [
@@ -59,7 +60,7 @@
59
60
  "@types/aws-lambda": "^8.10.76",
60
61
  "@types/node": "^20.0.0"
61
62
  },
62
- "gitHead": "87660575a7ac2b52e4153c407a4c63c9449dcd0d",
63
+ "gitHead": "2d9096a49cd8fb62359517be96d6c93609df41f0",
63
64
  "dependencies": {
64
65
  "@datastream/core": "0.0.35"
65
66
  }