@middy/util 7.2.1 → 7.2.3

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 +3 -0
  2. package/index.js +58 -10
  3. package/package.json +2 -2
package/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export interface Options<Client, ClientOptions> {
21
21
  cacheKey?: string;
22
22
  cacheExpiry?: number;
23
23
  cacheKeyExpiry?: Record<string, number>;
24
+ cacheMaxSize?: number;
24
25
  setToContext?: boolean;
25
26
  }
26
27
 
@@ -157,6 +158,8 @@ declare function jsonSafeStringify(
157
158
  space?: string | number,
158
159
  ): string | unknown;
159
160
 
161
+ declare const jsonContentTypePattern: RegExp;
162
+
160
163
  declare function decodeBody(event: {
161
164
  body?: string | null;
162
165
  isBase64Encoded?: boolean;
package/index.js CHANGED
@@ -51,7 +51,7 @@ const safeGet = (obj, key) =>
51
51
 
52
52
  // Internal Context
53
53
  export const getInternal = async (variables, request) => {
54
- if (!variables || !request) return {};
54
+ if (!variables || !request) return Object.create(null);
55
55
  let keys = [];
56
56
  let values = [];
57
57
  if (variables === true) {
@@ -86,7 +86,7 @@ export const getInternal = async (variables, request) => {
86
86
  syncResults[i] = value;
87
87
  }
88
88
  if (allSync) {
89
- const obj = {};
89
+ const obj = Object.create(null);
90
90
  for (let i = 0; i < keys.length; i++) {
91
91
  obj[sanitizeKey(keys[i])] = syncResults[i];
92
92
  }
@@ -110,7 +110,7 @@ export const getInternal = async (variables, request) => {
110
110
  // ensure promise has resolved by the time it's needed
111
111
  // If one of the promises throws it will bubble up to @middy/core
112
112
  values = await Promise.allSettled(promises);
113
- const obj = {};
113
+ const obj = Object.create(null);
114
114
  let errors;
115
115
  for (let i = 0; i < keys.length; i++) {
116
116
  if (values[i].status === "rejected") {
@@ -140,13 +140,30 @@ export const sanitizeKey = (key) => {
140
140
 
141
141
  // fetch Cache
142
142
  const cache = Object.create(null); // key: { value:{fetchKey:Promise}, expiry }
143
+ const defaultCacheMaxSize = 128;
144
+
145
+ const validateCacheExpiry = (cacheExpiry) => {
146
+ if (
147
+ typeof cacheExpiry === "number" &&
148
+ cacheExpiry < -1 &&
149
+ !Number.isNaN(cacheExpiry)
150
+ ) {
151
+ throw new Error(
152
+ `Invalid cacheExpiry value: ${cacheExpiry}. Must be -1 (infinite), 0 (disabled), or a positive number (ms duration or unix timestamp)`,
153
+ { cause: { package: "@middy/util" } },
154
+ );
155
+ }
156
+ };
157
+
143
158
  export const processCache = (
144
159
  options,
145
160
  middlewareFetch = () => undefined,
146
161
  middlewareFetchRequest = {},
147
162
  ) => {
148
- let { cacheKey, cacheKeyExpiry, cacheExpiry } = options;
163
+ let { cacheKey, cacheKeyExpiry, cacheExpiry, cacheMaxSize } = options;
164
+ cacheMaxSize ??= defaultCacheMaxSize;
149
165
  cacheExpiry = cacheKeyExpiry?.[cacheKey] ?? cacheExpiry;
166
+ validateCacheExpiry(cacheExpiry);
150
167
  const now = Date.now();
151
168
  if (cacheExpiry) {
152
169
  const cached = getCache(cacheKey);
@@ -164,10 +181,15 @@ export const processCache = (
164
181
  }
165
182
  }
166
183
  const value = middlewareFetch(middlewareFetchRequest);
167
- // secrets-manager can override to unix timestamp
184
+ // cacheExpiry semantics:
185
+ // >86400000 (24h): treated as unix timestamp (ms)
186
+ // >0 && <=86400000: treated as duration (ms) from now
187
+ // -1: infinite cache (never expires)
188
+ // 0/undefined/null: no caching
168
189
  const expiry = cacheExpiry > 86400000 ? cacheExpiry : now + cacheExpiry;
169
190
  const duration = cacheExpiry > 86400000 ? cacheExpiry - now : cacheExpiry;
170
191
  if (cacheExpiry) {
192
+ clearTimeout(cache[cacheKey]?.refresh);
171
193
  const refresh =
172
194
  duration > 0
173
195
  ? setTimeout(
@@ -177,6 +199,7 @@ export const processCache = (
177
199
  )
178
200
  : undefined;
179
201
  cache[cacheKey] = { value, expiry, refresh };
202
+ evictCache(cacheMaxSize);
180
203
  }
181
204
  return { value, expiry };
182
205
  };
@@ -195,10 +218,29 @@ export const getCache = (key) => {
195
218
 
196
219
  // Used to remove parts of a cache
197
220
  export const modifyCache = (cacheKey, value) => {
198
- if (!cache[cacheKey]) return;
199
- clearTimeout(cache[cacheKey].refresh);
200
- cache[cacheKey].value = value;
201
- cache[cacheKey].modified = true;
221
+ const entry = cache[cacheKey];
222
+ if (!entry) return;
223
+ clearTimeout(entry.refresh);
224
+ entry.value = value;
225
+ entry.modified = true;
226
+ };
227
+
228
+ const evictCache = (maxSize) => {
229
+ const cacheKeys = Object.keys(cache);
230
+ if (cacheKeys.length <= maxSize) return;
231
+ let oldestKey = null;
232
+ let oldestExpiry = Infinity;
233
+ for (const key of cacheKeys) {
234
+ const entry = cache[key];
235
+ if (entry && entry.expiry < oldestExpiry) {
236
+ oldestExpiry = entry.expiry;
237
+ oldestKey = key;
238
+ }
239
+ }
240
+ if (oldestKey) {
241
+ clearTimeout(cache[oldestKey]?.refresh);
242
+ cache[oldestKey] = undefined;
243
+ }
202
244
  };
203
245
 
204
246
  export const clearCache = (inputKeys = null) => {
@@ -269,6 +311,9 @@ export const jsonSafeStringify = (value, replacer, space) => {
269
311
  }
270
312
  };
271
313
 
314
+ export const jsonContentTypePattern =
315
+ /^application\/([a-z0-9.+-]+\+)?json(;|$)/i;
316
+
272
317
  export const decodeBody = (event) => {
273
318
  const { body, isBase64Encoded } = event;
274
319
  if (typeof body === "undefined" || body === null) return body;
@@ -304,7 +349,10 @@ export class HttpError extends Error {
304
349
  message ??= httpErrorCodes[code];
305
350
  super(message, options);
306
351
 
307
- const name = httpErrorCodes[code].replace(createErrorRegexp, "");
352
+ const name = (httpErrorCodes[code] ?? "Unknown").replace(
353
+ createErrorRegexp,
354
+ "",
355
+ );
308
356
  this.name = !name.endsWith("Error") ? `${name}Error` : name;
309
357
 
310
358
  this.status = this.statusCode = code; // setting `status` for backwards compatibility w/ `http-errors`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@middy/util",
3
- "version": "7.2.1",
3
+ "version": "7.2.3",
4
4
  "description": "🛵 The stylish Node.js middleware engine for AWS Lambda (util package)",
5
5
  "type": "module",
6
6
  "engines": {
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@aws-sdk/client-ssm": "^3.0.0",
63
- "@middy/core": "7.2.1",
63
+ "@middy/core": "7.2.3",
64
64
  "@types/aws-lambda": "^8.0.0",
65
65
  "@types/node": "^22.0.0",
66
66
  "aws-xray-sdk": "^3.3.3"