@63klabs/cache-data 1.3.5 → 1.3.7

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.
@@ -11,6 +11,35 @@ const printMsg = function() {
11
11
  console.log("This is a message from the demo package");
12
12
  };
13
13
 
14
+ /**
15
+ * Safely clones an object, handling Promises and other non-cloneable values.
16
+ * If the object contains Promises, they will be converted to empty objects.
17
+ * Falls back to JSON.parse(JSON.stringify()) if structuredClone fails.
18
+ *
19
+ * @param {*} obj - The object to clone
20
+ * @returns {*} A deep clone of the object
21
+ */
22
+ const safeClone = function(obj) {
23
+ if (obj === null || typeof obj !== 'object') {
24
+ return obj;
25
+ }
26
+
27
+ try {
28
+ // Try structuredClone first (fastest for most cases)
29
+ return structuredClone(obj);
30
+ } catch (e) {
31
+ // If structuredClone fails (e.g., due to Promises), fall back to JSON pattern
32
+ // This will convert Promises to empty objects, but won't throw
33
+ try {
34
+ return JSON.parse(JSON.stringify(obj));
35
+ } catch (jsonError) {
36
+ // If even JSON fails, return the original object
37
+ // This handles circular references and other edge cases
38
+ return obj;
39
+ }
40
+ }
41
+ };
42
+
14
43
  /**
15
44
  * Given a secret string, returns a string padded out at the beginning
16
45
  * with * or passed character leaving only the specified number of characters unobfuscated.
@@ -227,6 +256,9 @@ const sanitize = function (obj) {
227
256
  */
228
257
  const hashThisData = function(algorithm, data, options = {}) {
229
258
 
259
+ // Clone options to avoid modifying the original
260
+ options = safeClone(options);
261
+
230
262
  // set default values for options
231
263
  if ( !( "salt" in options) ) { options.salt = ""; }
232
264
  if ( !( "iterations" in options) || options.iterations < 1 ) { options.iterations = 1; }
@@ -234,7 +266,8 @@ const hashThisData = function(algorithm, data, options = {}) {
234
266
 
235
267
  // if it is an object or array, then parse it to remove non-data elements (functions, etc)
236
268
  if ( !options.skipParse && (typeof data === "object" || Array.isArray(data))) {
237
- data = JSON.parse(JSON.stringify(data, (key, value) => {
269
+ // Normalize the data structure first using JSON.stringify with custom replacer
270
+ const normalized = JSON.parse(JSON.stringify(data, (key, value) => {
238
271
  switch (typeof value) {
239
272
  case 'bigint':
240
273
  return value.toString();
@@ -244,6 +277,8 @@ const hashThisData = function(algorithm, data, options = {}) {
244
277
  return value;
245
278
  }
246
279
  }));
280
+ // Then clone for safety
281
+ data = safeClone(normalized);
247
282
  options.skipParse = true; // set to true so we don't parse during recursion
248
283
  }
249
284
 
@@ -272,7 +307,7 @@ const hashThisData = function(algorithm, data, options = {}) {
272
307
  // iterate through the keys alphabetically and add the key and value to the arrayOfStuff
273
308
  keys.forEach((key) => {
274
309
  // clone options
275
- const opts = JSON.parse(JSON.stringify(options));
310
+ const opts = safeClone(options);
276
311
  opts.iterations = 1; // don't iterate during recursion, only at end
277
312
 
278
313
  const value = hashThisData(algorithm, data[key], opts);
@@ -285,11 +320,11 @@ const hashThisData = function(algorithm, data, options = {}) {
285
320
  valueStr = `-:::${dataType}:::${data.toString()}`;
286
321
  }
287
322
 
288
- const hash = crypto.createHash(algorithm);
289
323
  let hashOfData = "";
290
324
 
291
325
  // hash for the number of iterations
292
326
  for (let i = 0; i < options.iterations; i++) {
327
+ const hash = crypto.createHash(algorithm);
293
328
  hash.update(valueStr + hashOfData + options.salt);
294
329
  hashOfData = hash.digest('hex');
295
330
  }
@@ -301,5 +336,6 @@ module.exports = {
301
336
  printMsg,
302
337
  sanitize,
303
338
  obfuscate,
304
- hashThisData
339
+ hashThisData,
340
+ safeClone
305
341
  };
@@ -0,0 +1,221 @@
1
+ /**
2
+ * InMemoryCache - Ultra-fast in-memory L0 cache for AWS Lambda
3
+ *
4
+ * Provides microsecond-level cache access using JavaScript Map with LRU eviction.
5
+ * Designed for Lambda execution model with synchronous operations and no background processes.
6
+ * @example
7
+ * // Create cache with automatic sizing based on Lambda memory
8
+ * const cache = new InMemoryCache();
9
+ *
10
+ * // Store data with expiration
11
+ * const data = { user: 'john', email: 'john@example.com' };
12
+ * const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes
13
+ * cache.set('user:123', data, expiresAt);
14
+ *
15
+ * // Retrieve data
16
+ * const result = cache.get('user:123');
17
+ * if (result.cache === 1) {
18
+ * console.log('Cache hit:', result.data);
19
+ * }
20
+ *
21
+ * @example
22
+ * // Create cache with explicit max entries
23
+ * const cache = new InMemoryCache({ maxEntries: 10000 });
24
+ *
25
+ * // Check cache info
26
+ * const info = cache.info();
27
+ * console.log(`Cache size: ${info.size}/${info.maxEntries}`);
28
+ * console.log(`Memory: ${info.memoryMB}MB`);
29
+ *
30
+ * @example
31
+ * // Use with Cache.init() for in-memory caching
32
+ * Cache.init({
33
+ * useInMemoryCache: true,
34
+ * inMemoryCacheMaxEntries: 5000
35
+ * });
36
+ *
37
+ * // Cache automatically uses InMemoryCache as L0 cache
38
+ * const cacheInstance = new Cache(connection, profile);
39
+ * const data = await cacheInstance.read(); // Checks in-memory first
40
+ *
41
+ * @example
42
+ * // Clear cache when needed
43
+ * cache.clear();
44
+ * console.log('Cache cleared');
45
+ */
46
+
47
+ class InMemoryCache {
48
+ #cache;
49
+ #maxEntries;
50
+ #memoryMB;
51
+
52
+ /**
53
+ * Creates a new InMemoryCache instance
54
+ *
55
+ * @param {Object} options - Configuration options
56
+ * @param {number} [options.maxEntries] - Maximum number of entries (overrides calculation)
57
+ * @param {number} [options.entriesPerGB=5000] - Heuristic for calculating maxEntries from memory
58
+ * @param {number} [options.defaultMaxEntries=1000] - Fallback if Lambda memory unavailable
59
+ */
60
+ constructor(options = {}) {
61
+ const {
62
+ maxEntries,
63
+ entriesPerGB = 5000,
64
+ defaultMaxEntries = 1000
65
+ } = options;
66
+
67
+ // Initialize Map storage
68
+ this.#cache = new Map();
69
+
70
+ // Determine MAX_ENTRIES
71
+ if (maxEntries !== undefined && maxEntries !== null) {
72
+ // Use explicit maxEntries parameter
73
+ this.#maxEntries = maxEntries;
74
+ this.#memoryMB = null;
75
+ } else {
76
+ // Calculate from Lambda memory allocation
77
+ const lambdaMemory = process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE;
78
+
79
+ if (lambdaMemory !== undefined && lambdaMemory !== null) {
80
+ this.#memoryMB = parseInt(lambdaMemory, 10);
81
+
82
+ if (!isNaN(this.#memoryMB) && this.#memoryMB > 0) {
83
+ // Calculate: (memoryMB / 1024) * entriesPerGB
84
+ this.#maxEntries = Math.floor((this.#memoryMB / 1024) * entriesPerGB);
85
+ } else {
86
+ // Invalid memory value, use default
87
+ this.#maxEntries = defaultMaxEntries;
88
+ this.#memoryMB = null;
89
+ }
90
+ } else {
91
+ // Lambda memory not available, use default
92
+ this.#maxEntries = defaultMaxEntries;
93
+ this.#memoryMB = null;
94
+ }
95
+ }
96
+
97
+ // Ensure maxEntries is at least 1
98
+ if (this.#maxEntries < 1) {
99
+ this.#maxEntries = 1;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Retrieves a cache entry by key
105
+ *
106
+ * Returns an object containing the cache status and the cached data (if available).
107
+ * The cache status indicates whether the lookup was a hit (1), miss (0), or expired (-1).
108
+ * For cache hits, the data property contains the stored CacheDataFormat object. For misses, data is null.
109
+ * For expired entries, data contains the expired CacheDataFormat object before it's removed from cache.
110
+ *
111
+ * @param {string} key - Cache key to look up
112
+ * @returns {{cache: number, data: Object|null}} Lookup result with cache status and data
113
+ * @returns {number} return.cache - Cache status: 1 (hit), 0 (miss), or -1 (expired)
114
+ * @returns {Object|null} return.data - The cached CacheDataFormat object on hit/expired, or null on miss
115
+ *
116
+ * @example
117
+ * // Cache hit
118
+ * cache.get('myKey')
119
+ * // => { cache: 1, data: { cache: { body: '...', headers: {...}, expires: 1234567890, statusCode: '200' } } }
120
+ *
121
+ * @example
122
+ * // Cache miss
123
+ * cache.get('nonExistent') // => { cache: 0, data: null }
124
+ *
125
+ * @example
126
+ * // Expired entry
127
+ * cache.get('expiredKey')
128
+ * // => { cache: -1, data: { cache: { body: '...', headers: {...}, expires: 1234567890, statusCode: '200' } } }
129
+ */
130
+ get(key) {
131
+ // Check if key exists
132
+ if (!this.#cache.has(key)) {
133
+ return { cache: 0, data: null };
134
+ }
135
+
136
+ const entry = this.#cache.get(key);
137
+ const now = Date.now();
138
+
139
+ // Check if expired
140
+ if (entry.expiresAt <= now) {
141
+ // Delete expired entry
142
+ this.#cache.delete(key);
143
+ return { cache: -1, data: entry.value };
144
+ }
145
+
146
+ // Valid entry - update LRU position by deleting and re-setting
147
+ this.#cache.delete(key);
148
+ this.#cache.set(key, entry);
149
+
150
+ return { cache: 1, data: entry.value };
151
+ }
152
+
153
+ /**
154
+ * Stores a cache entry with expiration
155
+ *
156
+ * Adds or updates a cache entry with the specified key, value, and expiration time.
157
+ * If the key already exists, it will be updated and moved to the most recent position (LRU).
158
+ * If the cache is at maximum capacity, the least recently used entry will be evicted automatically.
159
+ * The expiresAt timestamp should be in milliseconds since epoch (e.g., Date.now() + ttl).
160
+ *
161
+ * @param {string} key - Cache key to store the value under
162
+ * @param {Object} value - CacheDataFormat object or any data structure to cache
163
+ * @param {number} expiresAt - Expiration timestamp in milliseconds since epoch (e.g., Date.now() + 60000 for 1 minute)
164
+ * @returns {void}
165
+ *
166
+ * @example
167
+ * // Store a cache entry that expires in 5 minutes
168
+ * const fiveMinutes = 5 * 60 * 1000;
169
+ * cache.set('myKey', { cache: { body: 'response data', headers: {}, expires: Date.now() + fiveMinutes, statusCode: '200' } }, Date.now() + fiveMinutes);
170
+ *
171
+ * @example
172
+ * // Store with a specific expiration timestamp
173
+ * const expirationTime = Date.now() + (60 * 60 * 1000); // 1 hour from now
174
+ * cache.set('sessionData', { userId: 123, token: 'abc' }, expirationTime);
175
+ *
176
+ * @example
177
+ * // Update an existing entry (moves to most recent position)
178
+ * cache.set('existingKey', { updated: 'data' }, Date.now() + 300000);
179
+ */
180
+ set(key, value, expiresAt) {
181
+ // If key exists, delete it first for LRU repositioning
182
+ if (this.#cache.has(key)) {
183
+ this.#cache.delete(key);
184
+ }
185
+
186
+ // Check capacity and evict if necessary
187
+ if (this.#cache.size >= this.#maxEntries) {
188
+ // Get first (oldest) entry
189
+ const oldestKey = this.#cache.keys().next().value;
190
+ this.#cache.delete(oldestKey);
191
+ }
192
+
193
+ // Store new entry
194
+ this.#cache.set(key, { value, expiresAt });
195
+ }
196
+
197
+ /**
198
+ * Clears all entries from the cache
199
+ */
200
+ clear() {
201
+ this.#cache.clear();
202
+ }
203
+
204
+ /**
205
+ * Returns information about the cache state
206
+ *
207
+ * @returns {Object} Cache information
208
+ * @returns {number} return.size - Current number of entries
209
+ * @returns {number} return.maxEntries - Maximum capacity
210
+ * @returns {number|null} return.memoryMB - Lambda memory allocation (if available)
211
+ */
212
+ info() {
213
+ return {
214
+ size: this.#cache.size,
215
+ maxEntries: this.#maxEntries,
216
+ memoryMB: this.#memoryMB
217
+ };
218
+ }
219
+ }
220
+
221
+ module.exports = InMemoryCache;