@63klabs/cache-data 1.3.4 → 1.3.6

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.
@@ -33,9 +33,24 @@ const objHash = require('object-hash');
33
33
  const moment = require('moment-timezone');
34
34
 
35
35
  /**
36
- * Basic S3 read/write for cache data. No cache logic,
37
- * only handles the storage format and retrieval.
38
- * Logic is handled by CacheData.
36
+ * Basic S3 read/write for cache data. Provides low-level storage operations
37
+ * for cached data in Amazon S3. This class handles only the storage format
38
+ * and retrieval operations - cache expiration logic and data management are
39
+ * handled by the CacheData class.
40
+ *
41
+ * Use this class when you need direct access to S3 cache storage operations.
42
+ * For most use cases, use the Cache or CacheableDataAccess classes instead.
43
+ *
44
+ * @example
45
+ * // Initialize S3Cache with bucket name
46
+ * S3Cache.init('my-cache-bucket');
47
+ *
48
+ * // Write data to cache
49
+ * const data = JSON.stringify({ body: 'content', headers: {} });
50
+ * await S3Cache.write('cache-key-hash', data);
51
+ *
52
+ * // Read data from cache
53
+ * const cachedData = await S3Cache.read('cache-key-hash');
39
54
  */
40
55
  class S3Cache {
41
56
 
@@ -46,24 +61,50 @@ class S3Cache {
46
61
  };
47
62
 
48
63
  /**
64
+ * Get the S3 bucket name configured for cache storage.
65
+ * Returns null if S3Cache has not been initialized.
49
66
  *
50
- * @returns {string} The bucket name used for cached data
67
+ * @returns {string|null} The bucket name used for cached data, or null if not initialized
68
+ * @example
69
+ * const bucketName = S3Cache.getBucket();
70
+ * console.log(`Cache bucket: ${bucketName}`);
51
71
  */
52
72
  static getBucket() {
53
73
  return this.#bucket;
54
74
  };
55
75
 
56
76
  /**
77
+ * Get the S3 object key prefix (path) used for all cache objects.
78
+ * This prefix is prepended to all cache object keys to organize
79
+ * cached data within the bucket.
57
80
  *
58
- * @returns {string} The object key (path) for cache objects
81
+ * @returns {string} The object key prefix for cache objects (default: "cache/")
82
+ * @example
83
+ * const cachePath = S3Cache.getPath();
84
+ * console.log(`Cache path prefix: ${cachePath}`); // "cache/"
59
85
  */
60
86
  static getPath() {
61
87
  return this.#objPath;
62
88
  };
63
89
 
64
90
  /**
65
- * Initialize the S3 bucket for storing cached data.
66
- * @param {string} bucket The bucket name for storing cached data
91
+ * Initialize the S3 bucket for storing cached data. This method must be
92
+ * called before any read or write operations. Can only be initialized once -
93
+ * subsequent calls will be ignored with a warning.
94
+ *
95
+ * The bucket name can be provided as a parameter or via the
96
+ * CACHE_DATA_S3_BUCKET environment variable.
97
+ *
98
+ * @param {string|null} [bucket=null] The bucket name for storing cached data. If null, uses CACHE_DATA_S3_BUCKET environment variable
99
+ * @throws {Error} If no bucket name is provided and CACHE_DATA_S3_BUCKET environment variable is not set
100
+ * @example
101
+ * // Initialize with explicit bucket name
102
+ * S3Cache.init('my-cache-bucket');
103
+ *
104
+ * @example
105
+ * // Initialize using environment variable
106
+ * process.env.CACHE_DATA_S3_BUCKET = 'my-cache-bucket';
107
+ * S3Cache.init();
67
108
  */
68
109
 
69
110
  static init(bucket = null) {
@@ -81,8 +122,13 @@ class S3Cache {
81
122
  };
82
123
 
83
124
  /**
84
- * S3Cache information
85
- * @returns {{bucket: string, path: string}} The bucket and path (key) used for cached data
125
+ * Get configuration information about the S3Cache instance.
126
+ * Returns the bucket name and object key prefix currently in use.
127
+ *
128
+ * @returns {{bucket: string|null, path: string}} Object containing bucket name and path prefix used for cached data
129
+ * @example
130
+ * const info = S3Cache.info();
131
+ * console.log(`Bucket: ${info.bucket}, Path: ${info.path}`);
86
132
  */
87
133
  static info() {
88
134
  return {
@@ -92,8 +138,20 @@ class S3Cache {
92
138
  };
93
139
 
94
140
  /**
95
- * @param {Buffer|ReadableStream} s3Body
96
- * @returns {object} a parsed JSON object
141
+ * Convert an S3 response body to a JavaScript object. Handles both Buffer
142
+ * and ReadableStream response types from the AWS SDK, parsing the JSON
143
+ * content into a usable object.
144
+ *
145
+ * This method is used internally when reading cache data from S3 to convert
146
+ * the raw response body into a usable JavaScript object.
147
+ *
148
+ * @param {Buffer|ReadableStream} s3Body The S3 response body to convert
149
+ * @returns {Promise<Object>} A parsed JSON object from the S3 body content
150
+ * @throws {SyntaxError} If the body content is not valid JSON
151
+ * @example
152
+ * const s3Response = await tools.AWS.s3.get(params);
153
+ * const dataObject = await S3Cache.s3BodyToObject(s3Response.Body);
154
+ * console.log(dataObject); // { cache: { body: '...', headers: {...} } }
97
155
  */
98
156
  static async s3BodyToObject(s3Body) {
99
157
  let str = "";
@@ -112,9 +170,21 @@ class S3Cache {
112
170
  }
113
171
 
114
172
  /**
115
- * Read cache data from S3 for given idHash
116
- * @param {string} idHash The id of the cached content to retrieve
117
- * @returns {Promise<object>} Cache data
173
+ * Read cached data from S3 for the given cache identifier hash.
174
+ * Retrieves the JSON object stored at the cache key location and parses it.
175
+ * Returns null if the object doesn't exist or if an error occurs during retrieval.
176
+ *
177
+ * The cache object is stored at: {bucket}/{path}{idHash}.json
178
+ *
179
+ * @param {string} idHash The unique identifier hash of the cached content to retrieve
180
+ * @returns {Promise<Object|null>} The cached data object, or null if not found or on error
181
+ * @example
182
+ * const cachedData = await S3Cache.read('abc123def456');
183
+ * if (cachedData) {
184
+ * console.log('Cache hit:', cachedData);
185
+ * } else {
186
+ * console.log('Cache miss or error');
187
+ * }
118
188
  */
119
189
  static async read (idHash) {
120
190
 
@@ -154,10 +224,21 @@ class S3Cache {
154
224
  };
155
225
 
156
226
  /**
157
- * Write data to cache in S3
158
- * @param {string} idHash ID of data to write
159
- * @param {Object} data Data to write to cache
160
- * @returns {Promise<boolean>} Whether or not the write was successful
227
+ * Write data to the cache in S3. Stores the provided data as a JSON object
228
+ * at the cache key location. The data should already be in string format
229
+ * (typically JSON.stringify'd).
230
+ *
231
+ * The cache object is stored at: {bucket}/{path}{idHash}.json
232
+ *
233
+ * @param {string} idHash The unique identifier hash for the cache entry
234
+ * @param {string} data The data to write to cache (should be a JSON string)
235
+ * @returns {Promise<boolean>} True if write was successful, false if an error occurred
236
+ * @example
237
+ * const cacheData = JSON.stringify({ body: 'content', headers: {}, expires: 1234567890 });
238
+ * const success = await S3Cache.write('abc123def456', cacheData);
239
+ * if (success) {
240
+ * console.log('Cache written successfully');
241
+ * }
161
242
  */
162
243
  static async write (idHash, data) {
163
244
 
@@ -191,9 +272,24 @@ class S3Cache {
191
272
  };
192
273
 
193
274
  /**
194
- * Basic DynamoDb read/write for cache data. No cache logic,
195
- * only handles the storage format and retrieval.
196
- * Logic is handled by CacheData.
275
+ * Basic DynamoDb read/write for cache data. Provides low-level storage operations
276
+ * for cached data in Amazon DynamoDB. This class handles only the storage format
277
+ * and retrieval operations - cache expiration logic and data management are
278
+ * handled by the CacheData class.
279
+ *
280
+ * Use this class when you need direct access to DynamoDB cache storage operations.
281
+ * For most use cases, use the Cache or CacheableDataAccess classes instead.
282
+ *
283
+ * @example
284
+ * // Initialize DynamoDbCache with table name
285
+ * DynamoDbCache.init('my-cache-table');
286
+ *
287
+ * // Write data to cache
288
+ * const item = { id_hash: 'cache-key', data: {}, expires: 1234567890 };
289
+ * await DynamoDbCache.write(item);
290
+ *
291
+ * // Read data from cache
292
+ * const cachedData = await DynamoDbCache.read('cache-key');
197
293
  */
198
294
  class DynamoDbCache {
199
295
 
@@ -203,8 +299,23 @@ class DynamoDbCache {
203
299
  };
204
300
 
205
301
  /**
206
- * Initialize the DynamoDb settings for storing cached data
207
- * @param {string|null} table The table name to store cached data
302
+ * Initialize the DynamoDB table for storing cached data. This method must be
303
+ * called before any read or write operations. Can only be initialized once -
304
+ * subsequent calls will be ignored with a warning.
305
+ *
306
+ * The table name can be provided as a parameter or via the
307
+ * CACHE_DATA_DYNAMO_DB_TABLE environment variable.
308
+ *
309
+ * @param {string|null} [table=null] The table name to store cached data. If null, uses CACHE_DATA_DYNAMO_DB_TABLE environment variable
310
+ * @throws {Error} If no table name is provided and CACHE_DATA_DYNAMO_DB_TABLE environment variable is not set
311
+ * @example
312
+ * // Initialize with explicit table name
313
+ * DynamoDbCache.init('my-cache-table');
314
+ *
315
+ * @example
316
+ * // Initialize using environment variable
317
+ * process.env.CACHE_DATA_DYNAMO_DB_TABLE = 'my-cache-table';
318
+ * DynamoDbCache.init();
208
319
  */
209
320
  static init(table = null) {
210
321
  if ( this.#table === null ) {
@@ -221,17 +332,35 @@ class DynamoDbCache {
221
332
  };
222
333
 
223
334
  /**
224
- * Information about the DynamoDb table storing cached data
225
- * @returns {string} The DynamoDb table name
335
+ * Get configuration information about the DynamoDbCache instance.
336
+ * Returns the table name currently in use for cache storage.
337
+ *
338
+ * @returns {string|null} The DynamoDB table name, or null if not initialized
339
+ * @example
340
+ * const tableName = DynamoDbCache.info();
341
+ * console.log(`Cache table: ${tableName}`);
226
342
  */
227
343
  static info() {
228
344
  return this.#table;
229
345
  };
230
346
 
231
347
  /**
232
- * Read cache data from DynamoDb for given idHash
233
- * @param {string} idHash The id of the cached content to retrieve
234
- * @returns {Promise<object>} Cached data
348
+ * Read cached data from DynamoDB for the given cache identifier hash.
349
+ * Retrieves the cache record using the id_hash as the primary key.
350
+ * Returns an empty object if the record doesn't exist or if an error occurs.
351
+ *
352
+ * The query uses ProjectionExpression to retrieve only the necessary fields:
353
+ * id_hash, data, and expires.
354
+ *
355
+ * @param {string} idHash The unique identifier hash of the cached content to retrieve
356
+ * @returns {Promise<Object>} The DynamoDB query result containing the Item property with cached data, or empty object on error
357
+ * @example
358
+ * const result = await DynamoDbCache.read('abc123def456');
359
+ * if (result.Item) {
360
+ * console.log('Cache hit:', result.Item);
361
+ * } else {
362
+ * console.log('Cache miss or error');
363
+ * }
235
364
  */
236
365
  static async read (idHash) {
237
366
 
@@ -269,9 +398,27 @@ class DynamoDbCache {
269
398
  };
270
399
 
271
400
  /**
272
- * Write data to cache in DynamoDb
273
- * @param {object} item JSON object to write to DynamoDb
274
- * @returns {Promise<boolean>} Whether or not the write was successful
401
+ * Write data to the cache in DynamoDB. Stores the provided item object
402
+ * as a record in the DynamoDB table. The item must contain an id_hash
403
+ * property which serves as the primary key.
404
+ *
405
+ * @param {Object} item The cache item object to write to DynamoDB. Must include id_hash property
406
+ * @param {string} item.id_hash The unique identifier hash for the cache entry (primary key)
407
+ * @param {Object} item.data The cached data object
408
+ * @param {number} item.expires The expiration timestamp in seconds
409
+ * @param {number} item.purge_ts The timestamp when the expired entry should be purged
410
+ * @returns {Promise<boolean>} True if write was successful, false if an error occurred
411
+ * @example
412
+ * const item = {
413
+ * id_hash: 'abc123def456',
414
+ * data: { body: 'content', headers: {} },
415
+ * expires: 1234567890,
416
+ * purge_ts: 1234654290
417
+ * };
418
+ * const success = await DynamoDbCache.write(item);
419
+ * if (success) {
420
+ * console.log('Cache written successfully');
421
+ * }
275
422
  */
276
423
  static async write (item) {
277
424
 
@@ -303,9 +450,34 @@ class DynamoDbCache {
303
450
  };
304
451
 
305
452
  /**
306
- * Accesses cached data stored in DynamoDb and S3. CacheData is a static
307
- * object that manages expiration calculations, accessing and storing data.
308
- * This class is used by the publicly exposed class Cache
453
+ * Manages cached data stored in DynamoDB and S3. CacheData is a static class
454
+ * that handles expiration calculations, data encryption/decryption, and
455
+ * coordinates storage between DynamoDB (for small items) and S3 (for large items).
456
+ *
457
+ * This class is used internally by the publicly exposed Cache class and should
458
+ * not be used directly in most cases. Use the Cache or CacheableDataAccess
459
+ * classes instead for application-level caching.
460
+ *
461
+ * Key responsibilities:
462
+ * - Expiration time calculations and interval-based caching
463
+ * - Data encryption for private/sensitive content
464
+ * - Automatic routing between DynamoDB and S3 based on size
465
+ * - ETag and Last-Modified header generation
466
+ *
467
+ * @example
468
+ * // Initialize CacheData (typically done through Cache.init())
469
+ * CacheData.init({
470
+ * dynamoDbTable: 'my-cache-table',
471
+ * s3Bucket: 'my-cache-bucket',
472
+ * secureDataKey: Buffer.from('...'),
473
+ * secureDataAlgorithm: 'aes-256-cbc'
474
+ * });
475
+ *
476
+ * // Read from cache
477
+ * const cacheData = await CacheData.read('cache-key-hash', expiresTimestamp);
478
+ *
479
+ * // Write to cache
480
+ * await CacheData.write('cache-key-hash', now, body, headers, host, path, expires, statusCode, true);
309
481
  */
310
482
  class CacheData {
311
483
 
@@ -326,15 +498,34 @@ class CacheData {
326
498
  };
327
499
 
328
500
  /**
501
+ * Initialize CacheData with configuration parameters. This method must be called
502
+ * before any cache operations. It initializes both DynamoDbCache and S3Cache,
503
+ * sets up encryption parameters, and configures cache behavior settings.
329
504
  *
330
- * @param {Object} parameters
331
- * @param {string} parameters.dynamoDbTable
332
- * @param {string} parameters.s3Bucket
333
- * @param {string} parameters.secureDataAlgorithm
334
- * @param {string|Buffer|tools.Secret|tools.CachedSSMParameter|tools.CachedSecret} parameters.secureDataKey
335
- * @param {number} parameters.DynamoDbMaxCacheSize_kb
336
- * @param {number} parameters.purgeExpiredCacheEntriesAfterXHours
337
- * @param {string} parameters.timeZoneForInterval
505
+ * Configuration can be provided via parameters or environment variables.
506
+ *
507
+ * @param {Object} parameters Configuration object
508
+ * @param {string} [parameters.dynamoDbTable] DynamoDB table name (or use CACHE_DATA_DYNAMO_DB_TABLE env var)
509
+ * @param {string} [parameters.s3Bucket] S3 bucket name (or use CACHE_DATA_S3_BUCKET env var)
510
+ * @param {string} [parameters.secureDataAlgorithm='aes-256-cbc'] Encryption algorithm (or use CACHE_DATA_SECURE_DATA_ALGORITHM env var)
511
+ * @param {string|Buffer|tools.Secret|tools.CachedSSMParameter|tools.CachedSecret} parameters.secureDataKey Encryption key (required, no env var for security)
512
+ * @param {number} [parameters.DynamoDbMaxCacheSize_kb=10] Max size in KB for DynamoDB storage (or use CACHE_DATA_DYNAMO_DB_MAX_CACHE_SIZE_KB env var)
513
+ * @param {number} [parameters.purgeExpiredCacheEntriesAfterXHours=24] Hours after expiration to purge entries (or use CACHE_DATA_PURGE_EXPIRED_CACHE_ENTRIES_AFTER_X_HRS env var)
514
+ * @param {string} [parameters.timeZoneForInterval='Etc/UTC'] Timezone for interval calculations (or use CACHE_DATA_TIME_ZONE_FOR_INTERVAL env var)
515
+ * @throws {Error} If secureDataKey is not provided
516
+ * @throws {Error} If DynamoDbMaxCacheSize_kb is not a positive integer
517
+ * @throws {Error} If purgeExpiredCacheEntriesAfterXHours is not a positive integer
518
+ * @throws {Error} If timeZoneForInterval is not a non-empty string
519
+ * @example
520
+ * CacheData.init({
521
+ * dynamoDbTable: 'my-cache-table',
522
+ * s3Bucket: 'my-cache-bucket',
523
+ * secureDataKey: Buffer.from('my-32-byte-key-here', 'hex'),
524
+ * secureDataAlgorithm: 'aes-256-cbc',
525
+ * DynamoDbMaxCacheSize_kb: 10,
526
+ * purgeExpiredCacheEntriesAfterXHours: 24,
527
+ * timeZoneForInterval: 'America/Chicago'
528
+ * });
338
529
  */
339
530
  static init(parameters) {
340
531
 
@@ -399,10 +590,21 @@ class CacheData {
399
590
  };
400
591
 
401
592
  /**
402
- * Similar to init, but runs during execution time to refresh environment variables that may have changed since init.
403
- * Calling .prime() without an await can help get runtime refreshes started.
404
- * You can safely call .prime() again with an await to make sure it has completed just before you need it.
405
- * @returns {Promise<boolean>}
593
+ * Refresh runtime environment variables and cached secrets. This method can be
594
+ * called during execution to update values that may have changed since init().
595
+ *
596
+ * Calling prime() without await can help get runtime refreshes started in the
597
+ * background. You can safely call prime() again with await just before you need
598
+ * the refreshed values to ensure completion.
599
+ *
600
+ * @returns {Promise<boolean>} True if priming was successful, false if an error occurred
601
+ * @example
602
+ * // Start priming in background
603
+ * CacheData.prime();
604
+ *
605
+ * // Later, ensure priming is complete before using
606
+ * await CacheData.prime();
607
+ * const data = await CacheData.read(idHash, expires);
406
608
  */
407
609
  static async prime() {
408
610
  return new Promise(async (resolve) => {
@@ -424,42 +626,71 @@ class CacheData {
424
626
  }
425
627
 
426
628
  /**
427
- * Used in the init() method, based on the timeZoneForInterval and current
428
- * date, set the offset in minutes (offset from UTC taking into account
429
- * daylight savings time for that time zone)
629
+ * Internal method to set the timezone offset in minutes from UTC. This method
630
+ * is called during init() to calculate the offset based on the configured
631
+ * timeZoneForInterval and the current date/time, accounting for daylight
632
+ * saving time transitions.
633
+ *
634
+ * The offset is stored as a negative value following POSIX conventions, where
635
+ * positive offsets are west of UTC and negative offsets are east of UTC.
636
+ *
637
+ * @private
638
+ * @example
639
+ * // Called internally during CacheData.init()
640
+ * CacheData._setOffsetInMinutes();
430
641
  */
431
642
  static _setOffsetInMinutes() {
432
643
  this.#offsetInMinutes = ( -1 * moment.tz.zone(this.getTimeZoneForInterval()).utcOffset(Date.now())); // invert by *-1 because of POSIX
433
644
  };
434
645
 
435
646
  /**
647
+ * Get the timezone offset in minutes from UTC, accounting for daylight saving time.
648
+ * This offset is used for interval-based cache expiration calculations to align
649
+ * intervals with local midnight rather than UTC midnight.
650
+ *
651
+ * The offset is calculated based on the timeZoneForInterval setting and the
652
+ * current date/time to account for DST transitions.
436
653
  *
437
- * @returns {number} The offset in minutes taking into account whether or not daylight savings is in effect AND observed
654
+ * @returns {number} The offset in minutes from UTC (positive for west of UTC, negative for east)
655
+ * @example
656
+ * const offset = CacheData.getOffsetInMinutes();
657
+ * console.log(`Timezone offset: ${offset} minutes from UTC`);
438
658
  */
439
659
  static getOffsetInMinutes() {
440
660
  return this.#offsetInMinutes;
441
661
  };
442
662
 
443
663
  /**
444
- * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
445
- * @returns {string} Returns the TZ database name assigned to the time zone
664
+ * Get the TZ database timezone name used for interval calculations.
665
+ * This timezone is used to align cache expiration intervals with local time
666
+ * rather than UTC, which is useful for aligning with business hours or
667
+ * batch processing schedules.
668
+ *
669
+ * See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
670
+ *
671
+ * @returns {string} The TZ database name (e.g., 'America/Chicago', 'Etc/UTC')
672
+ * @example
673
+ * const tz = CacheData.getTimeZoneForInterval();
674
+ * console.log(`Cache intervals aligned to: ${tz}`);
446
675
  */
447
676
  static getTimeZoneForInterval() {
448
677
  return this.#timeZoneForInterval;
449
678
  };
450
679
 
451
680
  /**
452
- * Get information about the cache settings
453
- * @returns {{
454
- * dynamoDbTable: string,
455
- * s3Bucket: string,
456
- * secureDataAlgorithm: string,
457
- * secureDataKey: string,
458
- * DynamoDbMaxCacheSize_kb: number,
459
- * purgeExpiredCacheEntriesAfterXHours: number,
460
- * timeZoneForInterval: string,
461
- * offsetInMinutes: number
462
- * }}
681
+ * Get comprehensive information about the cache configuration settings.
682
+ * Returns an object containing all configuration parameters including
683
+ * DynamoDB table, S3 bucket, encryption settings, size limits, and
684
+ * timezone information.
685
+ *
686
+ * Note: The secureDataKey is masked for security and shows only the key type.
687
+ *
688
+ * @returns {{dynamoDbTable: string, s3Bucket: Object, secureDataAlgorithm: string, secureDataKey: string, DynamoDbMaxCacheSize_kb: number, purgeExpiredCacheEntriesAfterXHours: number, timeZoneForInterval: string, offsetInMinutes: number}} Configuration information object
689
+ * @example
690
+ * const info = CacheData.info();
691
+ * console.log(`Cache table: ${info.dynamoDbTable}`);
692
+ * console.log(`Max DynamoDB size: ${info.DynamoDbMaxCacheSize_kb} KB`);
693
+ * console.log(`Timezone: ${info.timeZoneForInterval}`);
463
694
  */
464
695
  static info() {
465
696
 
@@ -477,24 +708,43 @@ class CacheData {
477
708
  };
478
709
 
479
710
  /**
480
- * Format the cache object for returning to main program
481
- * @param {number} expires
482
- * @param {Object} body
483
- * @param {Object} headers
484
- * @param {string} statusCode
485
- * @returns {CacheDataFormat} Formatted cache object
711
+ * Format cache data into the standard CacheDataFormat structure.
712
+ * Creates a cache object with the specified expiration, body, headers, and status code.
713
+ *
714
+ * @param {number|null} [expires=null] Expiration timestamp in seconds
715
+ * @param {string|null} [body=null] The cached content body
716
+ * @param {Object|null} [headers=null] HTTP headers object
717
+ * @param {string|null} [statusCode=null] HTTP status code
718
+ * @returns {CacheDataFormat} Formatted cache object with cache property containing body, headers, expires, and statusCode
719
+ * @example
720
+ * const cacheData = CacheData.format(1234567890, '{"data":"value"}', {'content-type':'application/json'}, '200');
721
+ * console.log(cacheData.cache.body); // '{"data":"value"}'
722
+ * console.log(cacheData.cache.expires); // 1234567890
486
723
  */
487
724
  static format(expires = null, body = null, headers = null, statusCode = null) {
488
725
  return { "cache": { body: body, headers: headers, expires: expires, statusCode: statusCode } };
489
726
  };
490
727
 
491
728
  /**
729
+ * Internal method to process cached data retrieved from storage. Handles
730
+ * S3 pointer resolution, decryption of private data, and formatting of
731
+ * the cache response.
732
+ *
733
+ * If the cached item is stored in S3 (indicated by objInS3 flag), this method
734
+ * fetches the full data from S3. For private/encrypted data, it performs
735
+ * decryption before returning.
492
736
  *
493
- * @param {string} idHash
494
- * @param {Object} item
495
- * @param {number} syncedNow
496
- * @param {number} syncedLater
497
- * @returns {Promise<{ body: string, headers: Object, expires: number, statusCode: string }>}
737
+ * @private
738
+ * @param {string} idHash The unique identifier hash for the cache entry
739
+ * @param {Object} item The cache item object from DynamoDB
740
+ * @param {number} syncedNow Timestamp to use for immediate expiration on errors
741
+ * @param {number} syncedLater Default expiration timestamp to use if cache is not found
742
+ * @returns {Promise<{body: string|null, headers: Object|null, expires: number, statusCode: string|null}>} Processed cache data
743
+ * @example
744
+ * // Called internally by CacheData.read()
745
+ * const now = Math.floor(Date.now() / 1000);
746
+ * const later = now + 300;
747
+ * const processed = await CacheData._process(idHash, item, now, later);
498
748
  */
499
749
  static async _process(idHash, item, syncedNow, syncedLater) {
500
750
 
@@ -547,10 +797,25 @@ class CacheData {
547
797
  };
548
798
 
549
799
  /**
800
+ * Read cached data from storage (DynamoDB and potentially S3). Retrieves the
801
+ * cache entry for the given ID hash, handles decryption if needed, and returns
802
+ * the formatted cache data.
803
+ *
804
+ * If the cached item is stored in S3 (indicated by objInS3 flag), this method
805
+ * automatically fetches it from S3. Handles both public and private (encrypted)
806
+ * cache entries.
550
807
  *
551
- * @param {string} idHash
552
- * @param {number} syncedLater
553
- * @returns {Promise<CacheDataFormat>}
808
+ * @param {string} idHash The unique identifier hash for the cache entry
809
+ * @param {number} syncedLater Default expiration timestamp to use if cache is not found
810
+ * @returns {Promise<CacheDataFormat>} Formatted cache data with body, headers, expires, and statusCode
811
+ * @example
812
+ * const expiresDefault = Math.floor(Date.now() / 1000) + 300; // 5 minutes from now
813
+ * const cacheData = await CacheData.read('abc123def456', expiresDefault);
814
+ * if (cacheData.cache.body) {
815
+ * console.log('Cache hit:', cacheData.cache.body);
816
+ * } else {
817
+ * console.log('Cache miss');
818
+ * }
554
819
  */
555
820
  static async read(idHash, syncedLater) {
556
821
 
@@ -584,17 +849,40 @@ class CacheData {
584
849
  };
585
850
 
586
851
  /**
852
+ * Write data to cache storage (DynamoDB and potentially S3). Handles encryption
853
+ * for private data, generates ETags and headers, and automatically routes large
854
+ * items to S3 while keeping a pointer in DynamoDB.
855
+ *
856
+ * Items larger than DynamoDbMaxCacheSize_kb are stored in S3 with a reference
857
+ * in DynamoDB. Smaller items are stored entirely in DynamoDB for faster access.
587
858
  *
588
- * @param {string} idHash ID of data to write
589
- * @param {number} syncedNow
590
- * @param {string} body
591
- * @param {Object} headers
592
- * @param {string} host
593
- * @param {string} path
594
- * @param {number} expires
595
- * @param {number} statusCode
596
- * @param {boolean} encrypt
597
- * @returns {Promise<CacheDataFormat>}
859
+ * @param {string} idHash The unique identifier hash for the cache entry
860
+ * @param {number} syncedNow Current timestamp in seconds
861
+ * @param {string} body The content body to cache
862
+ * @param {Object} headers HTTP headers object
863
+ * @param {string} host Host identifier for logging
864
+ * @param {string} path Path identifier for logging
865
+ * @param {number} expires Expiration timestamp in seconds
866
+ * @param {string|number} statusCode HTTP status code
867
+ * @param {boolean} [encrypt=true] Whether to encrypt the body (true for private, false for public)
868
+ * @returns {Promise<CacheDataFormat>} Formatted cache data that was written
869
+ * @example
870
+ * const now = Math.floor(Date.now() / 1000);
871
+ * const expires = now + 3600; // 1 hour from now
872
+ * const body = JSON.stringify({ data: 'value' });
873
+ * const headers = { 'content-type': 'application/json' };
874
+ *
875
+ * const cacheData = await CacheData.write(
876
+ * 'abc123def456',
877
+ * now,
878
+ * body,
879
+ * headers,
880
+ * 'api.example.com',
881
+ * '/api/data',
882
+ * expires,
883
+ * '200',
884
+ * true
885
+ * );
598
886
  */
599
887
  static async write (idHash, syncedNow, body, headers, host, path, expires, statusCode, encrypt = true) {
600
888
 
@@ -727,9 +1015,14 @@ class CacheData {
727
1015
  */
728
1016
 
729
1017
  /**
730
- * Returns the type of secureDataKey
1018
+ * Get the type of the secure data key being used for encryption.
1019
+ * Returns a string indicating whether the key is a Buffer, string, or
1020
+ * CachedParameterSecret object.
731
1021
  *
732
- * @returns {string} 'buffer', 'string', 'CachedParameterSecret'
1022
+ * @returns {string} One of: 'buffer', 'string', or 'CachedParameterSecret'
1023
+ * @example
1024
+ * const keyType = CacheData.getSecureDataKeyType();
1025
+ * console.log(`Encryption key type: ${keyType}`);
733
1026
  */
734
1027
  static getSecureDataKeyType() {
735
1028
  // look at type of parameters.secureDataKey as it can be a string, Buffer, or object.
@@ -740,9 +1033,16 @@ class CacheData {
740
1033
  }
741
1034
 
742
1035
  /**
743
- * Obtain the secureDataKey as a Buffer for encryption/decryption.
744
- *
745
- * @returns {Buffer|null} The Data key as a buffer in the format specified by CacheData.CRYPT_ENCODING
1036
+ * Obtain the secure data key as a Buffer for encryption/decryption operations.
1037
+ * Handles different key storage formats (Buffer, string, CachedParameterSecret)
1038
+ * and converts them to a Buffer in the format specified by CRYPT_ENCODING.
1039
+ *
1040
+ * @returns {Buffer|null} The encryption key as a Buffer, or null if key cannot be retrieved
1041
+ * @example
1042
+ * const keyBuffer = CacheData.getSecureDataKey();
1043
+ * if (keyBuffer) {
1044
+ * // Use keyBuffer for encryption/decryption
1045
+ * }
746
1046
  */
747
1047
  static getSecureDataKey() {
748
1048
 
@@ -776,9 +1076,23 @@ class CacheData {
776
1076
  };
777
1077
 
778
1078
  /**
1079
+ * Internal method to encrypt data classified as private. Uses the configured
1080
+ * encryption algorithm and secure data key to encrypt the provided text.
1081
+ *
1082
+ * This method generates a random initialization vector (IV) for each encryption
1083
+ * operation to ensure unique ciphertext even for identical plaintext. The IV
1084
+ * is returned along with the encrypted data.
1085
+ *
1086
+ * Note: null values are substituted with "{{{null}}}" before encryption to
1087
+ * handle the edge case of null data.
779
1088
  *
780
- * @param {string} text Data to encrypt
781
- * @returns {string} Encrypted data
1089
+ * @private
1090
+ * @param {string|null} text Data to encrypt
1091
+ * @returns {{iv: string, encryptedData: string}} Object containing the IV and encrypted data, both as hex strings
1092
+ * @example
1093
+ * // Called internally by CacheData.write()
1094
+ * const encrypted = CacheData._encrypt('sensitive data');
1095
+ * console.log(encrypted); // { iv: 'abc123...', encryptedData: 'def456...' }
782
1096
  */
783
1097
  static _encrypt (text) {
784
1098
 
@@ -799,9 +1113,27 @@ class CacheData {
799
1113
  };
800
1114
 
801
1115
  /**
1116
+ * Internal method to decrypt data classified as private. Uses the configured
1117
+ * encryption algorithm and secure data key to decrypt the provided encrypted data.
1118
+ *
1119
+ * This method requires both the initialization vector (IV) and the encrypted data
1120
+ * that were produced by the _encrypt() method. It reverses the encryption process
1121
+ * to recover the original plaintext.
1122
+ *
1123
+ * Note: The special value "{{{null}}}" is converted back to null after decryption
1124
+ * to handle the edge case of null data.
802
1125
  *
803
- * @param {string} data Data to decrypt
804
- * @returns {string} Decrypted data
1126
+ * @private
1127
+ * @param {{iv: string, encryptedData: string, plainEncoding?: string, cryptEncoding?: string}} data Object containing encrypted data and IV
1128
+ * @param {string} data.iv The initialization vector as a hex string
1129
+ * @param {string} data.encryptedData The encrypted data as a hex string
1130
+ * @param {string} [data.plainEncoding] Optional plain text encoding (defaults to PLAIN_ENCODING)
1131
+ * @param {string} [data.cryptEncoding] Optional cipher text encoding (defaults to CRYPT_ENCODING)
1132
+ * @returns {string|null} Decrypted data as plaintext, or null if original data was null
1133
+ * @example
1134
+ * // Called internally by CacheData._process()
1135
+ * const decrypted = CacheData._decrypt({ iv: 'abc123...', encryptedData: 'def456...' });
1136
+ * console.log(decrypted); // 'sensitive data'
805
1137
  */
806
1138
  static _decrypt (data) {
807
1139
 
@@ -826,18 +1158,19 @@ class CacheData {
826
1158
 
827
1159
  // utility functions
828
1160
  /**
829
- * Generate an eTag hash
1161
+ * Generate an ETag hash for cache validation. Creates a unique hash by combining
1162
+ * the cache ID hash with the content. This is used for HTTP ETag headers to
1163
+ * enable conditional requests and cache validation.
1164
+ *
1165
+ * The ETag is a 10-character SHA-1 hash slice, which is sufficient for cache
1166
+ * validation at a specific endpoint without needing global uniqueness.
830
1167
  *
831
- * This is very basic as there is no specification for doing this.
832
- * All an eTag needs to be is a unique hash for a particular request.
833
- * We already have a unique ID for the request, so it's not like we
834
- * need to make sure the content matches (or does not match) any content
835
- * throughout the rest of the world. We are just doing a quick check
836
- * at this exact endpoint. So we pair the idHash (this exact endpoint)
837
- * with it's content.
838
- * @param {string} idHash The id of the endpoint
839
- * @param {string} content The string to generate an eTag for
840
- * @returns {string} 10 character ETag hash
1168
+ * @param {string} idHash The unique identifier hash for the cache entry
1169
+ * @param {string} content The content body to include in the ETag calculation
1170
+ * @returns {string} A 10-character ETag hash
1171
+ * @example
1172
+ * const etag = CacheData.generateEtag('abc123', '{"data":"value"}');
1173
+ * console.log(`ETag: ${etag}`); // e.g., "a1b2c3d4e5"
841
1174
  */
842
1175
  static generateEtag (idHash, content) {
843
1176
  const hasher = crypto.createHash('sha1');
@@ -847,16 +1180,22 @@ class CacheData {
847
1180
  };
848
1181
 
849
1182
  /**
850
- * Generate an internet formatted date such as those used in headers.
1183
+ * Generate an internet-formatted date string for use in HTTP headers.
1184
+ * Converts a Unix timestamp to the standard HTTP date format.
851
1185
  *
852
- * Example: "Wed, 28 Jul 2021 12:24:11 GMT"
1186
+ * Example output: "Wed, 28 Jul 2021 12:24:11 GMT"
1187
+ *
1188
+ * @param {number} timestamp Unix timestamp (in seconds by default, or milliseconds if inMilliSeconds is true)
1189
+ * @param {boolean} [inMilliSeconds=false] Set to true if timestamp is in milliseconds
1190
+ * @returns {string} Internet-formatted date string (e.g., "Wed, 28 Jul 2021 12:24:11 GMT")
1191
+ * @example
1192
+ * const now = Math.floor(Date.now() / 1000);
1193
+ * const dateStr = CacheData.generateInternetFormattedDate(now);
1194
+ * console.log(dateStr); // "Mon, 26 Jan 2026 15:30:45 GMT"
853
1195
  *
854
- * The timestamp passed is expected to be in seconds. If it is in
855
- * milliseconds then inMilliSeconds parameter needs to be set to
856
- * true.
857
- * @param {number} timestamp If in milliseconds, inMilliseconds parameter MUST be set to true
858
- * @param {boolean} inMilliSeconds Set to true if timestamp passed is in milliseconds. Default is false
859
- * @returns {string} An internet formatted date such as Wed, 28 Jul 2021 12:24:11 GMT
1196
+ * @example
1197
+ * const nowMs = Date.now();
1198
+ * const dateStr = CacheData.generateInternetFormattedDate(nowMs, true);
860
1199
  */
861
1200
  static generateInternetFormattedDate (timestamp, inMilliSeconds = false) {
862
1201
 
@@ -868,12 +1207,18 @@ class CacheData {
868
1207
  };
869
1208
 
870
1209
  /**
871
- * Returns an object with lowercase keys. Note that if after
872
- * lowercasing the keys there is a collision one will be
873
- * over-written.
874
- * Can be used for headers, response, or more.
875
- * @param {Object} objectWithKeys
876
- * @returns {Object} Same object but with lowercase keys
1210
+ * Convert all keys in an object to lowercase. Useful for normalizing HTTP headers
1211
+ * or other key-value pairs for case-insensitive comparison.
1212
+ *
1213
+ * Note: If lowercasing keys causes a collision (e.g., "Content-Type" and "content-type"),
1214
+ * one value will overwrite the other.
1215
+ *
1216
+ * @param {Object} objectWithKeys Object whose keys should be lowercased
1217
+ * @returns {Object} New object with all keys converted to lowercase
1218
+ * @example
1219
+ * const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' };
1220
+ * const normalized = CacheData.lowerCaseKeys(headers);
1221
+ * console.log(normalized); // { 'content-type': 'application/json', 'cache-control': 'no-cache' }
877
1222
  */
878
1223
  static lowerCaseKeys (objectWithKeys) {
879
1224
  let objectWithLowerCaseKeys = {};
@@ -888,12 +1233,24 @@ class CacheData {
888
1233
  }
889
1234
 
890
1235
  /**
891
- * Calculate the number of Kilobytes in memory a String takes up.
892
- * This function first calculates the number of bytes in the String using
893
- * Buffer.byteLength() and then converts it to KB = (bytes / 1024)
894
- * @param {string} aString A string to calculate on
895
- * @param {string} encode What character encoding should be used? Default is "utf8"
896
- * @returns {number} String size in estimated KB
1236
+ * Calculate the size of a string in kilobytes. Uses Buffer.byteLength() to
1237
+ * determine the byte size based on the specified character encoding, then
1238
+ * converts to KB (bytes / 1024).
1239
+ *
1240
+ * The result is rounded to 3 decimal places for precision (0.001 KB = 1 byte).
1241
+ *
1242
+ * @param {string} aString The string to measure
1243
+ * @param {string} [encode='utf8'] Character encoding to use for byte calculation
1244
+ * @returns {number} String size in kilobytes, rounded to 3 decimal places
1245
+ * @example
1246
+ * const text = 'Hello, World!';
1247
+ * const sizeKB = CacheData.calculateKBytes(text);
1248
+ * console.log(`Size: ${sizeKB} KB`);
1249
+ *
1250
+ * @example
1251
+ * const largeText = 'x'.repeat(10000);
1252
+ * const sizeKB = CacheData.calculateKBytes(largeText);
1253
+ * console.log(`Size: ${sizeKB} KB`); // ~9.766 KB
897
1254
  */
898
1255
  static calculateKBytes ( aString, encode = CacheData.PLAIN_ENCODING ) {
899
1256
  let kbytes = 0;
@@ -910,26 +1267,32 @@ class CacheData {
910
1267
  };
911
1268
 
912
1269
  /**
913
- * We can set times and expirations on intervals, such as every
914
- * 15 seconds (mm:00, mm:15, mm:30, mm:45), every half hour
915
- * (hh:00:00, hh:30:00), every hour (T00:00:00, T01:00:00), etc.
916
- * In some cases such as every 2 hours, the interval is calculated
917
- * from midnight in the timezone specified in timeZoneForInterval
918
- * Spans of days (such as every two days (48 hours) or every three
919
- * days (72 hours) are calculated from midnight of the UNIX epoch
1270
+ * Calculate the interval-based expiration time for cache entries. This method
1271
+ * rounds up to the next interval boundary based on the specified interval duration.
1272
+ *
1273
+ * Intervals can be set for various durations such as every 15 seconds (mm:00, mm:15,
1274
+ * mm:30, mm:45), every hour (T00:00:00, T01:00:00), etc. For intervals of 2+ hours,
1275
+ * calculations are based on midnight in the configured timeZoneForInterval. For
1276
+ * multi-day intervals (48+ hours), calculations are based on the UNIX epoch
920
1277
  * (January 1, 1970).
921
1278
  *
922
- * When a timezone is set in timeZoneForInterval, then there is
923
- * a slight adjustment made so that the interval lines up with
924
- * midnight of the "local" time. For example, if an organization
925
- * is primarily located in the Central Time Zone (or their
926
- * nightly batch jobs occur at GMT-05:00) then timeZoneForInterval
927
- * may be set to "America/Chicago" so that midnight in
928
- * "America/Chicago" may be used for calculations. That keeps
929
- * every 4 hours on hours 00, 04, 08, 12, 16, etc.
930
- * @param {number} intervalInSeconds
931
- * @param {number} timestampInSeconds
932
- * @returns {number} Next interval in seconds
1279
+ * When a timezone is configured, the interval aligns with local midnight rather
1280
+ * than UTC midnight. For example, with timeZoneForInterval set to "America/Chicago",
1281
+ * a 4-hour interval will align to hours 00, 04, 08, 12, 16, 20 in Chicago time.
1282
+ *
1283
+ * @param {number} intervalInSeconds The interval duration in seconds (e.g., 3600 for 1 hour)
1284
+ * @param {number} [timestampInSeconds=0] The timestamp to calculate from (0 = use current time)
1285
+ * @returns {number} The next interval boundary timestamp in seconds
1286
+ * @example
1287
+ * // Calculate next 15-minute interval
1288
+ * const now = Math.floor(Date.now() / 1000);
1289
+ * const nextInterval = CacheData.nextIntervalInSeconds(900, now); // 900 = 15 minutes
1290
+ * console.log(new Date(nextInterval * 1000)); // Next :00, :15, :30, or :45
1291
+ *
1292
+ * @example
1293
+ * // Calculate next hourly interval (uses current time)
1294
+ * const nextHour = CacheData.nextIntervalInSeconds(3600);
1295
+ * console.log(new Date(nextHour * 1000)); // Next hour boundary
933
1296
  */
934
1297
  static nextIntervalInSeconds(intervalInSeconds, timestampInSeconds = 0 ) {
935
1298
 
@@ -961,9 +1324,19 @@ class CacheData {
961
1324
  };
962
1325
 
963
1326
  /**
964
- * If no parameter is passed, Date.now() is used.
965
- * @param {number} timestampInMillseconds The timestamp in milliseconds to convert to seconds
966
- * @returns {number} The timestamp in seconds
1327
+ * Convert a Unix timestamp from milliseconds to seconds. If no parameter is
1328
+ * provided, uses the current time (Date.now()).
1329
+ *
1330
+ * @param {number} [timestampInMillseconds=0] Timestamp in milliseconds (0 = use current time)
1331
+ * @returns {number} Timestamp in seconds (rounded up using Math.ceil)
1332
+ * @example
1333
+ * const nowMs = Date.now();
1334
+ * const nowSec = CacheData.convertTimestampFromMilliToSeconds(nowMs);
1335
+ * console.log(`${nowMs}ms = ${nowSec}s`);
1336
+ *
1337
+ * @example
1338
+ * // Get current time in seconds
1339
+ * const nowSec = CacheData.convertTimestampFromMilliToSeconds();
967
1340
  */
968
1341
  static convertTimestampFromMilliToSeconds (timestampInMillseconds = 0) {
969
1342
  if (timestampInMillseconds === 0) { timestampInMillseconds = Date.now().getTime(); }
@@ -971,9 +1344,19 @@ class CacheData {
971
1344
  };
972
1345
 
973
1346
  /**
974
- * If no parameter is passed, Date.now() is used.
975
- * @param {number} timestampInSeconds
1347
+ * Convert a Unix timestamp from seconds to milliseconds. If no parameter is
1348
+ * provided, uses the current time (Date.now()).
1349
+ *
1350
+ * @param {number} [timestampInSeconds=0] Timestamp in seconds (0 = use current time)
976
1351
  * @returns {number} Timestamp in milliseconds
1352
+ * @example
1353
+ * const nowSec = Math.floor(Date.now() / 1000);
1354
+ * const nowMs = CacheData.convertTimestampFromSecondsToMilli(nowSec);
1355
+ * console.log(`${nowSec}s = ${nowMs}ms`);
1356
+ *
1357
+ * @example
1358
+ * // Get current time in milliseconds
1359
+ * const nowMs = CacheData.convertTimestampFromSecondsToMilli();
977
1360
  */
978
1361
  static convertTimestampFromSecondsToMilli (timestampInSeconds = 0) {
979
1362
  let timestampInMilli = 0;
@@ -990,12 +1373,12 @@ class CacheData {
990
1373
  };
991
1374
 
992
1375
  /**
993
- * The Cache object handles reads and writes from the cache.
994
- * It also acts as a proxy between the app and CacheData which is a private class.
995
- * This is the actual data object our application can work with and is returned
996
- * from CachableDataAccess.
1376
+ * The Cache object handles the settings for the cache system
997
1377
  *
998
- * Before using it must be initialized
1378
+ * Before using it must be initialized.
1379
+ *
1380
+ * Many settings can be set through Environment variables or by
1381
+ * passing parameters to Cache.init():
999
1382
  *
1000
1383
  * Cache.init({parameters});
1001
1384
  *
@@ -1009,6 +1392,22 @@ class CacheData {
1009
1392
  * conn,
1010
1393
  * daoQuery
1011
1394
  * );
1395
+ *
1396
+ * @example
1397
+ * // Initialize the cache system
1398
+ * Cache.init({
1399
+ * dynamoDbTable: 'my-cache-table',
1400
+ * s3Bucket: 'my-cache-bucket',
1401
+ * idHashAlgorithm: 'sha256'
1402
+ * });
1403
+ *
1404
+ * // Create a cache instance with connection and profile
1405
+ * const connection = { host: 'api.example.com', path: '/data' };
1406
+ * const cacheProfile = {
1407
+ * defaultExpirationInSeconds: 300,
1408
+ * encrypt: true
1409
+ * };
1410
+ * const cacheInstance = new Cache(connection, cacheProfile);
1012
1411
  */
1013
1412
  class Cache {
1014
1413
 
@@ -1021,6 +1420,7 @@ class Cache {
1021
1420
  static STATUS_NO_CACHE = "original";
1022
1421
  static STATUS_EXPIRED = "original:cache-expired";
1023
1422
  static STATUS_CACHE_SAME = "cache:original-same-as-cache";
1423
+ static STATUS_CACHE_IN_MEM = "cache:memory";
1024
1424
  static STATUS_CACHE = "cache";
1025
1425
  static STATUS_CACHE_ERROR = "error:cache"
1026
1426
  static STATUS_ORIGINAL_NOT_MODIFIED = "cache:original-not-modified";
@@ -1029,6 +1429,8 @@ class Cache {
1029
1429
 
1030
1430
  static #idHashAlgorithm = null;
1031
1431
  static #useToolsHash = null; // gets set in Cache.init()
1432
+ static #useInMemoryCache = false;
1433
+ static #inMemoryCache = null;
1032
1434
 
1033
1435
  #syncedNowTimestampInSeconds = 0; // consistent time base for calculations
1034
1436
  #syncedLaterTimestampInSeconds = 0; // default expiration if not adjusted
@@ -1128,6 +1530,7 @@ class Cache {
1128
1530
  * @param {number} parameters.DynamoDbMaxCacheSize_kb Can also be set with environment variable CACHE_DATA_DYNAMO_DB_MAX_CACHE_SIZE_KB
1129
1531
  * @param {number} parameters.purgeExpiredCacheEntriesAfterXHours Can also be set with environment variable CACHE_DATA_PURGE_EXPIRED_CACHE_ENTRIES_AFTER_X_HRS
1130
1532
  * @param {string} parameters.timeZoneForInterval Can also be set with environment variable CACHE_DATA_TIME_ZONE_FOR_INTERVAL
1533
+ * @throws {Error} If parameters is not an object or is null
1131
1534
  */
1132
1535
  static init(parameters) {
1133
1536
  // check if parameters is an object
@@ -1141,13 +1544,35 @@ class Cache {
1141
1544
  this.#useToolsHash = ( "useToolsHash" in parameters ) ? Cache.bool(parameters.useToolsHash) :
1142
1545
  ("CACHE_DATA_USE_TOOLS_HASH" in process.env ? Cache.bool(process.env.CACHE_DATA_USE_TOOLS_HASH_METHOD) : false);
1143
1546
 
1547
+ // Initialize in-memory cache feature flag
1548
+ this.#useInMemoryCache = parameters.useInMemoryCache ||
1549
+ (process.env.CACHE_USE_IN_MEMORY === 'true') ||
1550
+ false;
1551
+
1552
+ // Initialize InMemoryCache if enabled
1553
+ if (this.#useInMemoryCache) {
1554
+ const InMemoryCache = require('./utils/InMemoryCache.js');
1555
+ this.#inMemoryCache = new InMemoryCache({
1556
+ maxEntries: parameters.inMemoryCacheMaxEntries,
1557
+ entriesPerGB: parameters.inMemoryCacheEntriesPerGB,
1558
+ defaultMaxEntries: parameters.inMemoryCacheDefaultMaxEntries
1559
+ });
1560
+ tools.DebugAndLog.debug('In-memory cache initialized');
1561
+ }
1562
+
1144
1563
  // Let CacheData handle the rest of the initialization
1145
1564
  CacheData.init(parameters);
1146
1565
  };
1147
1566
 
1148
1567
  /**
1149
- * Returns all the common information such as hash algorithm, s3 bucket,
1150
- * dynamo db location, etc.
1568
+ * Get comprehensive configuration information about the Cache system.
1569
+ * Returns an object containing all configuration parameters including
1570
+ * the ID hash algorithm, DynamoDB table, S3 bucket, encryption settings,
1571
+ * size limits, timezone information, and in-memory cache status.
1572
+ *
1573
+ * This method combines Cache-specific settings with CacheData configuration
1574
+ * to provide a complete view of the cache system configuration.
1575
+ *
1151
1576
  * @returns {{
1152
1577
  * idHashAlgorithm: string,
1153
1578
  * dynamoDbTable: string,
@@ -1157,16 +1582,46 @@ class Cache {
1157
1582
  * DynamoDbMaxCacheSize_kb: number,
1158
1583
  * purgeExpiredCacheEntriesAfterXHours: number,
1159
1584
  * timeZoneForInterval: string,
1160
- * offsetInMinutes: number
1161
- * }}
1585
+ * offsetInMinutes: number,
1586
+ * useInMemoryCache: boolean,
1587
+ * inMemoryCache?: Object
1588
+ * }} Configuration information object
1589
+ * @example
1590
+ * const info = Cache.info();
1591
+ * console.log(`Hash algorithm: ${info.idHashAlgorithm}`);
1592
+ * console.log(`DynamoDB table: ${info.dynamoDbTable}`);
1593
+ * console.log(`S3 bucket: ${info.s3Bucket.bucket}`);
1594
+ * console.log(`In-memory cache enabled: ${info.useInMemoryCache}`);
1162
1595
  */
1163
1596
  static info() {
1164
- return Object.assign({ idHashAlgorithm: this.#idHashAlgorithm }, CacheData.info()); // merge into 1 object and return
1597
+ const info = Object.assign({ idHashAlgorithm: this.#idHashAlgorithm }, CacheData.info()); // merge into 1 object
1598
+
1599
+ // Add in-memory cache info
1600
+ info.useInMemoryCache = this.#useInMemoryCache;
1601
+ if (this.#useInMemoryCache && this.#inMemoryCache !== null) {
1602
+ info.inMemoryCache = this.#inMemoryCache.info();
1603
+ }
1604
+
1605
+ return info;
1165
1606
  };
1166
1607
 
1167
1608
  /**
1609
+ * Test the interval calculation functionality by computing next intervals
1610
+ * for various durations. This method is useful for debugging and verifying
1611
+ * that interval-based cache expiration is working correctly with the
1612
+ * configured timezone.
1168
1613
  *
1169
- * @returns {object} Test data of nextIntervalInSeconds method
1614
+ * Returns an object containing the current cache configuration and test
1615
+ * results showing the next interval boundary for various durations from
1616
+ * 15 seconds to 120 hours (5 days).
1617
+ *
1618
+ * @returns {{info: Object, tests: Object}} Object containing cache info and test results with next interval timestamps
1619
+ * @example
1620
+ * const testResults = Cache.testInterval();
1621
+ * console.log('Current time:', testResults.tests.start);
1622
+ * console.log('Next 15-second interval:', testResults.tests.sec_15);
1623
+ * console.log('Next hourly interval:', testResults.tests.min_60);
1624
+ * console.log('Next daily interval:', testResults.tests.hrs_24);
1170
1625
  */
1171
1626
  static testInterval () {
1172
1627
  let ts = CacheData.convertTimestampFromMilliToSeconds(Date.now());
@@ -1199,14 +1654,27 @@ class Cache {
1199
1654
  };
1200
1655
 
1201
1656
  /**
1202
- * When testing a variable as a boolean, 0, false, and null are false,
1203
- * but the string "false" is true. Since we can be dealing with JSON data,
1204
- * query parameters, and strings coded as "false" we want to include the
1205
- * string "false" as false.
1206
- * This function only adds "false" to the list of values already considered
1207
- * false by JavaScript
1208
- * @param {*} value A value you want to turn to boolean
1209
- * @returns {boolean} Does the value equal false according to JavaScript evaluation rules?
1657
+ * Convert a value to boolean with special handling for the string "false".
1658
+ *
1659
+ * JavaScript's Boolean() function treats any non-empty string as true, including
1660
+ * the string "false". This method adds special handling for the string "false"
1661
+ * (case-insensitive) to return false, which is useful when dealing with JSON data,
1662
+ * query parameters, or configuration strings.
1663
+ *
1664
+ * All other values are evaluated using JavaScript's standard Boolean() conversion.
1665
+ *
1666
+ * @param {*} value A value to convert to boolean
1667
+ * @returns {boolean} The boolean representation of the value, with "false" string treated as false
1668
+ * @example
1669
+ * Cache.bool(true); // true
1670
+ * Cache.bool(false); // false
1671
+ * Cache.bool("true"); // true
1672
+ * Cache.bool("false"); // false (special handling)
1673
+ * Cache.bool("FALSE"); // false (case-insensitive)
1674
+ * Cache.bool(1); // true
1675
+ * Cache.bool(0); // false
1676
+ * Cache.bool(null); // false
1677
+ * Cache.bool(""); // false
1210
1678
  */
1211
1679
  static bool (value) {
1212
1680
 
@@ -1217,33 +1685,62 @@ class Cache {
1217
1685
  };
1218
1686
 
1219
1687
  /**
1220
- * Generate an etag based on an id and content body
1221
- * @param {string} idHash Hashed content identifier. For web pages this a hash of host, path, query, etc.
1222
- * @param {string} content Content. usually body and static headers
1223
- * @returns {string} An etag specific to that content and id
1688
+ * Generate an ETag hash for cache validation. Creates a unique hash by combining
1689
+ * the cache ID hash with the content body. This is used for HTTP ETag headers to
1690
+ * enable conditional requests and cache validation.
1691
+ *
1692
+ * This is a convenience wrapper around CacheData.generateEtag().
1693
+ *
1694
+ * @param {string} idHash The unique identifier hash for the cache entry
1695
+ * @param {string} content The content body to include in the ETag calculation
1696
+ * @returns {string} A 10-character ETag hash
1697
+ * @example
1698
+ * const etag = Cache.generateEtag('abc123', '{"data":"value"}');
1699
+ * console.log(`ETag: ${etag}`); // e.g., "a1b2c3d4e5"
1224
1700
  */
1225
1701
  static generateEtag(idHash, content) {
1226
1702
  return CacheData.generateEtag(idHash, content);
1227
1703
  };
1228
1704
 
1229
1705
  /**
1230
- * To make submitted key comparison easier against a standard set of keys,
1231
- * lowercase keys in the object.
1232
- * @param {object} objectWithKeys Object we want to lowercase keys on
1233
- * @returns {object} Object with lowercase keys
1706
+ * Convert all keys in an object to lowercase. Useful for normalizing HTTP headers
1707
+ * or other key-value pairs for case-insensitive comparison.
1708
+ *
1709
+ * This is a convenience wrapper around CacheData.lowerCaseKeys().
1710
+ *
1711
+ * Note: If lowercasing keys causes a collision (e.g., "Content-Type" and "content-type"),
1712
+ * one value will overwrite the other.
1713
+ *
1714
+ * @param {Object} objectWithKeys Object whose keys should be lowercased
1715
+ * @returns {Object} New object with all keys converted to lowercase
1716
+ * @example
1717
+ * const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' };
1718
+ * const normalized = Cache.lowerCaseKeys(headers);
1719
+ * console.log(normalized); // { 'content-type': 'application/json', 'cache-control': 'no-cache' }
1234
1720
  */
1235
1721
  static lowerCaseKeys(objectWithKeys) {
1236
1722
  return CacheData.lowerCaseKeys(objectWithKeys);
1237
1723
  };
1238
1724
 
1239
1725
  /**
1240
- * Generate an internet formatted date such as those used in headers.
1726
+ * Generate an internet-formatted date string for use in HTTP headers.
1727
+ * Converts a Unix timestamp to the standard HTTP date format.
1241
1728
  *
1242
- * Example: "Wed, 28 Jul 2021 12:24:11 GMT"
1729
+ * This is a convenience wrapper around CacheData.generateInternetFormattedDate().
1730
+ *
1731
+ * Example output: "Wed, 28 Jul 2021 12:24:11 GMT"
1243
1732
  *
1244
- * @param {number} timestamp Unix timestamp in seconds or milliseconds.
1245
- * @param {boolean} inMilliseconds Set to true if timestamp is in milliseconds. Default is false.
1246
- * @returns {string} Formatted date/time such as Wed, 28 Jul 2021 12:24:11 GMT
1733
+ * @param {number} timestamp Unix timestamp (in seconds by default, or milliseconds if inMilliseconds is true)
1734
+ * @param {boolean} [inMilliseconds=false] Set to true if timestamp is in milliseconds
1735
+ * @returns {string} Internet-formatted date string (e.g., "Wed, 28 Jul 2021 12:24:11 GMT")
1736
+ * @example
1737
+ * const now = Math.floor(Date.now() / 1000);
1738
+ * const dateStr = Cache.generateInternetFormattedDate(now);
1739
+ * console.log(dateStr); // "Mon, 26 Jan 2026 15:30:45 GMT"
1740
+ *
1741
+ * @example
1742
+ * const nowMs = Date.now();
1743
+ * const dateStr = Cache.generateInternetFormattedDate(nowMs, true);
1247
1744
  */
1248
1745
  static generateInternetFormattedDate(timestamp, inMilliseconds = false) {
1249
1746
  return CacheData.generateInternetFormattedDate(timestamp, inMilliseconds);
@@ -1289,6 +1786,24 @@ class Cache {
1289
1786
  *
1290
1787
  * @param {Object|Array|string} idObject Object, Array, or string to hash. Object may contain a single value with a text string, or complex http request broken down into parts
1291
1788
  * @returns {string} A hash representing the object (Algorithm used is set in Cache object constructor)
1789
+ * @example
1790
+ * // Hash a simple object
1791
+ * const connection = { host: 'api.example.com', path: '/users' };
1792
+ * const hash = Cache.generateIdHash(connection);
1793
+ * console.log(hash); // "a1b2c3d4e5f6..."
1794
+ *
1795
+ * @example
1796
+ * // Hash a complex request object
1797
+ * const requestObj = {
1798
+ * query: { type: 'user', id: '123' },
1799
+ * connection: { host: 'api.example.com', path: '/data' },
1800
+ * cachePolicy: { ttl: 300 }
1801
+ * };
1802
+ * const hash = Cache.generateIdHash(requestObj);
1803
+ *
1804
+ * @example
1805
+ * // Hash a simple string ID
1806
+ * const hash = Cache.generateIdHash('MYID-03-88493');
1292
1807
  */
1293
1808
  static generateIdHash(idObject) {
1294
1809
 
@@ -1346,6 +1861,20 @@ class Cache {
1346
1861
  * @param {Array|string} identifierArrayOrString An array we wish to join together as an id. (also could be a string which we won't touch)
1347
1862
  * @param {string} glue The glue or delimiter to place between the array elements once it is in string form
1348
1863
  * @returns {string} The array in string form delimited by the glue.
1864
+ * @example
1865
+ * // Join array elements with default delimiter
1866
+ * const id = Cache.multipartId(['user', '123', 'profile']);
1867
+ * console.log(id); // "user-123-profile"
1868
+ *
1869
+ * @example
1870
+ * // Use custom delimiter
1871
+ * const id = Cache.multipartId(['api', 'v2', 'users'], '/');
1872
+ * console.log(id); // "api/v2/users"
1873
+ *
1874
+ * @example
1875
+ * // Pass a string (returns unchanged)
1876
+ * const id = Cache.multipartId('already-a-string');
1877
+ * console.log(id); // "already-a-string"
1349
1878
  */
1350
1879
  static multipartId (identifierArrayOrString, glue = "-") {
1351
1880
  let id = null;
@@ -1356,11 +1885,22 @@ class Cache {
1356
1885
  };
1357
1886
 
1358
1887
  /**
1359
- * Uses Date.parse() but returns seconds instead of milliseconds.
1360
- * Takes a date string (such as "2011-10-10T14:48:00") and returns the number of seconds since January 1, 1970, 00:00:00 UTC
1361
- * @param {string} date
1362
- * @returns {number} The date in seconds since January 1, 1970, 00:00:00 UTC
1363
- */
1888
+ * Convert a date string to a Unix timestamp in seconds. Uses Date.parse() to
1889
+ * parse the date string and converts the result from milliseconds to seconds.
1890
+ *
1891
+ * This method is useful for converting HTTP date headers (like "Expires" or
1892
+ * "Last-Modified") into Unix timestamps for comparison and calculation.
1893
+ *
1894
+ * @param {string} date Date string to parse (e.g., "2011-10-10T14:48:00" or "Wed, 28 Jul 2021 12:24:11 GMT")
1895
+ * @returns {number} The date in seconds since January 1, 1970, 00:00:00 UTC, or 0 if parsing fails
1896
+ * @example
1897
+ * const timestamp = Cache.parseToSeconds("2021-07-28T12:24:11Z");
1898
+ * console.log(timestamp); // 1627476251
1899
+ *
1900
+ * @example
1901
+ * const timestamp = Cache.parseToSeconds("Wed, 28 Jul 2021 12:24:11 GMT");
1902
+ * console.log(timestamp); // 1627476251
1903
+ */
1364
1904
  static parseToSeconds(date) {
1365
1905
  let timestampInSeconds = 0;
1366
1906
  try {
@@ -1392,6 +1932,16 @@ class Cache {
1392
1932
  * @param {number} intervalInSeconds
1393
1933
  * @param {number} timestampInSeconds
1394
1934
  * @returns {number} Next interval in seconds
1935
+ * @example
1936
+ * // Calculate next 15-minute interval
1937
+ * const now = Math.floor(Date.now() / 1000);
1938
+ * const next15Min = Cache.nextIntervalInSeconds(15 * 60, now);
1939
+ * console.log(new Date(next15Min * 1000)); // Next 15-minute boundary
1940
+ *
1941
+ * @example
1942
+ * // Calculate next hourly interval
1943
+ * const nextHour = Cache.nextIntervalInSeconds(3600);
1944
+ * console.log(new Date(nextHour * 1000)); // Next hour boundary
1395
1945
  */
1396
1946
  static nextIntervalInSeconds(intervalInSeconds, timestampInSeconds = 0) {
1397
1947
  return CacheData.nextIntervalInSeconds(intervalInSeconds, timestampInSeconds);
@@ -1399,23 +1949,48 @@ class Cache {
1399
1949
 
1400
1950
 
1401
1951
  /**
1402
- * Calculate the number of Kilobytes in memory a String takes up.
1403
- * This function first calculates the number of bytes in the String using
1404
- * Buffer.byteLength() and then converts it to KB = (bytes / 1024)
1405
- * @param {string} aString A string to calculate on
1406
- * @param {string} encode What character encoding should be used? Default is "utf8"
1407
- * @returns String size in estimated KB
1952
+ * Calculate the size of a string in kilobytes. Uses Buffer.byteLength() to
1953
+ * determine the byte size based on the specified character encoding, then
1954
+ * converts to KB (bytes / 1024).
1955
+ *
1956
+ * This is a convenience wrapper around CacheData.calculateKBytes().
1957
+ *
1958
+ * The result is rounded to 3 decimal places for precision (0.001 KB = 1 byte).
1959
+ *
1960
+ * @param {string} aString The string to measure
1961
+ * @param {string} [encode='utf8'] Character encoding to use for byte calculation
1962
+ * @returns {number} String size in kilobytes, rounded to 3 decimal places
1963
+ * @example
1964
+ * const text = 'Hello, World!';
1965
+ * const sizeKB = Cache.calculateKBytes(text);
1966
+ * console.log(`Size: ${sizeKB} KB`);
1967
+ *
1968
+ * @example
1969
+ * const largeText = 'x'.repeat(10000);
1970
+ * const sizeKB = Cache.calculateKBytes(largeText);
1971
+ * console.log(`Size: ${sizeKB} KB`); // ~9.766 KB
1408
1972
  */
1409
1973
  static calculateKBytes ( aString, encode = CacheData.PLAIN_ENCODING ) {
1410
1974
  return CacheData.calculateKBytes( aString, encode);
1411
1975
  };
1412
1976
 
1413
1977
  /**
1414
- * Converts a comma delimited string or an array to an array with all
1415
- * lowercase values. Can be used to pass a comma delimited string
1416
- * for conversion to an array that will then be used as (lowercase) keys.
1417
- * @param {string|Array} list
1418
- * @returns Array with lowercase values
1978
+ * Convert a comma-delimited string or array to an array with all lowercase values.
1979
+ * This method is useful for normalizing lists of header names or other identifiers
1980
+ * for case-insensitive comparison.
1981
+ *
1982
+ * If an array is provided, it is first converted to a comma-delimited string,
1983
+ * then lowercased and split back into an array.
1984
+ *
1985
+ * @param {string|Array.<string>} list Comma-delimited string or array to convert
1986
+ * @returns {Array.<string>} Array with all values converted to lowercase
1987
+ * @example
1988
+ * const headers = Cache.convertToLowerCaseArray('Content-Type,Cache-Control,ETag');
1989
+ * console.log(headers); // ['content-type', 'cache-control', 'etag']
1990
+ *
1991
+ * @example
1992
+ * const headers = Cache.convertToLowerCaseArray(['Content-Type', 'Cache-Control']);
1993
+ * console.log(headers); // ['content-type', 'cache-control']
1419
1994
  */
1420
1995
  static convertToLowerCaseArray(list) {
1421
1996
 
@@ -1429,15 +2004,43 @@ class Cache {
1429
2004
  };
1430
2005
 
1431
2006
  /**
1432
- * Takes either a csv string or an Array. It will return an array
1433
- * with lowercase values to be used as header keys
1434
- * @param {string|Array} list
1435
- * @returns Array with lowercase values
2007
+ * Internal method to parse and normalize the headersToRetain configuration.
2008
+ * Converts either a comma-delimited string or array into an array of lowercase
2009
+ * header names that should be retained from the origin response.
2010
+ *
2011
+ * @private
2012
+ * @param {string|Array.<string>} list Comma-delimited string or array of header names
2013
+ * @returns {Array.<string>} Array of lowercase header names to retain
2014
+ * @example
2015
+ * // Called internally during Cache constructor
2016
+ * const headers = this.#parseHeadersToRetain('Content-Type,Cache-Control');
2017
+ * // Returns: ['content-type', 'cache-control']
1436
2018
  */
1437
2019
  #parseHeadersToRetain (list) {
1438
2020
  return Cache.convertToLowerCaseArray(list);
1439
2021
  };
1440
2022
 
2023
+ /**
2024
+ * Get the cache profile configuration for this Cache instance.
2025
+ * Returns an object containing all the cache policy settings that were
2026
+ * configured when this Cache object was created.
2027
+ *
2028
+ * @returns {{
2029
+ * overrideOriginHeaderExpiration: boolean,
2030
+ * defaultExpirationInSeconds: number,
2031
+ * defaultExpirationExtensionOnErrorInSeconds: number,
2032
+ * expirationIsOnInterval: boolean,
2033
+ * headersToRetain: Array<string>,
2034
+ * hostId: string,
2035
+ * pathId: string,
2036
+ * encrypt: boolean
2037
+ * }} Cache profile configuration object
2038
+ * @example
2039
+ * const cache = new Cache(connection, cacheProfile);
2040
+ * const profile = cache.profile();
2041
+ * console.log(`Expiration: ${profile.defaultExpirationInSeconds}s`);
2042
+ * console.log(`Encrypted: ${profile.encrypt}`);
2043
+ */
1441
2044
  profile () {
1442
2045
  return {
1443
2046
  overrideOriginHeaderExpiration: this.#overrideOriginHeaderExpiration,
@@ -1452,8 +2055,25 @@ class Cache {
1452
2055
  };
1453
2056
 
1454
2057
  /**
2058
+ * Read cached data from storage (DynamoDB and potentially S3). This method
2059
+ * first checks the in-memory cache (if enabled), then falls back to DynamoDB.
1455
2060
  *
1456
- * @returns {Promise<CacheDataFormat>}
2061
+ * If data is found in the in-memory cache and not expired, it is returned
2062
+ * immediately. Otherwise, data is fetched from DynamoDB and stored in the
2063
+ * in-memory cache for future requests.
2064
+ *
2065
+ * This method is called automatically by CacheableDataAccess.getData() and
2066
+ * typically does not need to be called directly.
2067
+ *
2068
+ * @returns {Promise<CacheDataFormat>} Formatted cache data with body, headers, expires, and statusCode
2069
+ * @example
2070
+ * const cache = new Cache(connection, cacheProfile);
2071
+ * await cache.read();
2072
+ * if (cache.needsRefresh()) {
2073
+ * console.log('Cache miss or expired');
2074
+ * } else {
2075
+ * console.log('Cache hit:', cache.getBody());
2076
+ * }
1457
2077
  */
1458
2078
  async read () {
1459
2079
 
@@ -1463,15 +2083,63 @@ class Cache {
1463
2083
  resolve(this.#store);
1464
2084
  } else {
1465
2085
  try {
2086
+ let staleData = null;
2087
+
2088
+ // Check L0_Cache if feature is enabled
2089
+ if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null) {
2090
+ const memResult = Cache.#inMemoryCache.get(this.#idHash);
2091
+
2092
+ if (memResult.cache === 1) {
2093
+ // Cache hit - return immediately
2094
+ this.#store = memResult.data;
2095
+ this.#status = Cache.STATUS_CACHE_IN_MEM;
2096
+ tools.DebugAndLog.debug(`In-memory cache hit: ${this.#idHash}`);
2097
+ resolve(this.#store);
2098
+ return;
2099
+ } else if (memResult.cache === -1) {
2100
+ // Expired - retain for potential fallback
2101
+ staleData = memResult.data;
2102
+ tools.DebugAndLog.debug(`In-memory cache expired, retaining stale data: ${this.#idHash}`);
2103
+ }
2104
+ // cache === 0 means miss, continue to DynamoDB
2105
+ }
2106
+
2107
+ // Fetch from DynamoDB
1466
2108
  this.#store = await CacheData.read(this.#idHash, this.#syncedLaterTimestampInSeconds);
1467
2109
  this.#status = ( this.#store.cache.statusCode === null ) ? Cache.STATUS_NO_CACHE : Cache.STATUS_CACHE;
2110
+
2111
+ // Store in L0_Cache if successful and feature enabled
2112
+ if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null && this.#store.cache.statusCode !== null) {
2113
+ const expiresAt = this.#store.cache.expires * 1000; // Convert to milliseconds
2114
+ Cache.#inMemoryCache.set(this.#idHash, this.#store, expiresAt);
2115
+ tools.DebugAndLog.debug(`Stored in L0_Cache: ${this.#idHash}`);
2116
+ }
1468
2117
 
1469
2118
  tools.DebugAndLog.debug(`Cache Read status: ${this.#status}`);
1470
2119
 
1471
2120
  resolve(this.#store);
1472
2121
  } catch (error) {
1473
- this.#store = CacheData.format(this.#syncedLaterTimestampInSeconds);
1474
- this.#status = Cache.STATUS_CACHE_ERROR;
2122
+ // Error occurred - check if we have stale data to return
2123
+ if (staleData !== null) {
2124
+ // Calculate new expiration using error extension
2125
+ const newExpires = this.#syncedNowTimestampInSeconds + this.#defaultExpirationExtensionOnErrorInSeconds;
2126
+ const newExpiresAt = newExpires * 1000;
2127
+
2128
+ // Update stale data expiration
2129
+ staleData.cache.expires = newExpires;
2130
+
2131
+ // Store updated stale data back in L0_Cache
2132
+ if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null) {
2133
+ Cache.#inMemoryCache.set(this.#idHash, staleData, newExpiresAt);
2134
+ }
2135
+
2136
+ this.#store = staleData;
2137
+ this.#status = Cache.STATUS_CACHE_ERROR;
2138
+ tools.DebugAndLog.warn(`Returning stale data due to error: ${this.#idHash}`);
2139
+ } else {
2140
+ this.#store = CacheData.format(this.#syncedLaterTimestampInSeconds);
2141
+ this.#status = Cache.STATUS_CACHE_ERROR;
2142
+ }
1475
2143
 
1476
2144
  tools.DebugAndLog.error(`Cache Read: Cannot read cached data for ${this.#idHash}: ${error?.message || 'Unknown error'}`, error?.stack);
1477
2145
 
@@ -1483,6 +2151,39 @@ class Cache {
1483
2151
 
1484
2152
  };
1485
2153
 
2154
+ /**
2155
+ * Get diagnostic test data for this Cache instance. Returns an object containing
2156
+ * the results of calling various getter methods, useful for debugging and
2157
+ * verifying cache state.
2158
+ *
2159
+ * This method is primarily for testing and debugging purposes.
2160
+ *
2161
+ * @returns {{
2162
+ * get: CacheDataFormat,
2163
+ * getStatus: string,
2164
+ * getETag: string,
2165
+ * getLastModified: string,
2166
+ * getExpires: number,
2167
+ * getExpiresGMT: string,
2168
+ * getHeaders: Object,
2169
+ * getSyncedNowTimestampInSeconds: number,
2170
+ * getBody: string,
2171
+ * getIdHash: string,
2172
+ * getClassification: string,
2173
+ * needsRefresh: boolean,
2174
+ * isExpired: boolean,
2175
+ * isEmpty: boolean,
2176
+ * isPrivate: boolean,
2177
+ * isPublic: boolean,
2178
+ * currentStatus: string,
2179
+ * calculateDefaultExpires: number
2180
+ * }} Object containing test data from various cache methods
2181
+ * @example
2182
+ * const cache = new Cache(connection, cacheProfile);
2183
+ * await cache.read();
2184
+ * const testData = cache.test();
2185
+ * console.log('Cache test data:', testData);
2186
+ */
1486
2187
  test() {
1487
2188
  return {
1488
2189
  get: this.get(),
@@ -1507,40 +2208,86 @@ class Cache {
1507
2208
  };
1508
2209
 
1509
2210
  /**
2211
+ * Get the complete cache data object. Returns the internal cache store
2212
+ * containing the cached body, headers, expiration, and status code.
1510
2213
  *
1511
- * @returns {CacheDataFormat}
2214
+ * @returns {CacheDataFormat} The complete cache data object
2215
+ * @example
2216
+ * const cache = new Cache(connection, cacheProfile);
2217
+ * await cache.read();
2218
+ * const data = cache.get();
2219
+ * console.log(data.cache.body);
2220
+ * console.log(data.cache.expires);
1512
2221
  */
1513
2222
  get() {
1514
2223
  return this.#store;
1515
2224
  };
1516
2225
 
1517
2226
  /**
2227
+ * Get the source status of the cache data. Returns a status string indicating
2228
+ * where the data came from and its current state.
2229
+ *
2230
+ * Possible values include:
2231
+ * - Cache.STATUS_NO_CACHE: No cached data exists
2232
+ * - Cache.STATUS_CACHE: Data from cache
2233
+ * - Cache.STATUS_CACHE_IN_MEM: Data from in-memory cache
2234
+ * - Cache.STATUS_EXPIRED: Cached data was expired
2235
+ * - Cache.STATUS_CACHE_SAME: Cache updated but content unchanged
2236
+ * - Cache.STATUS_ORIGINAL_NOT_MODIFIED: Origin returned 304 Not Modified
2237
+ * - Cache.STATUS_ORIGINAL_ERROR: Error fetching from origin
2238
+ * - Cache.STATUS_CACHE_ERROR: Error reading from cache
2239
+ * - Cache.STATUS_FORCED: Cache update was forced
1518
2240
  *
1519
- * @returns {string}
2241
+ * @returns {string} The source status string
2242
+ * @example
2243
+ * const cache = new Cache(connection, cacheProfile);
2244
+ * await cache.read();
2245
+ * console.log(`Data source: ${cache.getSourceStatus()}`);
1520
2246
  */
1521
2247
  getSourceStatus() {
1522
2248
  return this.#status;
1523
2249
  };
1524
2250
 
1525
2251
  /**
2252
+ * Get the ETag header value from the cached data. The ETag is used for
2253
+ * cache validation and conditional requests.
1526
2254
  *
1527
- * @returns {string}
2255
+ * @returns {string|null} The ETag value, or null if not present
2256
+ * @example
2257
+ * const cache = new Cache(connection, cacheProfile);
2258
+ * await cache.read();
2259
+ * const etag = cache.getETag();
2260
+ * console.log(`ETag: ${etag}`);
1528
2261
  */
1529
2262
  getETag() {
1530
2263
  return this.getHeader("etag");
1531
2264
  };
1532
2265
 
1533
2266
  /**
2267
+ * Get the Last-Modified header value from the cached data. This timestamp
2268
+ * indicates when the cached content was last modified at the origin.
1534
2269
  *
1535
- * @returns {string} The falue of the cached header field last-modified
2270
+ * @returns {string|null} The Last-Modified header value in HTTP date format, or null if not present
2271
+ * @example
2272
+ * const cache = new Cache(connection, cacheProfile);
2273
+ * await cache.read();
2274
+ * const lastModified = cache.getLastModified();
2275
+ * console.log(`Last modified: ${lastModified}`);
1536
2276
  */
1537
2277
  getLastModified() {
1538
2278
  return this.getHeader("last-modified");
1539
2279
  };
1540
2280
 
1541
2281
  /**
2282
+ * Get the expiration timestamp of the cached data. Returns the Unix timestamp
2283
+ * (in seconds) when the cached data will expire.
1542
2284
  *
1543
- * @returns {number} Expiration timestamp in seconds
2285
+ * @returns {number} Expiration timestamp in seconds since Unix epoch, or 0 if no cache data
2286
+ * @example
2287
+ * const cache = new Cache(connection, cacheProfile);
2288
+ * await cache.read();
2289
+ * const expires = cache.getExpires();
2290
+ * console.log(`Expires at: ${new Date(expires * 1000)}`);
1544
2291
  */
1545
2292
  getExpires() {
1546
2293
  let exp = (this.#store !== null) ? this.#store.cache.expires : 0;
@@ -1553,14 +2300,26 @@ class Cache {
1553
2300
  * Example: "Wed, 28 Jul 2021 12:24:11 GMT"
1554
2301
  *
1555
2302
  * @returns {string} The expiration formated for use in headers. Same as expires header.
2303
+ * @example
2304
+ * const cache = new Cache(connection, cacheProfile);
2305
+ * await cache.read();
2306
+ * const expiresGMT = cache.getExpiresGMT();
2307
+ * console.log(expiresGMT); // "Wed, 28 Jul 2021 12:24:11 GMT"
1556
2308
  */
1557
2309
  getExpiresGMT() {
1558
2310
  return this.getHeader("expires");
1559
2311
  };
1560
2312
 
1561
2313
  /**
2314
+ * Calculate the number of seconds remaining until the cached data expires.
2315
+ * Returns 0 if the cache is already expired or if there is no cached data.
1562
2316
  *
1563
- * @returns {number} The calculated number of seconds from now until expires
2317
+ * @returns {number} The number of seconds until expiration, or 0 if expired
2318
+ * @example
2319
+ * const cache = new Cache(connection, cacheProfile);
2320
+ * await cache.read();
2321
+ * const secondsLeft = cache.calculateSecondsLeftUntilExpires();
2322
+ * console.log(`Cache expires in ${secondsLeft} seconds`);
1564
2323
  */
1565
2324
  calculateSecondsLeftUntilExpires() {
1566
2325
  let secondsLeftUntilExpires = this.getExpires() - CacheData.convertTimestampFromMilliToSeconds( Date.now() );
@@ -1570,59 +2329,120 @@ class Cache {
1570
2329
  };
1571
2330
 
1572
2331
  /**
1573
- * Example: public, max-age=123456
1574
- * @returns {string} The value for cache-control header
2332
+ * Generate the value for the Cache-Control HTTP header. Returns a string
2333
+ * containing the classification (public or private) and the max-age directive
2334
+ * based on the time remaining until expiration.
2335
+ *
2336
+ * Example output: "public, max-age=3600" or "private, max-age=300"
2337
+ *
2338
+ * @returns {string} The Cache-Control header value
2339
+ * @example
2340
+ * const cache = new Cache(connection, cacheProfile);
2341
+ * await cache.read();
2342
+ * const cacheControl = cache.getCacheControlHeaderValue();
2343
+ * console.log(cacheControl); // "public, max-age=3600"
1575
2344
  */
1576
2345
  getCacheControlHeaderValue() {
1577
2346
  return this.getClassification() +", max-age="+this.calculateSecondsLeftUntilExpires();
1578
2347
  };
1579
2348
 
1580
2349
  /**
2350
+ * Get all HTTP headers from the cached data. Returns an object containing
2351
+ * all header key-value pairs that were stored with the cached content.
1581
2352
  *
1582
- * @returns {object|null} All the header key/value pairs for the cached object
2353
+ * @returns {Object|null} Object containing all cached headers, or null if no cache data
2354
+ * @example
2355
+ * const cache = new Cache(connection, cacheProfile);
2356
+ * await cache.read();
2357
+ * const headers = cache.getHeaders();
2358
+ * console.log(headers['content-type']);
2359
+ * console.log(headers['etag']);
1583
2360
  */
1584
2361
  getHeaders() {
1585
2362
  return (this.#store !== null && "headers" in this.#store.cache) ? this.#store.cache.headers : null;
1586
2363
  };
1587
2364
 
1588
2365
  /**
2366
+ * Get the HTTP status code from the cached data. Returns the status code
2367
+ * that was stored when the data was originally cached.
1589
2368
  *
1590
- * @returns {string|null} The status code of the cache object
2369
+ * @returns {string|null} The HTTP status code (e.g., "200", "404"), or null if no cache data
2370
+ * @example
2371
+ * const cache = new Cache(connection, cacheProfile);
2372
+ * await cache.read();
2373
+ * const statusCode = cache.getStatusCode();
2374
+ * console.log(`Status: ${statusCode}`); // "200"
1591
2375
  */
1592
2376
  getStatusCode() {
1593
2377
  return (this.#store !== null && "statusCode" in this.#store.cache) ? this.#store.cache.statusCode : null;
1594
2378
  };
1595
2379
 
1596
2380
  /**
2381
+ * Get the current error code for this cache instance. Returns a non-zero
2382
+ * error code if an error occurred during cache operations, or 0 if no error.
2383
+ *
2384
+ * Error codes are typically HTTP status codes (400+) that were encountered
2385
+ * when trying to refresh the cache from the origin.
1597
2386
  *
1598
- * @returns {number} Current error code for this cache
2387
+ * @returns {number} The error code, or 0 if no error
2388
+ * @example
2389
+ * const cache = new Cache(connection, cacheProfile);
2390
+ * await cache.read();
2391
+ * const errorCode = cache.getErrorCode();
2392
+ * if (errorCode >= 400) {
2393
+ * console.log(`Error occurred: ${errorCode}`);
2394
+ * }
1599
2395
  */
1600
2396
  getErrorCode() {
1601
2397
  return this.#errorCode;
1602
2398
  };
1603
2399
 
1604
2400
  /**
1605
- * Classification is used in the cache header to note how the data returned
1606
- * should be treated in the cache. If it is private then it should be
1607
- * protected.
1608
- * @returns {string} Based on whether the cache is stored as encrypted, returns "private" (encrypted) or "public" (not encrypted)
2401
+ * Get the classification of the cached data. Returns "private" if the data
2402
+ * is encrypted, or "public" if not encrypted.
2403
+ *
2404
+ * This classification is used in the Cache-Control header to indicate how
2405
+ * the data should be treated by intermediate caches and proxies.
2406
+ *
2407
+ * @returns {string} Either "private" (encrypted) or "public" (not encrypted)
2408
+ * @example
2409
+ * const cache = new Cache(connection, { encrypt: true });
2410
+ * console.log(cache.getClassification()); // "private"
2411
+ *
2412
+ * const publicCache = new Cache(connection, { encrypt: false });
2413
+ * console.log(publicCache.getClassification()); // "public"
1609
2414
  */
1610
2415
  getClassification() {
1611
2416
  return (this.#encrypt ? Cache.PRIVATE : Cache.PUBLIC );
1612
2417
  };
1613
2418
 
1614
2419
  /**
2420
+ * Get the synchronized "now" timestamp in seconds. This timestamp is set when
2421
+ * the Cache object is created and remains constant throughout the cache operation,
2422
+ * providing a consistent time base for expiration calculations.
1615
2423
  *
1616
- * @returns {number} The timestamp in seconds of when the object was created and used for currency logic of the cache. (used as comparasion against expiration and for creating new expirations)
2424
+ * @returns {number} The timestamp in seconds when this Cache object was created
2425
+ * @example
2426
+ * const cache = new Cache(connection, cacheProfile);
2427
+ * const now = cache.getSyncedNowTimestampInSeconds();
2428
+ * console.log(`Cache created at: ${new Date(now * 1000)}`);
1617
2429
  */
1618
2430
  getSyncedNowTimestampInSeconds() {
1619
2431
  return this.#syncedNowTimestampInSeconds;
1620
2432
  };
1621
2433
 
1622
2434
  /**
2435
+ * Get a specific header value from the cached data by key. Header keys are
2436
+ * case-insensitive (stored as lowercase).
1623
2437
  *
1624
- * @param {string} key The header key to access
1625
- * @returns {string|number|null} The value assigned to the provided header key. null if it doesn't exist
2438
+ * @param {string} key The header key to retrieve (case-insensitive)
2439
+ * @returns {string|number|null} The header value, or null if the header doesn't exist
2440
+ * @example
2441
+ * const cache = new Cache(connection, cacheProfile);
2442
+ * await cache.read();
2443
+ * const contentType = cache.getHeader('content-type');
2444
+ * const etag = cache.getHeader('ETag'); // Case-insensitive
2445
+ * console.log(`Content-Type: ${contentType}`);
1626
2446
  */
1627
2447
  getHeader(key) {
1628
2448
  let headers = this.getHeaders();
@@ -1630,9 +2450,25 @@ class Cache {
1630
2450
  };
1631
2451
 
1632
2452
  /**
2453
+ * Get the cached body content. Optionally parses JSON content into an object.
1633
2454
  *
1634
- * @param {boolean} parseBody If set to true then JSON decode will be used on the body before returning.
1635
- * @returns {string|object|null} A string (as is) which could be encoded JSON but we want to leave it that way, an object if parseBody is set to true and it is parsable by JSON, or null if body is null
2455
+ * By default, returns the body as a string (which may be JSON-encoded). If
2456
+ * parseBody is true, attempts to parse the body as JSON and return an object.
2457
+ * If parsing fails, returns the original string.
2458
+ *
2459
+ * @param {boolean} [parseBody=false] If true, parse JSON body into an object
2460
+ * @returns {string|Object|null} The body as a string, parsed object, or null if no cache data
2461
+ * @example
2462
+ * const cache = new Cache(connection, cacheProfile);
2463
+ * await cache.read();
2464
+ *
2465
+ * // Get body as string
2466
+ * const bodyStr = cache.getBody();
2467
+ * console.log(bodyStr); // '{"data":"value"}'
2468
+ *
2469
+ * // Get body as parsed object
2470
+ * const bodyObj = cache.getBody(true);
2471
+ * console.log(bodyObj.data); // 'value'
1636
2472
  */
1637
2473
  getBody(parseBody = false) {
1638
2474
  let body = (this.#store !== null) ? this.#store.cache.body : null;
@@ -1649,10 +2485,20 @@ class Cache {
1649
2485
  };
1650
2486
 
1651
2487
  /**
1652
- * Returns a plain data response in the form of an object. If a full HTTP
1653
- * response is needed use generateResponseForAPIGateway()
1654
- * @param {boolean} parseBody If true we'll return body as object
1655
- * @returns {{statusCode: string, headers: object, body: string|object}} a plain data response in the form of an object
2488
+ * Get a plain data response object containing the status code, headers, and body.
2489
+ * This is a simplified response format suitable for internal use.
2490
+ *
2491
+ * For a full HTTP response suitable for API Gateway, use generateResponseForAPIGateway() instead.
2492
+ *
2493
+ * @param {boolean} [parseBody=false] If true, parse JSON body into an object
2494
+ * @returns {{statusCode: string, headers: Object, body: string|Object}|null} Response object, or null if no cache data
2495
+ * @example
2496
+ * const cache = new Cache(connection, cacheProfile);
2497
+ * await cache.read();
2498
+ * const response = cache.getResponse();
2499
+ * console.log(response.statusCode); // "200"
2500
+ * console.log(response.headers['content-type']);
2501
+ * console.log(response.body);
1656
2502
  */
1657
2503
  getResponse(parseBody = false) {
1658
2504
  let response = null;
@@ -1669,9 +2515,28 @@ class Cache {
1669
2515
  };
1670
2516
 
1671
2517
  /**
2518
+ * Generate a complete HTTP response suitable for AWS API Gateway. This method
2519
+ * adds standard headers (CORS, Cache-Control, data source), handles conditional
2520
+ * requests (If-None-Match, If-Modified-Since), and formats the response according
2521
+ * to API Gateway requirements.
2522
+ *
2523
+ * If the client's ETag or Last-Modified matches the cached data, returns a
2524
+ * 304 Not Modified response with no body.
2525
+ *
2526
+ * @param {Object} parameters Configuration parameters for the response
2527
+ * @param {string} [parameters.ifNoneMatch] The If-None-Match header value from the client request
2528
+ * @param {string} [parameters.ifModifiedSince] The If-Modified-Since header value from the client request
2529
+ * @returns {{statusCode: string, headers: Object, body: string|null}} Complete HTTP response for API Gateway
2530
+ * @example
2531
+ * const cache = new Cache(connection, cacheProfile);
2532
+ * await cache.read();
2533
+ *
2534
+ * const response = cache.generateResponseForAPIGateway({
2535
+ * ifNoneMatch: event.headers['if-none-match'],
2536
+ * ifModifiedSince: event.headers['if-modified-since']
2537
+ * });
1672
2538
  *
1673
- * @param {object} parameters
1674
- * @returns {{statusCode: string, headers: object, body: string}}
2539
+ * return response; // Return to API Gateway
1675
2540
  */
1676
2541
  generateResponseForAPIGateway( parameters ) {
1677
2542
 
@@ -1715,59 +2580,121 @@ class Cache {
1715
2580
  };
1716
2581
 
1717
2582
  /**
2583
+ * Get the unique identifier hash for this cache entry. This hash is generated
2584
+ * from the connection, data, and cache policy parameters and uniquely identifies
2585
+ * this specific cache entry.
1718
2586
  *
1719
- * @returns {string}
2587
+ * @returns {string} The unique cache identifier hash
2588
+ * @example
2589
+ * const cache = new Cache(connection, cacheProfile);
2590
+ * const idHash = cache.getIdHash();
2591
+ * console.log(`Cache ID: ${idHash}`);
1720
2592
  */
1721
2593
  getIdHash() {
1722
2594
  return this.#idHash;
1723
2595
  };
1724
2596
 
1725
2597
  /**
2598
+ * Check if the cache needs to be refreshed. Returns true if the cache is
2599
+ * expired or empty, indicating that fresh data should be fetched from the origin.
1726
2600
  *
1727
- * @returns {boolean}
2601
+ * @returns {boolean} True if cache needs refresh, false otherwise
2602
+ * @example
2603
+ * const cache = new Cache(connection, cacheProfile);
2604
+ * await cache.read();
2605
+ * if (cache.needsRefresh()) {
2606
+ * console.log('Fetching fresh data from origin');
2607
+ * // Fetch and update cache
2608
+ * }
1728
2609
  */
1729
2610
  needsRefresh() {
1730
2611
  return (this.isExpired() || this.isEmpty());
1731
2612
  };
1732
2613
 
1733
2614
  /**
2615
+ * Check if the cached data has expired. Compares the expiration timestamp
2616
+ * with the current time to determine if the cache is still valid.
1734
2617
  *
1735
- * @returns {boolean}
2618
+ * @returns {boolean} True if cache is expired, false if still valid
2619
+ * @example
2620
+ * const cache = new Cache(connection, cacheProfile);
2621
+ * await cache.read();
2622
+ * if (cache.isExpired()) {
2623
+ * console.log('Cache has expired');
2624
+ * }
1736
2625
  */
1737
2626
  isExpired() {
1738
2627
  return ( CacheData.convertTimestampFromSecondsToMilli(this.getExpires()) <= Date.now());
1739
2628
  };
1740
2629
 
1741
2630
  /**
2631
+ * Check if the cache is empty (no cached data exists). Returns true if there
2632
+ * is no cached data, indicating this is a cache miss.
1742
2633
  *
1743
- * @returns {boolean}
2634
+ * @returns {boolean} True if cache is empty, false if data exists
2635
+ * @example
2636
+ * const cache = new Cache(connection, cacheProfile);
2637
+ * await cache.read();
2638
+ * if (cache.isEmpty()) {
2639
+ * console.log('Cache miss - no data found');
2640
+ * }
1744
2641
  */
1745
2642
  isEmpty() {
1746
2643
  return (this.#store.cache.statusCode === null);
1747
2644
  };
1748
2645
 
1749
2646
  /**
2647
+ * Check if the cache is configured for private (encrypted) storage.
2648
+ * Private caches encrypt data at rest for sensitive content.
1750
2649
  *
1751
- * @returns {boolean}
2650
+ * @returns {boolean} True if cache is private/encrypted, false otherwise
2651
+ * @example
2652
+ * const cache = new Cache(connection, { encrypt: true });
2653
+ * if (cache.isPrivate()) {
2654
+ * console.log('Cache data is encrypted');
2655
+ * }
1752
2656
  */
1753
2657
  isPrivate() {
1754
2658
  return this.#encrypt;
1755
2659
  };
1756
2660
 
1757
2661
  /**
2662
+ * Check if the cache is configured for public (non-encrypted) storage.
2663
+ * Public caches do not encrypt data at rest.
1758
2664
  *
1759
- * @returns {boolean}
2665
+ * @returns {boolean} True if cache is public/non-encrypted, false otherwise
2666
+ * @example
2667
+ * const cache = new Cache(connection, { encrypt: false });
2668
+ * if (cache.isPublic()) {
2669
+ * console.log('Cache data is not encrypted');
2670
+ * }
1760
2671
  */
1761
2672
  isPublic() {
1762
2673
  return !this.#encrypt;
1763
2674
  };
1764
2675
 
1765
2676
  /**
2677
+ * Extend the expiration time of the cached data. This method is used when
2678
+ * the origin returns a 304 Not Modified response or when an error occurs
2679
+ * fetching fresh data.
2680
+ *
2681
+ * If an error occurred (reason is STATUS_ORIGINAL_ERROR), the cache is extended
2682
+ * by defaultExpirationExtensionOnErrorInSeconds. Otherwise, it's extended by
2683
+ * the specified seconds or the default expiration time.
2684
+ *
2685
+ * @param {string} reason Reason for extending: Cache.STATUS_ORIGINAL_ERROR or Cache.STATUS_ORIGINAL_NOT_MODIFIED
2686
+ * @param {number} [seconds=0] Number of seconds to extend (0 = use default)
2687
+ * @param {number} [errorCode=0] HTTP error code if extending due to error
2688
+ * @returns {Promise<boolean>} True if extension was successful, false otherwise
2689
+ * @example
2690
+ * const cache = new Cache(connection, cacheProfile);
2691
+ * await cache.read();
1766
2692
  *
1767
- * @param {string} reason Reason for extending, either Cache.STATUS_ORIGINAL_ERROR or Cache.STATUS_ORIGINAL_NOT_MODIFIED
1768
- * @param {number} seconds
1769
- * @param {number} errorCode
1770
- * @returns {Promise<boolean>}
2693
+ * // Extend due to 304 Not Modified
2694
+ * await cache.extendExpires(Cache.STATUS_ORIGINAL_NOT_MODIFIED);
2695
+ *
2696
+ * // Extend due to error
2697
+ * await cache.extendExpires(Cache.STATUS_ORIGINAL_ERROR, 0, 500);
1771
2698
  */
1772
2699
  async extendExpires(reason, seconds = 0, errorCode = 0) {
1773
2700
 
@@ -1821,28 +2748,65 @@ class Cache {
1821
2748
  };
1822
2749
 
1823
2750
  /**
2751
+ * Calculate the default expiration timestamp for cached data. If expiration
2752
+ * is configured to use intervals, calculates the next interval boundary.
2753
+ * Otherwise, returns the synced later timestamp (now + default expiration).
1824
2754
  *
1825
- * @returns {number}
2755
+ * @returns {number} The default expiration timestamp in seconds
2756
+ * @example
2757
+ * const cache = new Cache(connection, { expirationIsOnInterval: true, defaultExpirationInSeconds: 3600 });
2758
+ * const expires = cache.calculateDefaultExpires();
2759
+ * console.log(`Default expiration: ${new Date(expires * 1000)}`);
1826
2760
  */
1827
2761
  calculateDefaultExpires() {
1828
2762
  return (this.#expirationIsOnInterval) ? Cache.nextIntervalInSeconds(this.#defaultExpirationInSeconds, this.#syncedNowTimestampInSeconds) : this.#syncedLaterTimestampInSeconds;
1829
2763
  };
1830
2764
 
1831
2765
  /**
2766
+ * Get the current status of the cache. Returns a status string indicating
2767
+ * the source and state of the cached data.
1832
2768
  *
1833
- * @returns {string}
2769
+ * This is an alias for getSourceStatus().
2770
+ *
2771
+ * @returns {string} The cache status string
2772
+ * @example
2773
+ * const cache = new Cache(connection, cacheProfile);
2774
+ * await cache.read();
2775
+ * console.log(`Status: ${cache.getStatus()}`);
1834
2776
  */
1835
2777
  getStatus() {
1836
2778
  return this.#status;
1837
2779
  };
1838
2780
 
1839
2781
  /**
1840
- * Store data in cache. Returns a representation of data stored in the cache
1841
- * @param {object} body
1842
- * @param {object} headers Any headers you want to pass along, including last-modified, etag, and expires. Note that if expires is included as a header here, then it will override the expires paramter passed to .update()
1843
- * @param {number} statusCode Status code of original request
1844
- * @param {number} expires Expiration unix timestamp in seconds
2782
+ * Store data in cache. Updates the cache with new content, headers, status code,
2783
+ * and expiration time. This method handles header normalization, expiration
2784
+ * calculation, and writes the data to both DynamoDB and S3 (if needed).
2785
+ *
2786
+ * The method automatically:
2787
+ * - Lowercases all header keys for consistency
2788
+ * - Retains specified headers from the origin response
2789
+ * - Generates ETag and Last-Modified headers if not present
2790
+ * - Calculates expiration based on origin headers or default settings
2791
+ * - Encrypts data if configured as private
2792
+ * - Routes large items to S3 storage
2793
+ *
2794
+ * @param {string|Object} body The content body to cache (will be stringified if object)
2795
+ * @param {Object} headers HTTP headers from the origin response
2796
+ * @param {string|number} [statusCode=200] HTTP status code from the origin
2797
+ * @param {number} [expires=0] Expiration Unix timestamp in seconds (0 = calculate default)
2798
+ * @param {string|null} [status=null] Optional status override (e.g., Cache.STATUS_FORCED)
1845
2799
  * @returns {Promise<CacheDataFormat>} Representation of data stored in cache
2800
+ * @example
2801
+ * const cache = new Cache(connection, cacheProfile);
2802
+ * await cache.read();
2803
+ *
2804
+ * // Update cache with fresh data
2805
+ * const body = JSON.stringify({ data: 'value' });
2806
+ * const headers = { 'content-type': 'application/json' };
2807
+ * await cache.update(body, headers, 200);
2808
+ *
2809
+ * console.log(`Cache updated: ${cache.getStatus()}`);
1846
2810
  */
1847
2811
  async update (body, headers, statusCode = 200, expires = 0, status = null) {
1848
2812
 
@@ -1959,16 +2923,59 @@ class Cache {
1959
2923
  };
1960
2924
  };
1961
2925
 
2926
+ /**
2927
+ * The CacheableDataAccess object provides an interface to
2928
+ * the cache. It is responsible for reading from and writing to cache.
2929
+ * All requests to data go through the cache
2930
+ *
2931
+ * Before using CacheableDataAccess, the Cache must be initialized.
2932
+ *
2933
+ * @example
2934
+ * // Init should be done outside the handler
2935
+ * Cache.init({parameters});
2936
+ *
2937
+ * // Then you can then make a request in the handler
2938
+ * // sending it through CacheableDataAccess:
2939
+ * const { cache } = require("@63klabs/cache-data");
2940
+ * const cacheObj = await cache.CacheableDataAccess.getData(
2941
+ * cacheCfg,
2942
+ * yourFetchFunction,
2943
+ * conn,
2944
+ * daoQuery
2945
+ * );
2946
+ */
1962
2947
  class CacheableDataAccess {
1963
2948
  constructor() { };
1964
2949
 
1965
2950
  static #prevId = -1;
1966
2951
 
2952
+ /**
2953
+ * Internal method to generate sequential IDs for logging and tracking cache requests.
2954
+ * Each call increments and returns the next ID in the sequence.
2955
+ *
2956
+ * @private
2957
+ * @returns {string} The next sequential ID as a string
2958
+ * @example
2959
+ * // Called internally by CacheableDataAccess.getData()
2960
+ * const id = CacheableDataAccess.#getNextId(); // "0", "1", "2", etc.
2961
+ */
1967
2962
  static #getNextId() {
1968
2963
  this.#prevId++;
1969
2964
  return ""+this.#prevId;
1970
2965
  };
1971
2966
 
2967
+ /**
2968
+ * Prime (refresh) runtime environment variables and cached secrets. This method
2969
+ * can be called to update values that may have changed since initialization.
2970
+ *
2971
+ * This is a convenience wrapper around CacheData.prime().
2972
+ *
2973
+ * @returns {Promise<boolean>} True if priming was successful, false if an error occurred
2974
+ * @example
2975
+ * // Prime before making cache requests
2976
+ * await CacheableDataAccess.prime();
2977
+ * const cache = await CacheableDataAccess.getData(cachePolicy, fetchFn, connection, data);
2978
+ */
1972
2979
  static async prime() {
1973
2980
  return CacheData.prime();
1974
2981
  };
@@ -1977,49 +2984,40 @@ class CacheableDataAccess {
1977
2984
  * Data access object that will evaluate the cache and make a request to
1978
2985
  * an endpoint to refresh.
1979
2986
  *
2987
+ * @param {object} cachePolicy A cache policy object with properties: overrideOriginHeaderExpiration (boolean), defaultExpirationInSeconds (number), expirationIsOnInterval (boolean), headersToRetain (Array|string), hostId (string), pathId (string), encrypt (boolean)
2988
+ * @param {Function} apiCallFunction The function to call in order to make the request. This function can call ANY datasource (file, http endpoint, etc) as long as it returns a DAO object
2989
+ * @param {object} connection A connection object that specifies an id, location, and connection details for the apiCallFunction to access data. If you have a Connection object pass conn.toObject(). Properties: method (string), protocol (string), host (string), path (string), parameters (object), headers (object), body (string, for POST requests), options (object with timeout in ms)
2990
+ * @param {object} [data=null] An object passed to the apiCallFunction as a parameter. Set to null if the apiCallFunction does not require a data param
2991
+ * @param {object} [tags={}] For logging. Do not include sensitive information.
2992
+ * @returns {Promise<Cache>} A Cache object with either cached or fresh data.
1980
2993
  * @example
1981
- * cachePolicy = {
1982
- * overrideOriginHeaderExpiration: true,
1983
- * defaultExpirationInSeconds: 60,
1984
- * expirationIsOnInterval: true,
1985
- * headersToRetain: [],
1986
- * host: vars.policy.host,
1987
- * path: vars.policy.endpoint.path,
1988
- * encrypt: true
1989
- * }
2994
+ * const cachePolicy = {
2995
+ * overrideOriginHeaderExpiration: true,
2996
+ * defaultExpirationInSeconds: 60,
2997
+ * expirationIsOnInterval: true,
2998
+ * headersToRetain: [],
2999
+ * hostId: 'api.example.com',
3000
+ * pathId: '/users',
3001
+ * encrypt: true
3002
+ * };
1990
3003
  *
1991
- * connection = {
1992
- * method: vars.method,
1993
- * protocol: vars.protocol,
1994
- * host: vars.host,
1995
- * path: vars.path,
1996
- * parameters: vars.parameters,
1997
- * headers: vars.requestHeaders,
1998
- * options: {timeout: vars.timeout}
1999
- * }
3004
+ * const connection = {
3005
+ * method: 'GET',
3006
+ * protocol: 'https',
3007
+ * host: 'api.example.com',
3008
+ * path: '/users/123',
3009
+ * parameters: {},
3010
+ * headers: {},
3011
+ * options: {timeout: 5000}
3012
+ * };
2000
3013
  *
2001
- * @param {object} cachePolicy A cache policy object.
2002
- * @param {boolean} cachePolicy.overrideOriginHeaderExpiration
2003
- * @param {number} cachePolicy.defaultExpirationInSeconds
2004
- * @param {boolean} cachePolicy.expirationIsOnInterval
2005
- * @param {Array|string} cachePolicy.headersToRetain
2006
- * @param {string} cachePolicy.hostId
2007
- * @param {string} cachePolicy.pathId
2008
- * @param {boolean} cachePolicy.encrypt
2009
- * @param {object} apiCallFunction The function to call in order to make the request. This function can call ANY datasource (file, http endpoint, etc) as long as it returns a DAO object
2010
- * @param {object} connection A connection object that specifies an id, location, and connection details for the apiCallFunction to access data. If you have a Connection object pass conn.toObject()
2011
- * @param {string} connection.method
2012
- * @param {string} connection.protocol
2013
- * @param {string} connection.host
2014
- * @param {string} connection.path
2015
- * @param {object} connection.parameters
2016
- * @param {object} connection.headers
2017
- * @param {string} connection.body For POST requests a body with data may be sent.
2018
- * @param {object} connection.options
2019
- * @param {number} connection.options.timeout Number in ms for request to time out
2020
- * @param {object} data An object passed to the apiCallFunction as a parameter. Set to null if the apiCallFunction does not require a data param
2021
- * @param {object} tags For logging. Do not include sensitive information.
2022
- * @returns {Promise<Cache>} A Cache object with either cached or fresh data.
3014
+ * const cache = await CacheableDataAccess.getData(
3015
+ * cachePolicy,
3016
+ * endpoint.get,
3017
+ * connection,
3018
+ * null,
3019
+ * {path: 'users', id: '123'}
3020
+ * );
2023
3021
  */
2024
3022
  static async getData(cachePolicy, apiCallFunction, connection, data = null, tags = {} ) {
2025
3023