@63klabs/cache-data 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -2
- package/CONTRIBUTING.md +167 -0
- package/README.md +139 -27
- package/package.json +13 -5
- package/src/lib/dao-cache.js +1418 -294
- package/src/lib/dao-endpoint.js +165 -41
- package/src/lib/tools/AWS.classes.js +82 -0
- package/src/lib/tools/CachedParametersSecrets.classes.js +98 -7
- package/src/lib/tools/ClientRequest.class.js +43 -10
- package/src/lib/tools/Connections.classes.js +148 -13
- package/src/lib/tools/DebugAndLog.class.js +244 -75
- package/src/lib/tools/ImmutableObject.class.js +44 -2
- package/src/lib/tools/RequestInfo.class.js +38 -0
- package/src/lib/tools/Response.class.js +245 -81
- package/src/lib/tools/ResponseDataModel.class.js +123 -47
- package/src/lib/tools/Timer.class.js +138 -26
- package/src/lib/tools/index.js +89 -2
- package/src/lib/tools/utils.js +40 -4
- package/src/lib/utils/InMemoryCache.js +221 -0
package/src/lib/dao-cache.js
CHANGED
|
@@ -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.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
85
|
-
*
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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.
|
|
195
|
-
*
|
|
196
|
-
*
|
|
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
|
|
207
|
-
*
|
|
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
|
-
*
|
|
225
|
-
*
|
|
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
|
|
233
|
-
*
|
|
234
|
-
*
|
|
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
|
|
273
|
-
*
|
|
274
|
-
*
|
|
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
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
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
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* @param {
|
|
333
|
-
* @param {string} parameters.
|
|
334
|
-
* @param {string
|
|
335
|
-
* @param {
|
|
336
|
-
* @param {
|
|
337
|
-
* @param {
|
|
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
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
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
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
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
|
|
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
|
-
*
|
|
445
|
-
*
|
|
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
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
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
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
* @param {
|
|
484
|
-
* @param {string}
|
|
485
|
-
* @
|
|
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
|
-
* @
|
|
494
|
-
* @param {
|
|
495
|
-
* @param {
|
|
496
|
-
* @param {number}
|
|
497
|
-
* @
|
|
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.
|
|
858
|
+
*
|
|
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' };
|
|
587
874
|
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
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
|
-
*
|
|
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
|
|
744
|
-
*
|
|
745
|
-
*
|
|
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.
|
|
779
1081
|
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
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.
|
|
1088
|
+
*
|
|
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.
|
|
802
1118
|
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
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.
|
|
1125
|
+
*
|
|
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
|
|
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.
|
|
830
1164
|
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
* @returns {string} 10 character ETag hash
|
|
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.
|
|
1167
|
+
*
|
|
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
|
|
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
|
-
*
|
|
855
|
-
*
|
|
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
|
-
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
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
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
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
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
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
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
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
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
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
|
-
*
|
|
975
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1150
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
* string "false"
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
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
|
|
|
@@ -1215,35 +1683,103 @@ class Cache {
|
|
|
1215
1683
|
// Boolean("false") is true so we need to code for it. As long as it is not "false", trust Boolean()
|
|
1216
1684
|
return (( value !== "false") ? Boolean(value) : false );
|
|
1217
1685
|
};
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Validate that a header value is suitable for HTTP header assignment.
|
|
1689
|
+
* Returns true if the value is a non-empty string (except "undefined") or number, false otherwise.
|
|
1690
|
+
*
|
|
1691
|
+
* This method is used internally to validate header values before assignment
|
|
1692
|
+
* to prevent undefined, null, or invalid types from being used in HTTP headers.
|
|
1693
|
+
* The string "undefined" is explicitly rejected because it causes HTTP errors
|
|
1694
|
+
* when used as a header value.
|
|
1695
|
+
*
|
|
1696
|
+
* @param {*} value The value to validate
|
|
1697
|
+
* @returns {boolean} True if value is valid for HTTP header, false otherwise
|
|
1698
|
+
* @private
|
|
1699
|
+
* @example
|
|
1700
|
+
* Cache._isValidHeaderValue('text'); // true
|
|
1701
|
+
* Cache._isValidHeaderValue(123); // true
|
|
1702
|
+
* Cache._isValidHeaderValue(''); // false
|
|
1703
|
+
* Cache._isValidHeaderValue('undefined'); // false (string "undefined" is invalid)
|
|
1704
|
+
* Cache._isValidHeaderValue(null); // false
|
|
1705
|
+
* Cache._isValidHeaderValue(undefined); // false
|
|
1706
|
+
* Cache._isValidHeaderValue(NaN); // false
|
|
1707
|
+
* Cache._isValidHeaderValue(true); // false
|
|
1708
|
+
* Cache._isValidHeaderValue({}); // false
|
|
1709
|
+
* Cache._isValidHeaderValue([]); // false
|
|
1710
|
+
*/
|
|
1711
|
+
static _isValidHeaderValue(value) {
|
|
1712
|
+
if (value === null || value === undefined) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
const type = typeof value;
|
|
1716
|
+
if (type === 'string') {
|
|
1717
|
+
// Filter out empty strings and the string "undefined"
|
|
1718
|
+
return value.length > 0 && value !== 'undefined';
|
|
1719
|
+
}
|
|
1720
|
+
if (type === 'number') {
|
|
1721
|
+
return !isNaN(value);
|
|
1722
|
+
}
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1218
1725
|
|
|
1219
1726
|
/**
|
|
1220
|
-
* Generate an
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1727
|
+
* Generate an ETag hash for cache validation. Creates a unique hash by combining
|
|
1728
|
+
* the cache ID hash with the content body. This is used for HTTP ETag headers to
|
|
1729
|
+
* enable conditional requests and cache validation.
|
|
1730
|
+
*
|
|
1731
|
+
* This is a convenience wrapper around CacheData.generateEtag().
|
|
1732
|
+
*
|
|
1733
|
+
* @param {string} idHash The unique identifier hash for the cache entry
|
|
1734
|
+
* @param {string} content The content body to include in the ETag calculation
|
|
1735
|
+
* @returns {string} A 10-character ETag hash
|
|
1736
|
+
* @example
|
|
1737
|
+
* const etag = Cache.generateEtag('abc123', '{"data":"value"}');
|
|
1738
|
+
* console.log(`ETag: ${etag}`); // e.g., "a1b2c3d4e5"
|
|
1224
1739
|
*/
|
|
1225
1740
|
static generateEtag(idHash, content) {
|
|
1226
1741
|
return CacheData.generateEtag(idHash, content);
|
|
1227
1742
|
};
|
|
1228
1743
|
|
|
1229
1744
|
/**
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1745
|
+
* Convert all keys in an object to lowercase. Useful for normalizing HTTP headers
|
|
1746
|
+
* or other key-value pairs for case-insensitive comparison.
|
|
1747
|
+
*
|
|
1748
|
+
* This is a convenience wrapper around CacheData.lowerCaseKeys().
|
|
1749
|
+
*
|
|
1750
|
+
* Note: If lowercasing keys causes a collision (e.g., "Content-Type" and "content-type"),
|
|
1751
|
+
* one value will overwrite the other.
|
|
1752
|
+
*
|
|
1753
|
+
* @param {Object} objectWithKeys Object whose keys should be lowercased
|
|
1754
|
+
* @returns {Object} New object with all keys converted to lowercase
|
|
1755
|
+
* @example
|
|
1756
|
+
* const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' };
|
|
1757
|
+
* const normalized = Cache.lowerCaseKeys(headers);
|
|
1758
|
+
* console.log(normalized); // { 'content-type': 'application/json', 'cache-control': 'no-cache' }
|
|
1234
1759
|
*/
|
|
1235
1760
|
static lowerCaseKeys(objectWithKeys) {
|
|
1236
1761
|
return CacheData.lowerCaseKeys(objectWithKeys);
|
|
1237
1762
|
};
|
|
1238
1763
|
|
|
1239
1764
|
/**
|
|
1240
|
-
* Generate an internet
|
|
1765
|
+
* Generate an internet-formatted date string for use in HTTP headers.
|
|
1766
|
+
* Converts a Unix timestamp to the standard HTTP date format.
|
|
1241
1767
|
*
|
|
1242
|
-
*
|
|
1768
|
+
* This is a convenience wrapper around CacheData.generateInternetFormattedDate().
|
|
1243
1769
|
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
* @
|
|
1770
|
+
* Example output: "Wed, 28 Jul 2021 12:24:11 GMT"
|
|
1771
|
+
*
|
|
1772
|
+
* @param {number} timestamp Unix timestamp (in seconds by default, or milliseconds if inMilliseconds is true)
|
|
1773
|
+
* @param {boolean} [inMilliseconds=false] Set to true if timestamp is in milliseconds
|
|
1774
|
+
* @returns {string} Internet-formatted date string (e.g., "Wed, 28 Jul 2021 12:24:11 GMT")
|
|
1775
|
+
* @example
|
|
1776
|
+
* const now = Math.floor(Date.now() / 1000);
|
|
1777
|
+
* const dateStr = Cache.generateInternetFormattedDate(now);
|
|
1778
|
+
* console.log(dateStr); // "Mon, 26 Jan 2026 15:30:45 GMT"
|
|
1779
|
+
*
|
|
1780
|
+
* @example
|
|
1781
|
+
* const nowMs = Date.now();
|
|
1782
|
+
* const dateStr = Cache.generateInternetFormattedDate(nowMs, true);
|
|
1247
1783
|
*/
|
|
1248
1784
|
static generateInternetFormattedDate(timestamp, inMilliseconds = false) {
|
|
1249
1785
|
return CacheData.generateInternetFormattedDate(timestamp, inMilliseconds);
|
|
@@ -1289,6 +1825,24 @@ class Cache {
|
|
|
1289
1825
|
*
|
|
1290
1826
|
* @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
1827
|
* @returns {string} A hash representing the object (Algorithm used is set in Cache object constructor)
|
|
1828
|
+
* @example
|
|
1829
|
+
* // Hash a simple object
|
|
1830
|
+
* const connection = { host: 'api.example.com', path: '/users' };
|
|
1831
|
+
* const hash = Cache.generateIdHash(connection);
|
|
1832
|
+
* console.log(hash); // "a1b2c3d4e5f6..."
|
|
1833
|
+
*
|
|
1834
|
+
* @example
|
|
1835
|
+
* // Hash a complex request object
|
|
1836
|
+
* const requestObj = {
|
|
1837
|
+
* query: { type: 'user', id: '123' },
|
|
1838
|
+
* connection: { host: 'api.example.com', path: '/data' },
|
|
1839
|
+
* cachePolicy: { ttl: 300 }
|
|
1840
|
+
* };
|
|
1841
|
+
* const hash = Cache.generateIdHash(requestObj);
|
|
1842
|
+
*
|
|
1843
|
+
* @example
|
|
1844
|
+
* // Hash a simple string ID
|
|
1845
|
+
* const hash = Cache.generateIdHash('MYID-03-88493');
|
|
1292
1846
|
*/
|
|
1293
1847
|
static generateIdHash(idObject) {
|
|
1294
1848
|
|
|
@@ -1346,6 +1900,20 @@ class Cache {
|
|
|
1346
1900
|
* @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
1901
|
* @param {string} glue The glue or delimiter to place between the array elements once it is in string form
|
|
1348
1902
|
* @returns {string} The array in string form delimited by the glue.
|
|
1903
|
+
* @example
|
|
1904
|
+
* // Join array elements with default delimiter
|
|
1905
|
+
* const id = Cache.multipartId(['user', '123', 'profile']);
|
|
1906
|
+
* console.log(id); // "user-123-profile"
|
|
1907
|
+
*
|
|
1908
|
+
* @example
|
|
1909
|
+
* // Use custom delimiter
|
|
1910
|
+
* const id = Cache.multipartId(['api', 'v2', 'users'], '/');
|
|
1911
|
+
* console.log(id); // "api/v2/users"
|
|
1912
|
+
*
|
|
1913
|
+
* @example
|
|
1914
|
+
* // Pass a string (returns unchanged)
|
|
1915
|
+
* const id = Cache.multipartId('already-a-string');
|
|
1916
|
+
* console.log(id); // "already-a-string"
|
|
1349
1917
|
*/
|
|
1350
1918
|
static multipartId (identifierArrayOrString, glue = "-") {
|
|
1351
1919
|
let id = null;
|
|
@@ -1356,11 +1924,22 @@ class Cache {
|
|
|
1356
1924
|
};
|
|
1357
1925
|
|
|
1358
1926
|
/**
|
|
1359
|
-
* Uses Date.parse()
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1363
|
-
|
|
1927
|
+
* Convert a date string to a Unix timestamp in seconds. Uses Date.parse() to
|
|
1928
|
+
* parse the date string and converts the result from milliseconds to seconds.
|
|
1929
|
+
*
|
|
1930
|
+
* This method is useful for converting HTTP date headers (like "Expires" or
|
|
1931
|
+
* "Last-Modified") into Unix timestamps for comparison and calculation.
|
|
1932
|
+
*
|
|
1933
|
+
* @param {string} date Date string to parse (e.g., "2011-10-10T14:48:00" or "Wed, 28 Jul 2021 12:24:11 GMT")
|
|
1934
|
+
* @returns {number} The date in seconds since January 1, 1970, 00:00:00 UTC, or 0 if parsing fails
|
|
1935
|
+
* @example
|
|
1936
|
+
* const timestamp = Cache.parseToSeconds("2021-07-28T12:24:11Z");
|
|
1937
|
+
* console.log(timestamp); // 1627476251
|
|
1938
|
+
*
|
|
1939
|
+
* @example
|
|
1940
|
+
* const timestamp = Cache.parseToSeconds("Wed, 28 Jul 2021 12:24:11 GMT");
|
|
1941
|
+
* console.log(timestamp); // 1627476251
|
|
1942
|
+
*/
|
|
1364
1943
|
static parseToSeconds(date) {
|
|
1365
1944
|
let timestampInSeconds = 0;
|
|
1366
1945
|
try {
|
|
@@ -1392,6 +1971,16 @@ class Cache {
|
|
|
1392
1971
|
* @param {number} intervalInSeconds
|
|
1393
1972
|
* @param {number} timestampInSeconds
|
|
1394
1973
|
* @returns {number} Next interval in seconds
|
|
1974
|
+
* @example
|
|
1975
|
+
* // Calculate next 15-minute interval
|
|
1976
|
+
* const now = Math.floor(Date.now() / 1000);
|
|
1977
|
+
* const next15Min = Cache.nextIntervalInSeconds(15 * 60, now);
|
|
1978
|
+
* console.log(new Date(next15Min * 1000)); // Next 15-minute boundary
|
|
1979
|
+
*
|
|
1980
|
+
* @example
|
|
1981
|
+
* // Calculate next hourly interval
|
|
1982
|
+
* const nextHour = Cache.nextIntervalInSeconds(3600);
|
|
1983
|
+
* console.log(new Date(nextHour * 1000)); // Next hour boundary
|
|
1395
1984
|
*/
|
|
1396
1985
|
static nextIntervalInSeconds(intervalInSeconds, timestampInSeconds = 0) {
|
|
1397
1986
|
return CacheData.nextIntervalInSeconds(intervalInSeconds, timestampInSeconds);
|
|
@@ -1399,23 +1988,48 @@ class Cache {
|
|
|
1399
1988
|
|
|
1400
1989
|
|
|
1401
1990
|
/**
|
|
1402
|
-
* Calculate the
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1991
|
+
* Calculate the size of a string in kilobytes. Uses Buffer.byteLength() to
|
|
1992
|
+
* determine the byte size based on the specified character encoding, then
|
|
1993
|
+
* converts to KB (bytes / 1024).
|
|
1994
|
+
*
|
|
1995
|
+
* This is a convenience wrapper around CacheData.calculateKBytes().
|
|
1996
|
+
*
|
|
1997
|
+
* The result is rounded to 3 decimal places for precision (0.001 KB = 1 byte).
|
|
1998
|
+
*
|
|
1999
|
+
* @param {string} aString The string to measure
|
|
2000
|
+
* @param {string} [encode='utf8'] Character encoding to use for byte calculation
|
|
2001
|
+
* @returns {number} String size in kilobytes, rounded to 3 decimal places
|
|
2002
|
+
* @example
|
|
2003
|
+
* const text = 'Hello, World!';
|
|
2004
|
+
* const sizeKB = Cache.calculateKBytes(text);
|
|
2005
|
+
* console.log(`Size: ${sizeKB} KB`);
|
|
2006
|
+
*
|
|
2007
|
+
* @example
|
|
2008
|
+
* const largeText = 'x'.repeat(10000);
|
|
2009
|
+
* const sizeKB = Cache.calculateKBytes(largeText);
|
|
2010
|
+
* console.log(`Size: ${sizeKB} KB`); // ~9.766 KB
|
|
1408
2011
|
*/
|
|
1409
2012
|
static calculateKBytes ( aString, encode = CacheData.PLAIN_ENCODING ) {
|
|
1410
2013
|
return CacheData.calculateKBytes( aString, encode);
|
|
1411
2014
|
};
|
|
1412
2015
|
|
|
1413
2016
|
/**
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
* for
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
2017
|
+
* Convert a comma-delimited string or array to an array with all lowercase values.
|
|
2018
|
+
* This method is useful for normalizing lists of header names or other identifiers
|
|
2019
|
+
* for case-insensitive comparison.
|
|
2020
|
+
*
|
|
2021
|
+
* If an array is provided, it is first converted to a comma-delimited string,
|
|
2022
|
+
* then lowercased and split back into an array.
|
|
2023
|
+
*
|
|
2024
|
+
* @param {string|Array.<string>} list Comma-delimited string or array to convert
|
|
2025
|
+
* @returns {Array.<string>} Array with all values converted to lowercase
|
|
2026
|
+
* @example
|
|
2027
|
+
* const headers = Cache.convertToLowerCaseArray('Content-Type,Cache-Control,ETag');
|
|
2028
|
+
* console.log(headers); // ['content-type', 'cache-control', 'etag']
|
|
2029
|
+
*
|
|
2030
|
+
* @example
|
|
2031
|
+
* const headers = Cache.convertToLowerCaseArray(['Content-Type', 'Cache-Control']);
|
|
2032
|
+
* console.log(headers); // ['content-type', 'cache-control']
|
|
1419
2033
|
*/
|
|
1420
2034
|
static convertToLowerCaseArray(list) {
|
|
1421
2035
|
|
|
@@ -1429,15 +2043,43 @@ class Cache {
|
|
|
1429
2043
|
};
|
|
1430
2044
|
|
|
1431
2045
|
/**
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
2046
|
+
* Internal method to parse and normalize the headersToRetain configuration.
|
|
2047
|
+
* Converts either a comma-delimited string or array into an array of lowercase
|
|
2048
|
+
* header names that should be retained from the origin response.
|
|
2049
|
+
*
|
|
2050
|
+
* @private
|
|
2051
|
+
* @param {string|Array.<string>} list Comma-delimited string or array of header names
|
|
2052
|
+
* @returns {Array.<string>} Array of lowercase header names to retain
|
|
2053
|
+
* @example
|
|
2054
|
+
* // Called internally during Cache constructor
|
|
2055
|
+
* const headers = this.#parseHeadersToRetain('Content-Type,Cache-Control');
|
|
2056
|
+
* // Returns: ['content-type', 'cache-control']
|
|
1436
2057
|
*/
|
|
1437
2058
|
#parseHeadersToRetain (list) {
|
|
1438
2059
|
return Cache.convertToLowerCaseArray(list);
|
|
1439
2060
|
};
|
|
1440
2061
|
|
|
2062
|
+
/**
|
|
2063
|
+
* Get the cache profile configuration for this Cache instance.
|
|
2064
|
+
* Returns an object containing all the cache policy settings that were
|
|
2065
|
+
* configured when this Cache object was created.
|
|
2066
|
+
*
|
|
2067
|
+
* @returns {{
|
|
2068
|
+
* overrideOriginHeaderExpiration: boolean,
|
|
2069
|
+
* defaultExpirationInSeconds: number,
|
|
2070
|
+
* defaultExpirationExtensionOnErrorInSeconds: number,
|
|
2071
|
+
* expirationIsOnInterval: boolean,
|
|
2072
|
+
* headersToRetain: Array<string>,
|
|
2073
|
+
* hostId: string,
|
|
2074
|
+
* pathId: string,
|
|
2075
|
+
* encrypt: boolean
|
|
2076
|
+
* }} Cache profile configuration object
|
|
2077
|
+
* @example
|
|
2078
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2079
|
+
* const profile = cache.profile();
|
|
2080
|
+
* console.log(`Expiration: ${profile.defaultExpirationInSeconds}s`);
|
|
2081
|
+
* console.log(`Encrypted: ${profile.encrypt}`);
|
|
2082
|
+
*/
|
|
1441
2083
|
profile () {
|
|
1442
2084
|
return {
|
|
1443
2085
|
overrideOriginHeaderExpiration: this.#overrideOriginHeaderExpiration,
|
|
@@ -1452,8 +2094,25 @@ class Cache {
|
|
|
1452
2094
|
};
|
|
1453
2095
|
|
|
1454
2096
|
/**
|
|
2097
|
+
* Read cached data from storage (DynamoDB and potentially S3). This method
|
|
2098
|
+
* first checks the in-memory cache (if enabled), then falls back to DynamoDB.
|
|
2099
|
+
*
|
|
2100
|
+
* If data is found in the in-memory cache and not expired, it is returned
|
|
2101
|
+
* immediately. Otherwise, data is fetched from DynamoDB and stored in the
|
|
2102
|
+
* in-memory cache for future requests.
|
|
2103
|
+
*
|
|
2104
|
+
* This method is called automatically by CacheableDataAccess.getData() and
|
|
2105
|
+
* typically does not need to be called directly.
|
|
1455
2106
|
*
|
|
1456
|
-
* @returns {Promise<CacheDataFormat>}
|
|
2107
|
+
* @returns {Promise<CacheDataFormat>} Formatted cache data with body, headers, expires, and statusCode
|
|
2108
|
+
* @example
|
|
2109
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2110
|
+
* await cache.read();
|
|
2111
|
+
* if (cache.needsRefresh()) {
|
|
2112
|
+
* console.log('Cache miss or expired');
|
|
2113
|
+
* } else {
|
|
2114
|
+
* console.log('Cache hit:', cache.getBody());
|
|
2115
|
+
* }
|
|
1457
2116
|
*/
|
|
1458
2117
|
async read () {
|
|
1459
2118
|
|
|
@@ -1463,15 +2122,63 @@ class Cache {
|
|
|
1463
2122
|
resolve(this.#store);
|
|
1464
2123
|
} else {
|
|
1465
2124
|
try {
|
|
2125
|
+
let staleData = null;
|
|
2126
|
+
|
|
2127
|
+
// Check L0_Cache if feature is enabled
|
|
2128
|
+
if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null) {
|
|
2129
|
+
const memResult = Cache.#inMemoryCache.get(this.#idHash);
|
|
2130
|
+
|
|
2131
|
+
if (memResult.cache === 1) {
|
|
2132
|
+
// Cache hit - return immediately
|
|
2133
|
+
this.#store = memResult.data;
|
|
2134
|
+
this.#status = Cache.STATUS_CACHE_IN_MEM;
|
|
2135
|
+
tools.DebugAndLog.debug(`In-memory cache hit: ${this.#idHash}`);
|
|
2136
|
+
resolve(this.#store);
|
|
2137
|
+
return;
|
|
2138
|
+
} else if (memResult.cache === -1) {
|
|
2139
|
+
// Expired - retain for potential fallback
|
|
2140
|
+
staleData = memResult.data;
|
|
2141
|
+
tools.DebugAndLog.debug(`In-memory cache expired, retaining stale data: ${this.#idHash}`);
|
|
2142
|
+
}
|
|
2143
|
+
// cache === 0 means miss, continue to DynamoDB
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// Fetch from DynamoDB
|
|
1466
2147
|
this.#store = await CacheData.read(this.#idHash, this.#syncedLaterTimestampInSeconds);
|
|
1467
2148
|
this.#status = ( this.#store.cache.statusCode === null ) ? Cache.STATUS_NO_CACHE : Cache.STATUS_CACHE;
|
|
2149
|
+
|
|
2150
|
+
// Store in L0_Cache if successful and feature enabled
|
|
2151
|
+
if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null && this.#store.cache.statusCode !== null) {
|
|
2152
|
+
const expiresAt = this.#store.cache.expires * 1000; // Convert to milliseconds
|
|
2153
|
+
Cache.#inMemoryCache.set(this.#idHash, this.#store, expiresAt);
|
|
2154
|
+
tools.DebugAndLog.debug(`Stored in L0_Cache: ${this.#idHash}`);
|
|
2155
|
+
}
|
|
1468
2156
|
|
|
1469
2157
|
tools.DebugAndLog.debug(`Cache Read status: ${this.#status}`);
|
|
1470
2158
|
|
|
1471
2159
|
resolve(this.#store);
|
|
1472
2160
|
} catch (error) {
|
|
1473
|
-
|
|
1474
|
-
|
|
2161
|
+
// Error occurred - check if we have stale data to return
|
|
2162
|
+
if (staleData !== null) {
|
|
2163
|
+
// Calculate new expiration using error extension
|
|
2164
|
+
const newExpires = this.#syncedNowTimestampInSeconds + this.#defaultExpirationExtensionOnErrorInSeconds;
|
|
2165
|
+
const newExpiresAt = newExpires * 1000;
|
|
2166
|
+
|
|
2167
|
+
// Update stale data expiration
|
|
2168
|
+
staleData.cache.expires = newExpires;
|
|
2169
|
+
|
|
2170
|
+
// Store updated stale data back in L0_Cache
|
|
2171
|
+
if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null) {
|
|
2172
|
+
Cache.#inMemoryCache.set(this.#idHash, staleData, newExpiresAt);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
this.#store = staleData;
|
|
2176
|
+
this.#status = Cache.STATUS_CACHE_ERROR;
|
|
2177
|
+
tools.DebugAndLog.warn(`Returning stale data due to error: ${this.#idHash}`);
|
|
2178
|
+
} else {
|
|
2179
|
+
this.#store = CacheData.format(this.#syncedLaterTimestampInSeconds);
|
|
2180
|
+
this.#status = Cache.STATUS_CACHE_ERROR;
|
|
2181
|
+
}
|
|
1475
2182
|
|
|
1476
2183
|
tools.DebugAndLog.error(`Cache Read: Cannot read cached data for ${this.#idHash}: ${error?.message || 'Unknown error'}`, error?.stack);
|
|
1477
2184
|
|
|
@@ -1483,6 +2190,39 @@ class Cache {
|
|
|
1483
2190
|
|
|
1484
2191
|
};
|
|
1485
2192
|
|
|
2193
|
+
/**
|
|
2194
|
+
* Get diagnostic test data for this Cache instance. Returns an object containing
|
|
2195
|
+
* the results of calling various getter methods, useful for debugging and
|
|
2196
|
+
* verifying cache state.
|
|
2197
|
+
*
|
|
2198
|
+
* This method is primarily for testing and debugging purposes.
|
|
2199
|
+
*
|
|
2200
|
+
* @returns {{
|
|
2201
|
+
* get: CacheDataFormat,
|
|
2202
|
+
* getStatus: string,
|
|
2203
|
+
* getETag: string,
|
|
2204
|
+
* getLastModified: string,
|
|
2205
|
+
* getExpires: number,
|
|
2206
|
+
* getExpiresGMT: string,
|
|
2207
|
+
* getHeaders: Object,
|
|
2208
|
+
* getSyncedNowTimestampInSeconds: number,
|
|
2209
|
+
* getBody: string,
|
|
2210
|
+
* getIdHash: string,
|
|
2211
|
+
* getClassification: string,
|
|
2212
|
+
* needsRefresh: boolean,
|
|
2213
|
+
* isExpired: boolean,
|
|
2214
|
+
* isEmpty: boolean,
|
|
2215
|
+
* isPrivate: boolean,
|
|
2216
|
+
* isPublic: boolean,
|
|
2217
|
+
* currentStatus: string,
|
|
2218
|
+
* calculateDefaultExpires: number
|
|
2219
|
+
* }} Object containing test data from various cache methods
|
|
2220
|
+
* @example
|
|
2221
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2222
|
+
* await cache.read();
|
|
2223
|
+
* const testData = cache.test();
|
|
2224
|
+
* console.log('Cache test data:', testData);
|
|
2225
|
+
*/
|
|
1486
2226
|
test() {
|
|
1487
2227
|
return {
|
|
1488
2228
|
get: this.get(),
|
|
@@ -1507,40 +2247,98 @@ class Cache {
|
|
|
1507
2247
|
};
|
|
1508
2248
|
|
|
1509
2249
|
/**
|
|
2250
|
+
* Get the complete cache data object. Returns the internal cache store
|
|
2251
|
+
* containing the cached body, headers, expiration, and status code.
|
|
1510
2252
|
*
|
|
1511
|
-
* @returns {CacheDataFormat}
|
|
2253
|
+
* @returns {CacheDataFormat} The complete cache data object
|
|
2254
|
+
* @example
|
|
2255
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2256
|
+
* await cache.read();
|
|
2257
|
+
* const data = cache.get();
|
|
2258
|
+
* console.log(data.cache.body);
|
|
2259
|
+
* console.log(data.cache.expires);
|
|
1512
2260
|
*/
|
|
1513
2261
|
get() {
|
|
1514
2262
|
return this.#store;
|
|
1515
2263
|
};
|
|
1516
2264
|
|
|
1517
2265
|
/**
|
|
2266
|
+
* Get the source status of the cache data. Returns a status string indicating
|
|
2267
|
+
* where the data came from and its current state.
|
|
2268
|
+
*
|
|
2269
|
+
* Possible values include:
|
|
2270
|
+
* - Cache.STATUS_NO_CACHE: No cached data exists
|
|
2271
|
+
* - Cache.STATUS_CACHE: Data from cache
|
|
2272
|
+
* - Cache.STATUS_CACHE_IN_MEM: Data from in-memory cache
|
|
2273
|
+
* - Cache.STATUS_EXPIRED: Cached data was expired
|
|
2274
|
+
* - Cache.STATUS_CACHE_SAME: Cache updated but content unchanged
|
|
2275
|
+
* - Cache.STATUS_ORIGINAL_NOT_MODIFIED: Origin returned 304 Not Modified
|
|
2276
|
+
* - Cache.STATUS_ORIGINAL_ERROR: Error fetching from origin
|
|
2277
|
+
* - Cache.STATUS_CACHE_ERROR: Error reading from cache
|
|
2278
|
+
* - Cache.STATUS_FORCED: Cache update was forced
|
|
1518
2279
|
*
|
|
1519
|
-
* @returns {string}
|
|
2280
|
+
* @returns {string} The source status string
|
|
2281
|
+
* @example
|
|
2282
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2283
|
+
* await cache.read();
|
|
2284
|
+
* console.log(`Data source: ${cache.getSourceStatus()}`);
|
|
1520
2285
|
*/
|
|
1521
2286
|
getSourceStatus() {
|
|
1522
2287
|
return this.#status;
|
|
1523
2288
|
};
|
|
1524
2289
|
|
|
1525
2290
|
/**
|
|
2291
|
+
* Get the ETag header value from the cached data. The ETag is used for
|
|
2292
|
+
* cache validation and conditional requests.
|
|
1526
2293
|
*
|
|
1527
|
-
*
|
|
2294
|
+
* Returns null if the ETag header is not present, is null, or is undefined.
|
|
2295
|
+
* This method uses getHeader() internally, which normalizes undefined values
|
|
2296
|
+
* to null for consistent behavior.
|
|
2297
|
+
*
|
|
2298
|
+
* @returns {string|null} The ETag value, or null if not present, null, or undefined
|
|
2299
|
+
* @example
|
|
2300
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2301
|
+
* await cache.read();
|
|
2302
|
+
* const etag = cache.getETag();
|
|
2303
|
+
* if (etag !== null) {
|
|
2304
|
+
* console.log(`ETag: ${etag}`);
|
|
2305
|
+
* }
|
|
1528
2306
|
*/
|
|
1529
2307
|
getETag() {
|
|
1530
2308
|
return this.getHeader("etag");
|
|
1531
2309
|
};
|
|
1532
2310
|
|
|
1533
2311
|
/**
|
|
2312
|
+
* Get the Last-Modified header value from the cached data. This timestamp
|
|
2313
|
+
* indicates when the cached content was last modified at the origin.
|
|
1534
2314
|
*
|
|
1535
|
-
*
|
|
2315
|
+
* Returns null if the Last-Modified header is not present, is null, or is undefined.
|
|
2316
|
+
* This method uses getHeader() internally, which normalizes undefined values
|
|
2317
|
+
* to null for consistent behavior.
|
|
2318
|
+
*
|
|
2319
|
+
* @returns {string|null} The Last-Modified header value in HTTP date format, or null if not present, null, or undefined
|
|
2320
|
+
* @example
|
|
2321
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2322
|
+
* await cache.read();
|
|
2323
|
+
* const lastModified = cache.getLastModified();
|
|
2324
|
+
* if (lastModified !== null) {
|
|
2325
|
+
* console.log(`Last modified: ${lastModified}`);
|
|
2326
|
+
* }
|
|
1536
2327
|
*/
|
|
1537
2328
|
getLastModified() {
|
|
1538
2329
|
return this.getHeader("last-modified");
|
|
1539
2330
|
};
|
|
1540
2331
|
|
|
1541
2332
|
/**
|
|
2333
|
+
* Get the expiration timestamp of the cached data. Returns the Unix timestamp
|
|
2334
|
+
* (in seconds) when the cached data will expire.
|
|
1542
2335
|
*
|
|
1543
|
-
* @returns {number} Expiration timestamp in seconds
|
|
2336
|
+
* @returns {number} Expiration timestamp in seconds since Unix epoch, or 0 if no cache data
|
|
2337
|
+
* @example
|
|
2338
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2339
|
+
* await cache.read();
|
|
2340
|
+
* const expires = cache.getExpires();
|
|
2341
|
+
* console.log(`Expires at: ${new Date(expires * 1000)}`);
|
|
1544
2342
|
*/
|
|
1545
2343
|
getExpires() {
|
|
1546
2344
|
let exp = (this.#store !== null) ? this.#store.cache.expires : 0;
|
|
@@ -1553,14 +2351,26 @@ class Cache {
|
|
|
1553
2351
|
* Example: "Wed, 28 Jul 2021 12:24:11 GMT"
|
|
1554
2352
|
*
|
|
1555
2353
|
* @returns {string} The expiration formated for use in headers. Same as expires header.
|
|
2354
|
+
* @example
|
|
2355
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2356
|
+
* await cache.read();
|
|
2357
|
+
* const expiresGMT = cache.getExpiresGMT();
|
|
2358
|
+
* console.log(expiresGMT); // "Wed, 28 Jul 2021 12:24:11 GMT"
|
|
1556
2359
|
*/
|
|
1557
2360
|
getExpiresGMT() {
|
|
1558
2361
|
return this.getHeader("expires");
|
|
1559
2362
|
};
|
|
1560
2363
|
|
|
1561
2364
|
/**
|
|
2365
|
+
* Calculate the number of seconds remaining until the cached data expires.
|
|
2366
|
+
* Returns 0 if the cache is already expired or if there is no cached data.
|
|
1562
2367
|
*
|
|
1563
|
-
* @returns {number} The
|
|
2368
|
+
* @returns {number} The number of seconds until expiration, or 0 if expired
|
|
2369
|
+
* @example
|
|
2370
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2371
|
+
* await cache.read();
|
|
2372
|
+
* const secondsLeft = cache.calculateSecondsLeftUntilExpires();
|
|
2373
|
+
* console.log(`Cache expires in ${secondsLeft} seconds`);
|
|
1564
2374
|
*/
|
|
1565
2375
|
calculateSecondsLeftUntilExpires() {
|
|
1566
2376
|
let secondsLeftUntilExpires = this.getExpires() - CacheData.convertTimestampFromMilliToSeconds( Date.now() );
|
|
@@ -1570,69 +2380,164 @@ class Cache {
|
|
|
1570
2380
|
};
|
|
1571
2381
|
|
|
1572
2382
|
/**
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
2383
|
+
* Generate the value for the Cache-Control HTTP header. Returns a string
|
|
2384
|
+
* containing the classification (public or private) and the max-age directive
|
|
2385
|
+
* based on the time remaining until expiration.
|
|
2386
|
+
*
|
|
2387
|
+
* Example output: "public, max-age=3600" or "private, max-age=300"
|
|
2388
|
+
*
|
|
2389
|
+
* @returns {string} The Cache-Control header value
|
|
2390
|
+
* @example
|
|
2391
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2392
|
+
* await cache.read();
|
|
2393
|
+
* const cacheControl = cache.getCacheControlHeaderValue();
|
|
2394
|
+
* console.log(cacheControl); // "public, max-age=3600"
|
|
1575
2395
|
*/
|
|
1576
2396
|
getCacheControlHeaderValue() {
|
|
1577
2397
|
return this.getClassification() +", max-age="+this.calculateSecondsLeftUntilExpires();
|
|
1578
2398
|
};
|
|
1579
2399
|
|
|
1580
2400
|
/**
|
|
2401
|
+
* Get all HTTP headers from the cached data. Returns an object containing
|
|
2402
|
+
* all header key-value pairs that were stored with the cached content.
|
|
1581
2403
|
*
|
|
1582
|
-
* @returns {
|
|
2404
|
+
* @returns {Object|null} Object containing all cached headers, or null if no cache data
|
|
2405
|
+
* @example
|
|
2406
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2407
|
+
* await cache.read();
|
|
2408
|
+
* const headers = cache.getHeaders();
|
|
2409
|
+
* console.log(headers['content-type']);
|
|
2410
|
+
* console.log(headers['etag']);
|
|
1583
2411
|
*/
|
|
1584
2412
|
getHeaders() {
|
|
1585
2413
|
return (this.#store !== null && "headers" in this.#store.cache) ? this.#store.cache.headers : null;
|
|
1586
2414
|
};
|
|
1587
2415
|
|
|
1588
2416
|
/**
|
|
2417
|
+
* Get the HTTP status code from the cached data. Returns the status code
|
|
2418
|
+
* that was stored when the data was originally cached.
|
|
1589
2419
|
*
|
|
1590
|
-
* @returns {string|null} The status code
|
|
2420
|
+
* @returns {string|null} The HTTP status code (e.g., "200", "404"), or null if no cache data
|
|
2421
|
+
* @example
|
|
2422
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2423
|
+
* await cache.read();
|
|
2424
|
+
* const statusCode = cache.getStatusCode();
|
|
2425
|
+
* console.log(`Status: ${statusCode}`); // "200"
|
|
1591
2426
|
*/
|
|
1592
2427
|
getStatusCode() {
|
|
1593
2428
|
return (this.#store !== null && "statusCode" in this.#store.cache) ? this.#store.cache.statusCode : null;
|
|
1594
2429
|
};
|
|
1595
2430
|
|
|
1596
2431
|
/**
|
|
2432
|
+
* Get the current error code for this cache instance. Returns a non-zero
|
|
2433
|
+
* error code if an error occurred during cache operations, or 0 if no error.
|
|
1597
2434
|
*
|
|
1598
|
-
*
|
|
2435
|
+
* Error codes are typically HTTP status codes (400+) that were encountered
|
|
2436
|
+
* when trying to refresh the cache from the origin.
|
|
2437
|
+
*
|
|
2438
|
+
* @returns {number} The error code, or 0 if no error
|
|
2439
|
+
* @example
|
|
2440
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2441
|
+
* await cache.read();
|
|
2442
|
+
* const errorCode = cache.getErrorCode();
|
|
2443
|
+
* if (errorCode >= 400) {
|
|
2444
|
+
* console.log(`Error occurred: ${errorCode}`);
|
|
2445
|
+
* }
|
|
1599
2446
|
*/
|
|
1600
2447
|
getErrorCode() {
|
|
1601
2448
|
return this.#errorCode;
|
|
1602
2449
|
};
|
|
1603
2450
|
|
|
1604
2451
|
/**
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
2452
|
+
* Get the classification of the cached data. Returns "private" if the data
|
|
2453
|
+
* is encrypted, or "public" if not encrypted.
|
|
2454
|
+
*
|
|
2455
|
+
* This classification is used in the Cache-Control header to indicate how
|
|
2456
|
+
* the data should be treated by intermediate caches and proxies.
|
|
2457
|
+
*
|
|
2458
|
+
* @returns {string} Either "private" (encrypted) or "public" (not encrypted)
|
|
2459
|
+
* @example
|
|
2460
|
+
* const cache = new Cache(connection, { encrypt: true });
|
|
2461
|
+
* console.log(cache.getClassification()); // "private"
|
|
2462
|
+
*
|
|
2463
|
+
* const publicCache = new Cache(connection, { encrypt: false });
|
|
2464
|
+
* console.log(publicCache.getClassification()); // "public"
|
|
1609
2465
|
*/
|
|
1610
2466
|
getClassification() {
|
|
1611
2467
|
return (this.#encrypt ? Cache.PRIVATE : Cache.PUBLIC );
|
|
1612
2468
|
};
|
|
1613
2469
|
|
|
1614
2470
|
/**
|
|
2471
|
+
* Get the synchronized "now" timestamp in seconds. This timestamp is set when
|
|
2472
|
+
* the Cache object is created and remains constant throughout the cache operation,
|
|
2473
|
+
* providing a consistent time base for expiration calculations.
|
|
1615
2474
|
*
|
|
1616
|
-
* @returns {number} The timestamp in seconds
|
|
2475
|
+
* @returns {number} The timestamp in seconds when this Cache object was created
|
|
2476
|
+
* @example
|
|
2477
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2478
|
+
* const now = cache.getSyncedNowTimestampInSeconds();
|
|
2479
|
+
* console.log(`Cache created at: ${new Date(now * 1000)}`);
|
|
1617
2480
|
*/
|
|
1618
2481
|
getSyncedNowTimestampInSeconds() {
|
|
1619
2482
|
return this.#syncedNowTimestampInSeconds;
|
|
1620
2483
|
};
|
|
1621
2484
|
|
|
1622
2485
|
/**
|
|
2486
|
+
* Get a specific header value from the cached data by key. Header keys are
|
|
2487
|
+
* case-insensitive (stored as lowercase).
|
|
2488
|
+
*
|
|
2489
|
+
* Returns null if the header doesn't exist, has a null value, or has an undefined value.
|
|
2490
|
+
* This normalization ensures consistent behavior for conditional checks and prevents
|
|
2491
|
+
* undefined values from being passed to HTTP headers.
|
|
2492
|
+
*
|
|
2493
|
+
* Note: This method normalizes undefined to null. If a header key exists but has an
|
|
2494
|
+
* undefined value, this method returns null (never undefined). This ensures that
|
|
2495
|
+
* conditional checks like `!== null` work reliably.
|
|
2496
|
+
*
|
|
2497
|
+
* @param {string} key The header key to retrieve (case-insensitive)
|
|
2498
|
+
* @returns {string|number|null} The header value, or null if the header doesn't exist, is null, or is undefined
|
|
2499
|
+
* @example
|
|
2500
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2501
|
+
* await cache.read();
|
|
2502
|
+
* const contentType = cache.getHeader('content-type');
|
|
2503
|
+
* const etag = cache.getHeader('ETag'); // Case-insensitive
|
|
2504
|
+
* console.log(`Content-Type: ${contentType}`);
|
|
1623
2505
|
*
|
|
1624
|
-
* @
|
|
1625
|
-
*
|
|
2506
|
+
* @example
|
|
2507
|
+
* // Undefined values are normalized to null
|
|
2508
|
+
* const undefinedHeader = cache.getHeader('missing-header');
|
|
2509
|
+
* console.log(undefinedHeader === null); // true (never undefined)
|
|
1626
2510
|
*/
|
|
1627
2511
|
getHeader(key) {
|
|
1628
2512
|
let headers = this.getHeaders();
|
|
1629
|
-
|
|
2513
|
+
if (headers === null || !(key in headers)) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
// Normalize undefined to null for consistent behavior
|
|
2517
|
+
const value = headers[key];
|
|
2518
|
+
return (value === undefined || value === null) ? null : value;
|
|
1630
2519
|
};
|
|
1631
2520
|
|
|
1632
2521
|
/**
|
|
2522
|
+
* Get the cached body content. Optionally parses JSON content into an object.
|
|
2523
|
+
*
|
|
2524
|
+
* By default, returns the body as a string (which may be JSON-encoded). If
|
|
2525
|
+
* parseBody is true, attempts to parse the body as JSON and return an object.
|
|
2526
|
+
* If parsing fails, returns the original string.
|
|
2527
|
+
*
|
|
2528
|
+
* @param {boolean} [parseBody=false] If true, parse JSON body into an object
|
|
2529
|
+
* @returns {string|Object|null} The body as a string, parsed object, or null if no cache data
|
|
2530
|
+
* @example
|
|
2531
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2532
|
+
* await cache.read();
|
|
1633
2533
|
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
2534
|
+
* // Get body as string
|
|
2535
|
+
* const bodyStr = cache.getBody();
|
|
2536
|
+
* console.log(bodyStr); // '{"data":"value"}'
|
|
2537
|
+
*
|
|
2538
|
+
* // Get body as parsed object
|
|
2539
|
+
* const bodyObj = cache.getBody(true);
|
|
2540
|
+
* console.log(bodyObj.data); // 'value'
|
|
1636
2541
|
*/
|
|
1637
2542
|
getBody(parseBody = false) {
|
|
1638
2543
|
let body = (this.#store !== null) ? this.#store.cache.body : null;
|
|
@@ -1649,10 +2554,20 @@ class Cache {
|
|
|
1649
2554
|
};
|
|
1650
2555
|
|
|
1651
2556
|
/**
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
2557
|
+
* Get a plain data response object containing the status code, headers, and body.
|
|
2558
|
+
* This is a simplified response format suitable for internal use.
|
|
2559
|
+
*
|
|
2560
|
+
* For a full HTTP response suitable for API Gateway, use generateResponseForAPIGateway() instead.
|
|
2561
|
+
*
|
|
2562
|
+
* @param {boolean} [parseBody=false] If true, parse JSON body into an object
|
|
2563
|
+
* @returns {{statusCode: string, headers: Object, body: string|Object}|null} Response object, or null if no cache data
|
|
2564
|
+
* @example
|
|
2565
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2566
|
+
* await cache.read();
|
|
2567
|
+
* const response = cache.getResponse();
|
|
2568
|
+
* console.log(response.statusCode); // "200"
|
|
2569
|
+
* console.log(response.headers['content-type']);
|
|
2570
|
+
* console.log(response.body);
|
|
1656
2571
|
*/
|
|
1657
2572
|
getResponse(parseBody = false) {
|
|
1658
2573
|
let response = null;
|
|
@@ -1669,9 +2584,28 @@ class Cache {
|
|
|
1669
2584
|
};
|
|
1670
2585
|
|
|
1671
2586
|
/**
|
|
2587
|
+
* Generate a complete HTTP response suitable for AWS API Gateway. This method
|
|
2588
|
+
* adds standard headers (CORS, Cache-Control, data source), handles conditional
|
|
2589
|
+
* requests (If-None-Match, If-Modified-Since), and formats the response according
|
|
2590
|
+
* to API Gateway requirements.
|
|
2591
|
+
*
|
|
2592
|
+
* If the client's ETag or Last-Modified matches the cached data, returns a
|
|
2593
|
+
* 304 Not Modified response with no body.
|
|
1672
2594
|
*
|
|
1673
|
-
* @param {
|
|
1674
|
-
* @
|
|
2595
|
+
* @param {Object} parameters Configuration parameters for the response
|
|
2596
|
+
* @param {string} [parameters.ifNoneMatch] The If-None-Match header value from the client request
|
|
2597
|
+
* @param {string} [parameters.ifModifiedSince] The If-Modified-Since header value from the client request
|
|
2598
|
+
* @returns {{statusCode: string, headers: Object, body: string|null}} Complete HTTP response for API Gateway
|
|
2599
|
+
* @example
|
|
2600
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2601
|
+
* await cache.read();
|
|
2602
|
+
*
|
|
2603
|
+
* const response = cache.generateResponseForAPIGateway({
|
|
2604
|
+
* ifNoneMatch: event.headers['if-none-match'],
|
|
2605
|
+
* ifModifiedSince: event.headers['if-modified-since']
|
|
2606
|
+
* });
|
|
2607
|
+
*
|
|
2608
|
+
* return response; // Return to API Gateway
|
|
1675
2609
|
*/
|
|
1676
2610
|
generateResponseForAPIGateway( parameters ) {
|
|
1677
2611
|
|
|
@@ -1715,59 +2649,121 @@ class Cache {
|
|
|
1715
2649
|
};
|
|
1716
2650
|
|
|
1717
2651
|
/**
|
|
2652
|
+
* Get the unique identifier hash for this cache entry. This hash is generated
|
|
2653
|
+
* from the connection, data, and cache policy parameters and uniquely identifies
|
|
2654
|
+
* this specific cache entry.
|
|
1718
2655
|
*
|
|
1719
|
-
* @returns {string}
|
|
2656
|
+
* @returns {string} The unique cache identifier hash
|
|
2657
|
+
* @example
|
|
2658
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2659
|
+
* const idHash = cache.getIdHash();
|
|
2660
|
+
* console.log(`Cache ID: ${idHash}`);
|
|
1720
2661
|
*/
|
|
1721
2662
|
getIdHash() {
|
|
1722
2663
|
return this.#idHash;
|
|
1723
2664
|
};
|
|
1724
2665
|
|
|
1725
2666
|
/**
|
|
2667
|
+
* Check if the cache needs to be refreshed. Returns true if the cache is
|
|
2668
|
+
* expired or empty, indicating that fresh data should be fetched from the origin.
|
|
1726
2669
|
*
|
|
1727
|
-
* @returns {boolean}
|
|
2670
|
+
* @returns {boolean} True if cache needs refresh, false otherwise
|
|
2671
|
+
* @example
|
|
2672
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2673
|
+
* await cache.read();
|
|
2674
|
+
* if (cache.needsRefresh()) {
|
|
2675
|
+
* console.log('Fetching fresh data from origin');
|
|
2676
|
+
* // Fetch and update cache
|
|
2677
|
+
* }
|
|
1728
2678
|
*/
|
|
1729
2679
|
needsRefresh() {
|
|
1730
2680
|
return (this.isExpired() || this.isEmpty());
|
|
1731
2681
|
};
|
|
1732
2682
|
|
|
1733
2683
|
/**
|
|
2684
|
+
* Check if the cached data has expired. Compares the expiration timestamp
|
|
2685
|
+
* with the current time to determine if the cache is still valid.
|
|
1734
2686
|
*
|
|
1735
|
-
* @returns {boolean}
|
|
2687
|
+
* @returns {boolean} True if cache is expired, false if still valid
|
|
2688
|
+
* @example
|
|
2689
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2690
|
+
* await cache.read();
|
|
2691
|
+
* if (cache.isExpired()) {
|
|
2692
|
+
* console.log('Cache has expired');
|
|
2693
|
+
* }
|
|
1736
2694
|
*/
|
|
1737
2695
|
isExpired() {
|
|
1738
2696
|
return ( CacheData.convertTimestampFromSecondsToMilli(this.getExpires()) <= Date.now());
|
|
1739
2697
|
};
|
|
1740
2698
|
|
|
1741
2699
|
/**
|
|
2700
|
+
* Check if the cache is empty (no cached data exists). Returns true if there
|
|
2701
|
+
* is no cached data, indicating this is a cache miss.
|
|
1742
2702
|
*
|
|
1743
|
-
* @returns {boolean}
|
|
2703
|
+
* @returns {boolean} True if cache is empty, false if data exists
|
|
2704
|
+
* @example
|
|
2705
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2706
|
+
* await cache.read();
|
|
2707
|
+
* if (cache.isEmpty()) {
|
|
2708
|
+
* console.log('Cache miss - no data found');
|
|
2709
|
+
* }
|
|
1744
2710
|
*/
|
|
1745
2711
|
isEmpty() {
|
|
1746
2712
|
return (this.#store.cache.statusCode === null);
|
|
1747
2713
|
};
|
|
1748
2714
|
|
|
1749
2715
|
/**
|
|
2716
|
+
* Check if the cache is configured for private (encrypted) storage.
|
|
2717
|
+
* Private caches encrypt data at rest for sensitive content.
|
|
1750
2718
|
*
|
|
1751
|
-
* @returns {boolean}
|
|
2719
|
+
* @returns {boolean} True if cache is private/encrypted, false otherwise
|
|
2720
|
+
* @example
|
|
2721
|
+
* const cache = new Cache(connection, { encrypt: true });
|
|
2722
|
+
* if (cache.isPrivate()) {
|
|
2723
|
+
* console.log('Cache data is encrypted');
|
|
2724
|
+
* }
|
|
1752
2725
|
*/
|
|
1753
2726
|
isPrivate() {
|
|
1754
2727
|
return this.#encrypt;
|
|
1755
2728
|
};
|
|
1756
2729
|
|
|
1757
2730
|
/**
|
|
2731
|
+
* Check if the cache is configured for public (non-encrypted) storage.
|
|
2732
|
+
* Public caches do not encrypt data at rest.
|
|
1758
2733
|
*
|
|
1759
|
-
* @returns {boolean}
|
|
2734
|
+
* @returns {boolean} True if cache is public/non-encrypted, false otherwise
|
|
2735
|
+
* @example
|
|
2736
|
+
* const cache = new Cache(connection, { encrypt: false });
|
|
2737
|
+
* if (cache.isPublic()) {
|
|
2738
|
+
* console.log('Cache data is not encrypted');
|
|
2739
|
+
* }
|
|
1760
2740
|
*/
|
|
1761
2741
|
isPublic() {
|
|
1762
2742
|
return !this.#encrypt;
|
|
1763
2743
|
};
|
|
1764
2744
|
|
|
1765
2745
|
/**
|
|
2746
|
+
* Extend the expiration time of the cached data. This method is used when
|
|
2747
|
+
* the origin returns a 304 Not Modified response or when an error occurs
|
|
2748
|
+
* fetching fresh data.
|
|
2749
|
+
*
|
|
2750
|
+
* If an error occurred (reason is STATUS_ORIGINAL_ERROR), the cache is extended
|
|
2751
|
+
* by defaultExpirationExtensionOnErrorInSeconds. Otherwise, it's extended by
|
|
2752
|
+
* the specified seconds or the default expiration time.
|
|
1766
2753
|
*
|
|
1767
|
-
* @param {string} reason Reason for extending
|
|
1768
|
-
* @param {number} seconds
|
|
1769
|
-
* @param {number} errorCode
|
|
1770
|
-
* @returns {Promise<boolean>}
|
|
2754
|
+
* @param {string} reason Reason for extending: Cache.STATUS_ORIGINAL_ERROR or Cache.STATUS_ORIGINAL_NOT_MODIFIED
|
|
2755
|
+
* @param {number} [seconds=0] Number of seconds to extend (0 = use default)
|
|
2756
|
+
* @param {number} [errorCode=0] HTTP error code if extending due to error
|
|
2757
|
+
* @returns {Promise<boolean>} True if extension was successful, false otherwise
|
|
2758
|
+
* @example
|
|
2759
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2760
|
+
* await cache.read();
|
|
2761
|
+
*
|
|
2762
|
+
* // Extend due to 304 Not Modified
|
|
2763
|
+
* await cache.extendExpires(Cache.STATUS_ORIGINAL_NOT_MODIFIED);
|
|
2764
|
+
*
|
|
2765
|
+
* // Extend due to error
|
|
2766
|
+
* await cache.extendExpires(Cache.STATUS_ORIGINAL_ERROR, 0, 500);
|
|
1771
2767
|
*/
|
|
1772
2768
|
async extendExpires(reason, seconds = 0, errorCode = 0) {
|
|
1773
2769
|
|
|
@@ -1821,28 +2817,65 @@ class Cache {
|
|
|
1821
2817
|
};
|
|
1822
2818
|
|
|
1823
2819
|
/**
|
|
2820
|
+
* Calculate the default expiration timestamp for cached data. If expiration
|
|
2821
|
+
* is configured to use intervals, calculates the next interval boundary.
|
|
2822
|
+
* Otherwise, returns the synced later timestamp (now + default expiration).
|
|
1824
2823
|
*
|
|
1825
|
-
* @returns {number}
|
|
2824
|
+
* @returns {number} The default expiration timestamp in seconds
|
|
2825
|
+
* @example
|
|
2826
|
+
* const cache = new Cache(connection, { expirationIsOnInterval: true, defaultExpirationInSeconds: 3600 });
|
|
2827
|
+
* const expires = cache.calculateDefaultExpires();
|
|
2828
|
+
* console.log(`Default expiration: ${new Date(expires * 1000)}`);
|
|
1826
2829
|
*/
|
|
1827
2830
|
calculateDefaultExpires() {
|
|
1828
2831
|
return (this.#expirationIsOnInterval) ? Cache.nextIntervalInSeconds(this.#defaultExpirationInSeconds, this.#syncedNowTimestampInSeconds) : this.#syncedLaterTimestampInSeconds;
|
|
1829
2832
|
};
|
|
1830
2833
|
|
|
1831
2834
|
/**
|
|
2835
|
+
* Get the current status of the cache. Returns a status string indicating
|
|
2836
|
+
* the source and state of the cached data.
|
|
2837
|
+
*
|
|
2838
|
+
* This is an alias for getSourceStatus().
|
|
1832
2839
|
*
|
|
1833
|
-
* @returns {string}
|
|
2840
|
+
* @returns {string} The cache status string
|
|
2841
|
+
* @example
|
|
2842
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2843
|
+
* await cache.read();
|
|
2844
|
+
* console.log(`Status: ${cache.getStatus()}`);
|
|
1834
2845
|
*/
|
|
1835
2846
|
getStatus() {
|
|
1836
2847
|
return this.#status;
|
|
1837
2848
|
};
|
|
1838
2849
|
|
|
1839
2850
|
/**
|
|
1840
|
-
* Store data in cache.
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
2851
|
+
* Store data in cache. Updates the cache with new content, headers, status code,
|
|
2852
|
+
* and expiration time. This method handles header normalization, expiration
|
|
2853
|
+
* calculation, and writes the data to both DynamoDB and S3 (if needed).
|
|
2854
|
+
*
|
|
2855
|
+
* The method automatically:
|
|
2856
|
+
* - Lowercases all header keys for consistency
|
|
2857
|
+
* - Retains specified headers from the origin response
|
|
2858
|
+
* - Generates ETag and Last-Modified headers if not present
|
|
2859
|
+
* - Calculates expiration based on origin headers or default settings
|
|
2860
|
+
* - Encrypts data if configured as private
|
|
2861
|
+
* - Routes large items to S3 storage
|
|
2862
|
+
*
|
|
2863
|
+
* @param {string|Object} body The content body to cache (will be stringified if object)
|
|
2864
|
+
* @param {Object} headers HTTP headers from the origin response
|
|
2865
|
+
* @param {string|number} [statusCode=200] HTTP status code from the origin
|
|
2866
|
+
* @param {number} [expires=0] Expiration Unix timestamp in seconds (0 = calculate default)
|
|
2867
|
+
* @param {string|null} [status=null] Optional status override (e.g., Cache.STATUS_FORCED)
|
|
1845
2868
|
* @returns {Promise<CacheDataFormat>} Representation of data stored in cache
|
|
2869
|
+
* @example
|
|
2870
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2871
|
+
* await cache.read();
|
|
2872
|
+
*
|
|
2873
|
+
* // Update cache with fresh data
|
|
2874
|
+
* const body = JSON.stringify({ data: 'value' });
|
|
2875
|
+
* const headers = { 'content-type': 'application/json' };
|
|
2876
|
+
* await cache.update(body, headers, 200);
|
|
2877
|
+
*
|
|
2878
|
+
* console.log(`Cache updated: ${cache.getStatus()}`);
|
|
1846
2879
|
*/
|
|
1847
2880
|
async update (body, headers, statusCode = 200, expires = 0, status = null) {
|
|
1848
2881
|
|
|
@@ -1959,16 +2992,59 @@ class Cache {
|
|
|
1959
2992
|
};
|
|
1960
2993
|
};
|
|
1961
2994
|
|
|
2995
|
+
/**
|
|
2996
|
+
* The CacheableDataAccess object provides an interface to
|
|
2997
|
+
* the cache. It is responsible for reading from and writing to cache.
|
|
2998
|
+
* All requests to data go through the cache
|
|
2999
|
+
*
|
|
3000
|
+
* Before using CacheableDataAccess, the Cache must be initialized.
|
|
3001
|
+
*
|
|
3002
|
+
* @example
|
|
3003
|
+
* // Init should be done outside the handler
|
|
3004
|
+
* Cache.init({parameters});
|
|
3005
|
+
*
|
|
3006
|
+
* // Then you can then make a request in the handler
|
|
3007
|
+
* // sending it through CacheableDataAccess:
|
|
3008
|
+
* const { cache } = require("@63klabs/cache-data");
|
|
3009
|
+
* const cacheObj = await cache.CacheableDataAccess.getData(
|
|
3010
|
+
* cacheCfg,
|
|
3011
|
+
* yourFetchFunction,
|
|
3012
|
+
* conn,
|
|
3013
|
+
* daoQuery
|
|
3014
|
+
* );
|
|
3015
|
+
*/
|
|
1962
3016
|
class CacheableDataAccess {
|
|
1963
3017
|
constructor() { };
|
|
1964
3018
|
|
|
1965
3019
|
static #prevId = -1;
|
|
1966
3020
|
|
|
3021
|
+
/**
|
|
3022
|
+
* Internal method to generate sequential IDs for logging and tracking cache requests.
|
|
3023
|
+
* Each call increments and returns the next ID in the sequence.
|
|
3024
|
+
*
|
|
3025
|
+
* @private
|
|
3026
|
+
* @returns {string} The next sequential ID as a string
|
|
3027
|
+
* @example
|
|
3028
|
+
* // Called internally by CacheableDataAccess.getData()
|
|
3029
|
+
* const id = CacheableDataAccess.#getNextId(); // "0", "1", "2", etc.
|
|
3030
|
+
*/
|
|
1967
3031
|
static #getNextId() {
|
|
1968
3032
|
this.#prevId++;
|
|
1969
3033
|
return ""+this.#prevId;
|
|
1970
3034
|
};
|
|
1971
3035
|
|
|
3036
|
+
/**
|
|
3037
|
+
* Prime (refresh) runtime environment variables and cached secrets. This method
|
|
3038
|
+
* can be called to update values that may have changed since initialization.
|
|
3039
|
+
*
|
|
3040
|
+
* This is a convenience wrapper around CacheData.prime().
|
|
3041
|
+
*
|
|
3042
|
+
* @returns {Promise<boolean>} True if priming was successful, false if an error occurred
|
|
3043
|
+
* @example
|
|
3044
|
+
* // Prime before making cache requests
|
|
3045
|
+
* await CacheableDataAccess.prime();
|
|
3046
|
+
* const cache = await CacheableDataAccess.getData(cachePolicy, fetchFn, connection, data);
|
|
3047
|
+
*/
|
|
1972
3048
|
static async prime() {
|
|
1973
3049
|
return CacheData.prime();
|
|
1974
3050
|
};
|
|
@@ -1977,49 +3053,40 @@ class CacheableDataAccess {
|
|
|
1977
3053
|
* Data access object that will evaluate the cache and make a request to
|
|
1978
3054
|
* an endpoint to refresh.
|
|
1979
3055
|
*
|
|
3056
|
+
* @param {object} cachePolicy A cache policy object with properties: overrideOriginHeaderExpiration (boolean), defaultExpirationInSeconds (number), expirationIsOnInterval (boolean), headersToRetain (Array|string), hostId (string), pathId (string), encrypt (boolean)
|
|
3057
|
+
* @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
|
|
3058
|
+
* @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)
|
|
3059
|
+
* @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
|
|
3060
|
+
* @param {object} [tags={}] For logging. Do not include sensitive information.
|
|
3061
|
+
* @returns {Promise<Cache>} A Cache object with either cached or fresh data.
|
|
1980
3062
|
* @example
|
|
1981
|
-
* cachePolicy = {
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1989
|
-
*
|
|
3063
|
+
* const cachePolicy = {
|
|
3064
|
+
* overrideOriginHeaderExpiration: true,
|
|
3065
|
+
* defaultExpirationInSeconds: 60,
|
|
3066
|
+
* expirationIsOnInterval: true,
|
|
3067
|
+
* headersToRetain: [],
|
|
3068
|
+
* hostId: 'api.example.com',
|
|
3069
|
+
* pathId: '/users',
|
|
3070
|
+
* encrypt: true
|
|
3071
|
+
* };
|
|
1990
3072
|
*
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
1997
|
-
*
|
|
1998
|
-
*
|
|
1999
|
-
*
|
|
3073
|
+
* const connection = {
|
|
3074
|
+
* method: 'GET',
|
|
3075
|
+
* protocol: 'https',
|
|
3076
|
+
* host: 'api.example.com',
|
|
3077
|
+
* path: '/users/123',
|
|
3078
|
+
* parameters: {},
|
|
3079
|
+
* headers: {},
|
|
3080
|
+
* options: {timeout: 5000}
|
|
3081
|
+
* };
|
|
2000
3082
|
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
*
|
|
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.
|
|
3083
|
+
* const cache = await CacheableDataAccess.getData(
|
|
3084
|
+
* cachePolicy,
|
|
3085
|
+
* endpoint.get,
|
|
3086
|
+
* connection,
|
|
3087
|
+
* null,
|
|
3088
|
+
* {path: 'users', id: '123'}
|
|
3089
|
+
* );
|
|
2023
3090
|
*/
|
|
2024
3091
|
static async getData(cachePolicy, apiCallFunction, connection, data = null, tags = {} ) {
|
|
2025
3092
|
|
|
@@ -2049,11 +3116,23 @@ class CacheableDataAccess {
|
|
|
2049
3116
|
|
|
2050
3117
|
// add etag and last modified to connection
|
|
2051
3118
|
if ( !("headers" in connection)) { connection.headers = {}; }
|
|
2052
|
-
|
|
2053
|
-
|
|
3119
|
+
|
|
3120
|
+
// Sanitize existing headers to remove invalid values (including string "undefined")
|
|
3121
|
+
for (const key in connection.headers) {
|
|
3122
|
+
if (!Cache._isValidHeaderValue(connection.headers[key])) {
|
|
3123
|
+
delete connection.headers[key];
|
|
3124
|
+
tools.DebugAndLog.debug(`Removed invalid header value for "${key}": ${connection.headers[key]}`);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
const etag = cache.getETag();
|
|
3129
|
+
if ( !("if-none-match" in connection.headers) && Cache._isValidHeaderValue(etag)) {
|
|
3130
|
+
connection.headers['if-none-match'] = etag;
|
|
2054
3131
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
3132
|
+
|
|
3133
|
+
const lastModified = cache.getLastModified();
|
|
3134
|
+
if (!("if-modified-since" in connection.headers) && Cache._isValidHeaderValue(lastModified)) {
|
|
3135
|
+
connection.headers['if-modified-since'] = lastModified;
|
|
2057
3136
|
}
|
|
2058
3137
|
|
|
2059
3138
|
// request data from original source
|
|
@@ -2097,7 +3176,52 @@ class CacheableDataAccess {
|
|
|
2097
3176
|
};
|
|
2098
3177
|
};
|
|
2099
3178
|
|
|
3179
|
+
/**
|
|
3180
|
+
* TestHarness provides access to internal classes for testing purposes only.
|
|
3181
|
+
* This class should NEVER be used in production code - it exists solely to
|
|
3182
|
+
* enable proper testing of the cache-dao module without exposing internal
|
|
3183
|
+
* implementation details to end users.
|
|
3184
|
+
*
|
|
3185
|
+
* @private
|
|
3186
|
+
* @example
|
|
3187
|
+
* // In tests only - DO NOT use in production
|
|
3188
|
+
* import { TestHarness } from '../../src/lib/dao-cache.js';
|
|
3189
|
+
* const { CacheData } = TestHarness.getInternals();
|
|
3190
|
+
*
|
|
3191
|
+
* // Mock CacheData.read for testing
|
|
3192
|
+
* const originalRead = CacheData.read;
|
|
3193
|
+
* CacheData.read = async () => ({ cache: { body: 'test', headers: {} } });
|
|
3194
|
+
* // ... run tests ...
|
|
3195
|
+
* CacheData.read = originalRead; // Restore
|
|
3196
|
+
*/
|
|
3197
|
+
class TestHarness {
|
|
3198
|
+
/**
|
|
3199
|
+
* Get access to internal classes for testing purposes.
|
|
3200
|
+
* WARNING: This method is for testing only and should never be used in production.
|
|
3201
|
+
*
|
|
3202
|
+
* @returns {{CacheData: typeof CacheData, S3Cache: typeof S3Cache, DynamoDbCache: typeof DynamoDbCache}} Object containing internal classes
|
|
3203
|
+
* @private
|
|
3204
|
+
* @example
|
|
3205
|
+
* // In tests only - DO NOT use in production
|
|
3206
|
+
* const { CacheData, S3Cache, DynamoDbCache } = TestHarness.getInternals();
|
|
3207
|
+
*
|
|
3208
|
+
* // Mock CacheData.read for property-based testing
|
|
3209
|
+
* const originalRead = CacheData.read;
|
|
3210
|
+
* CacheData.read = async () => ({
|
|
3211
|
+
* cache: { body: 'test', headers: { 'content-type': 'application/json' }, expires: 1234567890, statusCode: '200' }
|
|
3212
|
+
* });
|
|
3213
|
+
*/
|
|
3214
|
+
static getInternals() {
|
|
3215
|
+
return {
|
|
3216
|
+
CacheData,
|
|
3217
|
+
S3Cache,
|
|
3218
|
+
DynamoDbCache
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
2100
3223
|
module.exports = {
|
|
2101
3224
|
Cache,
|
|
2102
|
-
CacheableDataAccess
|
|
3225
|
+
CacheableDataAccess,
|
|
3226
|
+
TestHarness
|
|
2103
3227
|
};
|