@63klabs/cache-data 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_CONTEXT.md +863 -0
- package/CHANGELOG.md +44 -1
- package/CONTRIBUTING.md +161 -0
- package/README.md +139 -27
- package/package.json +9 -5
- package/scripts/README.md +175 -0
- package/scripts/audit-documentation.mjs +856 -0
- package/scripts/review-documentation-files.mjs +406 -0
- package/scripts/setup-dev-environment.sh +59 -0
- package/scripts/verify-example-files.mjs +194 -0
- package/src/lib/dao-cache.js +1286 -288
- package/src/lib/dao-endpoint.js +181 -42
- 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.
|
|
587
858
|
*
|
|
588
|
-
* @param {string} idHash
|
|
589
|
-
* @param {number} syncedNow
|
|
590
|
-
* @param {string} body
|
|
591
|
-
* @param {Object} headers
|
|
592
|
-
* @param {string} host
|
|
593
|
-
* @param {string} path
|
|
594
|
-
* @param {number} expires
|
|
595
|
-
* @param {number} statusCode
|
|
596
|
-
* @param {boolean} encrypt
|
|
597
|
-
* @returns {Promise<CacheDataFormat>}
|
|
859
|
+
* @param {string} idHash The unique identifier hash for the cache entry
|
|
860
|
+
* @param {number} syncedNow Current timestamp in seconds
|
|
861
|
+
* @param {string} body The content body to cache
|
|
862
|
+
* @param {Object} headers HTTP headers object
|
|
863
|
+
* @param {string} host Host identifier for logging
|
|
864
|
+
* @param {string} path Path identifier for logging
|
|
865
|
+
* @param {number} expires Expiration timestamp in seconds
|
|
866
|
+
* @param {string|number} statusCode HTTP status code
|
|
867
|
+
* @param {boolean} [encrypt=true] Whether to encrypt the body (true for private, false for public)
|
|
868
|
+
* @returns {Promise<CacheDataFormat>} Formatted cache data that was written
|
|
869
|
+
* @example
|
|
870
|
+
* const now = Math.floor(Date.now() / 1000);
|
|
871
|
+
* const expires = now + 3600; // 1 hour from now
|
|
872
|
+
* const body = JSON.stringify({ data: 'value' });
|
|
873
|
+
* const headers = { 'content-type': 'application/json' };
|
|
874
|
+
*
|
|
875
|
+
* const cacheData = await CacheData.write(
|
|
876
|
+
* 'abc123def456',
|
|
877
|
+
* now,
|
|
878
|
+
* body,
|
|
879
|
+
* headers,
|
|
880
|
+
* 'api.example.com',
|
|
881
|
+
* '/api/data',
|
|
882
|
+
* expires,
|
|
883
|
+
* '200',
|
|
884
|
+
* true
|
|
885
|
+
* );
|
|
598
886
|
*/
|
|
599
887
|
static async write (idHash, syncedNow, body, headers, host, path, expires, statusCode, encrypt = true) {
|
|
600
888
|
|
|
@@ -727,9 +1015,14 @@ class CacheData {
|
|
|
727
1015
|
*/
|
|
728
1016
|
|
|
729
1017
|
/**
|
|
730
|
-
*
|
|
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.
|
|
1081
|
+
*
|
|
1082
|
+
* This method generates a random initialization vector (IV) for each encryption
|
|
1083
|
+
* operation to ensure unique ciphertext even for identical plaintext. The IV
|
|
1084
|
+
* is returned along with the encrypted data.
|
|
1085
|
+
*
|
|
1086
|
+
* Note: null values are substituted with "{{{null}}}" before encryption to
|
|
1087
|
+
* handle the edge case of null data.
|
|
779
1088
|
*
|
|
780
|
-
* @
|
|
781
|
-
* @
|
|
1089
|
+
* @private
|
|
1090
|
+
* @param {string|null} text Data to encrypt
|
|
1091
|
+
* @returns {{iv: string, encryptedData: string}} Object containing the IV and encrypted data, both as hex strings
|
|
1092
|
+
* @example
|
|
1093
|
+
* // Called internally by CacheData.write()
|
|
1094
|
+
* const encrypted = CacheData._encrypt('sensitive data');
|
|
1095
|
+
* console.log(encrypted); // { iv: 'abc123...', encryptedData: 'def456...' }
|
|
782
1096
|
*/
|
|
783
1097
|
static _encrypt (text) {
|
|
784
1098
|
|
|
@@ -799,9 +1113,27 @@ class CacheData {
|
|
|
799
1113
|
};
|
|
800
1114
|
|
|
801
1115
|
/**
|
|
1116
|
+
* Internal method to decrypt data classified as private. Uses the configured
|
|
1117
|
+
* encryption algorithm and secure data key to decrypt the provided encrypted data.
|
|
1118
|
+
*
|
|
1119
|
+
* This method requires both the initialization vector (IV) and the encrypted data
|
|
1120
|
+
* that were produced by the _encrypt() method. It reverses the encryption process
|
|
1121
|
+
* to recover the original plaintext.
|
|
1122
|
+
*
|
|
1123
|
+
* Note: The special value "{{{null}}}" is converted back to null after decryption
|
|
1124
|
+
* to handle the edge case of null data.
|
|
802
1125
|
*
|
|
803
|
-
* @
|
|
804
|
-
* @
|
|
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.
|
|
1164
|
+
*
|
|
1165
|
+
* The ETag is a 10-character SHA-1 hash slice, which is sufficient for cache
|
|
1166
|
+
* validation at a specific endpoint without needing global uniqueness.
|
|
830
1167
|
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
* with it's content.
|
|
838
|
-
* @param {string} idHash The id of the endpoint
|
|
839
|
-
* @param {string} content The string to generate an eTag for
|
|
840
|
-
* @returns {string} 10 character ETag hash
|
|
1168
|
+
* @param {string} idHash The unique identifier hash for the cache entry
|
|
1169
|
+
* @param {string} content The content body to include in the ETag calculation
|
|
1170
|
+
* @returns {string} A 10-character ETag hash
|
|
1171
|
+
* @example
|
|
1172
|
+
* const etag = CacheData.generateEtag('abc123', '{"data":"value"}');
|
|
1173
|
+
* console.log(`ETag: ${etag}`); // e.g., "a1b2c3d4e5"
|
|
841
1174
|
*/
|
|
842
1175
|
static generateEtag (idHash, content) {
|
|
843
1176
|
const hasher = crypto.createHash('sha1');
|
|
@@ -847,16 +1180,22 @@ class CacheData {
|
|
|
847
1180
|
};
|
|
848
1181
|
|
|
849
1182
|
/**
|
|
850
|
-
* Generate an internet
|
|
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
|
|
|
@@ -1217,33 +1685,62 @@ class Cache {
|
|
|
1217
1685
|
};
|
|
1218
1686
|
|
|
1219
1687
|
/**
|
|
1220
|
-
* Generate an
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1688
|
+
* Generate an ETag hash for cache validation. Creates a unique hash by combining
|
|
1689
|
+
* the cache ID hash with the content body. This is used for HTTP ETag headers to
|
|
1690
|
+
* enable conditional requests and cache validation.
|
|
1691
|
+
*
|
|
1692
|
+
* This is a convenience wrapper around CacheData.generateEtag().
|
|
1693
|
+
*
|
|
1694
|
+
* @param {string} idHash The unique identifier hash for the cache entry
|
|
1695
|
+
* @param {string} content The content body to include in the ETag calculation
|
|
1696
|
+
* @returns {string} A 10-character ETag hash
|
|
1697
|
+
* @example
|
|
1698
|
+
* const etag = Cache.generateEtag('abc123', '{"data":"value"}');
|
|
1699
|
+
* console.log(`ETag: ${etag}`); // e.g., "a1b2c3d4e5"
|
|
1224
1700
|
*/
|
|
1225
1701
|
static generateEtag(idHash, content) {
|
|
1226
1702
|
return CacheData.generateEtag(idHash, content);
|
|
1227
1703
|
};
|
|
1228
1704
|
|
|
1229
1705
|
/**
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1706
|
+
* Convert all keys in an object to lowercase. Useful for normalizing HTTP headers
|
|
1707
|
+
* or other key-value pairs for case-insensitive comparison.
|
|
1708
|
+
*
|
|
1709
|
+
* This is a convenience wrapper around CacheData.lowerCaseKeys().
|
|
1710
|
+
*
|
|
1711
|
+
* Note: If lowercasing keys causes a collision (e.g., "Content-Type" and "content-type"),
|
|
1712
|
+
* one value will overwrite the other.
|
|
1713
|
+
*
|
|
1714
|
+
* @param {Object} objectWithKeys Object whose keys should be lowercased
|
|
1715
|
+
* @returns {Object} New object with all keys converted to lowercase
|
|
1716
|
+
* @example
|
|
1717
|
+
* const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' };
|
|
1718
|
+
* const normalized = Cache.lowerCaseKeys(headers);
|
|
1719
|
+
* console.log(normalized); // { 'content-type': 'application/json', 'cache-control': 'no-cache' }
|
|
1234
1720
|
*/
|
|
1235
1721
|
static lowerCaseKeys(objectWithKeys) {
|
|
1236
1722
|
return CacheData.lowerCaseKeys(objectWithKeys);
|
|
1237
1723
|
};
|
|
1238
1724
|
|
|
1239
1725
|
/**
|
|
1240
|
-
* Generate an internet
|
|
1726
|
+
* Generate an internet-formatted date string for use in HTTP headers.
|
|
1727
|
+
* Converts a Unix timestamp to the standard HTTP date format.
|
|
1241
1728
|
*
|
|
1242
|
-
*
|
|
1729
|
+
* This is a convenience wrapper around CacheData.generateInternetFormattedDate().
|
|
1730
|
+
*
|
|
1731
|
+
* Example output: "Wed, 28 Jul 2021 12:24:11 GMT"
|
|
1243
1732
|
*
|
|
1244
|
-
* @param {number} timestamp Unix timestamp in seconds or milliseconds
|
|
1245
|
-
* @param {boolean} inMilliseconds Set to true if timestamp is in milliseconds
|
|
1246
|
-
* @returns {string}
|
|
1733
|
+
* @param {number} timestamp Unix timestamp (in seconds by default, or milliseconds if inMilliseconds is true)
|
|
1734
|
+
* @param {boolean} [inMilliseconds=false] Set to true if timestamp is in milliseconds
|
|
1735
|
+
* @returns {string} Internet-formatted date string (e.g., "Wed, 28 Jul 2021 12:24:11 GMT")
|
|
1736
|
+
* @example
|
|
1737
|
+
* const now = Math.floor(Date.now() / 1000);
|
|
1738
|
+
* const dateStr = Cache.generateInternetFormattedDate(now);
|
|
1739
|
+
* console.log(dateStr); // "Mon, 26 Jan 2026 15:30:45 GMT"
|
|
1740
|
+
*
|
|
1741
|
+
* @example
|
|
1742
|
+
* const nowMs = Date.now();
|
|
1743
|
+
* const dateStr = Cache.generateInternetFormattedDate(nowMs, true);
|
|
1247
1744
|
*/
|
|
1248
1745
|
static generateInternetFormattedDate(timestamp, inMilliseconds = false) {
|
|
1249
1746
|
return CacheData.generateInternetFormattedDate(timestamp, inMilliseconds);
|
|
@@ -1289,6 +1786,24 @@ class Cache {
|
|
|
1289
1786
|
*
|
|
1290
1787
|
* @param {Object|Array|string} idObject Object, Array, or string to hash. Object may contain a single value with a text string, or complex http request broken down into parts
|
|
1291
1788
|
* @returns {string} A hash representing the object (Algorithm used is set in Cache object constructor)
|
|
1789
|
+
* @example
|
|
1790
|
+
* // Hash a simple object
|
|
1791
|
+
* const connection = { host: 'api.example.com', path: '/users' };
|
|
1792
|
+
* const hash = Cache.generateIdHash(connection);
|
|
1793
|
+
* console.log(hash); // "a1b2c3d4e5f6..."
|
|
1794
|
+
*
|
|
1795
|
+
* @example
|
|
1796
|
+
* // Hash a complex request object
|
|
1797
|
+
* const requestObj = {
|
|
1798
|
+
* query: { type: 'user', id: '123' },
|
|
1799
|
+
* connection: { host: 'api.example.com', path: '/data' },
|
|
1800
|
+
* cachePolicy: { ttl: 300 }
|
|
1801
|
+
* };
|
|
1802
|
+
* const hash = Cache.generateIdHash(requestObj);
|
|
1803
|
+
*
|
|
1804
|
+
* @example
|
|
1805
|
+
* // Hash a simple string ID
|
|
1806
|
+
* const hash = Cache.generateIdHash('MYID-03-88493');
|
|
1292
1807
|
*/
|
|
1293
1808
|
static generateIdHash(idObject) {
|
|
1294
1809
|
|
|
@@ -1346,6 +1861,20 @@ class Cache {
|
|
|
1346
1861
|
* @param {Array|string} identifierArrayOrString An array we wish to join together as an id. (also could be a string which we won't touch)
|
|
1347
1862
|
* @param {string} glue The glue or delimiter to place between the array elements once it is in string form
|
|
1348
1863
|
* @returns {string} The array in string form delimited by the glue.
|
|
1864
|
+
* @example
|
|
1865
|
+
* // Join array elements with default delimiter
|
|
1866
|
+
* const id = Cache.multipartId(['user', '123', 'profile']);
|
|
1867
|
+
* console.log(id); // "user-123-profile"
|
|
1868
|
+
*
|
|
1869
|
+
* @example
|
|
1870
|
+
* // Use custom delimiter
|
|
1871
|
+
* const id = Cache.multipartId(['api', 'v2', 'users'], '/');
|
|
1872
|
+
* console.log(id); // "api/v2/users"
|
|
1873
|
+
*
|
|
1874
|
+
* @example
|
|
1875
|
+
* // Pass a string (returns unchanged)
|
|
1876
|
+
* const id = Cache.multipartId('already-a-string');
|
|
1877
|
+
* console.log(id); // "already-a-string"
|
|
1349
1878
|
*/
|
|
1350
1879
|
static multipartId (identifierArrayOrString, glue = "-") {
|
|
1351
1880
|
let id = null;
|
|
@@ -1356,11 +1885,22 @@ class Cache {
|
|
|
1356
1885
|
};
|
|
1357
1886
|
|
|
1358
1887
|
/**
|
|
1359
|
-
* Uses Date.parse()
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1363
|
-
|
|
1888
|
+
* Convert a date string to a Unix timestamp in seconds. Uses Date.parse() to
|
|
1889
|
+
* parse the date string and converts the result from milliseconds to seconds.
|
|
1890
|
+
*
|
|
1891
|
+
* This method is useful for converting HTTP date headers (like "Expires" or
|
|
1892
|
+
* "Last-Modified") into Unix timestamps for comparison and calculation.
|
|
1893
|
+
*
|
|
1894
|
+
* @param {string} date Date string to parse (e.g., "2011-10-10T14:48:00" or "Wed, 28 Jul 2021 12:24:11 GMT")
|
|
1895
|
+
* @returns {number} The date in seconds since January 1, 1970, 00:00:00 UTC, or 0 if parsing fails
|
|
1896
|
+
* @example
|
|
1897
|
+
* const timestamp = Cache.parseToSeconds("2021-07-28T12:24:11Z");
|
|
1898
|
+
* console.log(timestamp); // 1627476251
|
|
1899
|
+
*
|
|
1900
|
+
* @example
|
|
1901
|
+
* const timestamp = Cache.parseToSeconds("Wed, 28 Jul 2021 12:24:11 GMT");
|
|
1902
|
+
* console.log(timestamp); // 1627476251
|
|
1903
|
+
*/
|
|
1364
1904
|
static parseToSeconds(date) {
|
|
1365
1905
|
let timestampInSeconds = 0;
|
|
1366
1906
|
try {
|
|
@@ -1392,6 +1932,16 @@ class Cache {
|
|
|
1392
1932
|
* @param {number} intervalInSeconds
|
|
1393
1933
|
* @param {number} timestampInSeconds
|
|
1394
1934
|
* @returns {number} Next interval in seconds
|
|
1935
|
+
* @example
|
|
1936
|
+
* // Calculate next 15-minute interval
|
|
1937
|
+
* const now = Math.floor(Date.now() / 1000);
|
|
1938
|
+
* const next15Min = Cache.nextIntervalInSeconds(15 * 60, now);
|
|
1939
|
+
* console.log(new Date(next15Min * 1000)); // Next 15-minute boundary
|
|
1940
|
+
*
|
|
1941
|
+
* @example
|
|
1942
|
+
* // Calculate next hourly interval
|
|
1943
|
+
* const nextHour = Cache.nextIntervalInSeconds(3600);
|
|
1944
|
+
* console.log(new Date(nextHour * 1000)); // Next hour boundary
|
|
1395
1945
|
*/
|
|
1396
1946
|
static nextIntervalInSeconds(intervalInSeconds, timestampInSeconds = 0) {
|
|
1397
1947
|
return CacheData.nextIntervalInSeconds(intervalInSeconds, timestampInSeconds);
|
|
@@ -1399,23 +1949,48 @@ class Cache {
|
|
|
1399
1949
|
|
|
1400
1950
|
|
|
1401
1951
|
/**
|
|
1402
|
-
* Calculate the
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1952
|
+
* Calculate the size of a string in kilobytes. Uses Buffer.byteLength() to
|
|
1953
|
+
* determine the byte size based on the specified character encoding, then
|
|
1954
|
+
* converts to KB (bytes / 1024).
|
|
1955
|
+
*
|
|
1956
|
+
* This is a convenience wrapper around CacheData.calculateKBytes().
|
|
1957
|
+
*
|
|
1958
|
+
* The result is rounded to 3 decimal places for precision (0.001 KB = 1 byte).
|
|
1959
|
+
*
|
|
1960
|
+
* @param {string} aString The string to measure
|
|
1961
|
+
* @param {string} [encode='utf8'] Character encoding to use for byte calculation
|
|
1962
|
+
* @returns {number} String size in kilobytes, rounded to 3 decimal places
|
|
1963
|
+
* @example
|
|
1964
|
+
* const text = 'Hello, World!';
|
|
1965
|
+
* const sizeKB = Cache.calculateKBytes(text);
|
|
1966
|
+
* console.log(`Size: ${sizeKB} KB`);
|
|
1967
|
+
*
|
|
1968
|
+
* @example
|
|
1969
|
+
* const largeText = 'x'.repeat(10000);
|
|
1970
|
+
* const sizeKB = Cache.calculateKBytes(largeText);
|
|
1971
|
+
* console.log(`Size: ${sizeKB} KB`); // ~9.766 KB
|
|
1408
1972
|
*/
|
|
1409
1973
|
static calculateKBytes ( aString, encode = CacheData.PLAIN_ENCODING ) {
|
|
1410
1974
|
return CacheData.calculateKBytes( aString, encode);
|
|
1411
1975
|
};
|
|
1412
1976
|
|
|
1413
1977
|
/**
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
* for
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1978
|
+
* Convert a comma-delimited string or array to an array with all lowercase values.
|
|
1979
|
+
* This method is useful for normalizing lists of header names or other identifiers
|
|
1980
|
+
* for case-insensitive comparison.
|
|
1981
|
+
*
|
|
1982
|
+
* If an array is provided, it is first converted to a comma-delimited string,
|
|
1983
|
+
* then lowercased and split back into an array.
|
|
1984
|
+
*
|
|
1985
|
+
* @param {string|Array.<string>} list Comma-delimited string or array to convert
|
|
1986
|
+
* @returns {Array.<string>} Array with all values converted to lowercase
|
|
1987
|
+
* @example
|
|
1988
|
+
* const headers = Cache.convertToLowerCaseArray('Content-Type,Cache-Control,ETag');
|
|
1989
|
+
* console.log(headers); // ['content-type', 'cache-control', 'etag']
|
|
1990
|
+
*
|
|
1991
|
+
* @example
|
|
1992
|
+
* const headers = Cache.convertToLowerCaseArray(['Content-Type', 'Cache-Control']);
|
|
1993
|
+
* console.log(headers); // ['content-type', 'cache-control']
|
|
1419
1994
|
*/
|
|
1420
1995
|
static convertToLowerCaseArray(list) {
|
|
1421
1996
|
|
|
@@ -1429,15 +2004,43 @@ class Cache {
|
|
|
1429
2004
|
};
|
|
1430
2005
|
|
|
1431
2006
|
/**
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
2007
|
+
* Internal method to parse and normalize the headersToRetain configuration.
|
|
2008
|
+
* Converts either a comma-delimited string or array into an array of lowercase
|
|
2009
|
+
* header names that should be retained from the origin response.
|
|
2010
|
+
*
|
|
2011
|
+
* @private
|
|
2012
|
+
* @param {string|Array.<string>} list Comma-delimited string or array of header names
|
|
2013
|
+
* @returns {Array.<string>} Array of lowercase header names to retain
|
|
2014
|
+
* @example
|
|
2015
|
+
* // Called internally during Cache constructor
|
|
2016
|
+
* const headers = this.#parseHeadersToRetain('Content-Type,Cache-Control');
|
|
2017
|
+
* // Returns: ['content-type', 'cache-control']
|
|
1436
2018
|
*/
|
|
1437
2019
|
#parseHeadersToRetain (list) {
|
|
1438
2020
|
return Cache.convertToLowerCaseArray(list);
|
|
1439
2021
|
};
|
|
1440
2022
|
|
|
2023
|
+
/**
|
|
2024
|
+
* Get the cache profile configuration for this Cache instance.
|
|
2025
|
+
* Returns an object containing all the cache policy settings that were
|
|
2026
|
+
* configured when this Cache object was created.
|
|
2027
|
+
*
|
|
2028
|
+
* @returns {{
|
|
2029
|
+
* overrideOriginHeaderExpiration: boolean,
|
|
2030
|
+
* defaultExpirationInSeconds: number,
|
|
2031
|
+
* defaultExpirationExtensionOnErrorInSeconds: number,
|
|
2032
|
+
* expirationIsOnInterval: boolean,
|
|
2033
|
+
* headersToRetain: Array<string>,
|
|
2034
|
+
* hostId: string,
|
|
2035
|
+
* pathId: string,
|
|
2036
|
+
* encrypt: boolean
|
|
2037
|
+
* }} Cache profile configuration object
|
|
2038
|
+
* @example
|
|
2039
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2040
|
+
* const profile = cache.profile();
|
|
2041
|
+
* console.log(`Expiration: ${profile.defaultExpirationInSeconds}s`);
|
|
2042
|
+
* console.log(`Encrypted: ${profile.encrypt}`);
|
|
2043
|
+
*/
|
|
1441
2044
|
profile () {
|
|
1442
2045
|
return {
|
|
1443
2046
|
overrideOriginHeaderExpiration: this.#overrideOriginHeaderExpiration,
|
|
@@ -1452,8 +2055,25 @@ class Cache {
|
|
|
1452
2055
|
};
|
|
1453
2056
|
|
|
1454
2057
|
/**
|
|
2058
|
+
* Read cached data from storage (DynamoDB and potentially S3). This method
|
|
2059
|
+
* first checks the in-memory cache (if enabled), then falls back to DynamoDB.
|
|
1455
2060
|
*
|
|
1456
|
-
*
|
|
2061
|
+
* If data is found in the in-memory cache and not expired, it is returned
|
|
2062
|
+
* immediately. Otherwise, data is fetched from DynamoDB and stored in the
|
|
2063
|
+
* in-memory cache for future requests.
|
|
2064
|
+
*
|
|
2065
|
+
* This method is called automatically by CacheableDataAccess.getData() and
|
|
2066
|
+
* typically does not need to be called directly.
|
|
2067
|
+
*
|
|
2068
|
+
* @returns {Promise<CacheDataFormat>} Formatted cache data with body, headers, expires, and statusCode
|
|
2069
|
+
* @example
|
|
2070
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2071
|
+
* await cache.read();
|
|
2072
|
+
* if (cache.needsRefresh()) {
|
|
2073
|
+
* console.log('Cache miss or expired');
|
|
2074
|
+
* } else {
|
|
2075
|
+
* console.log('Cache hit:', cache.getBody());
|
|
2076
|
+
* }
|
|
1457
2077
|
*/
|
|
1458
2078
|
async read () {
|
|
1459
2079
|
|
|
@@ -1463,15 +2083,63 @@ class Cache {
|
|
|
1463
2083
|
resolve(this.#store);
|
|
1464
2084
|
} else {
|
|
1465
2085
|
try {
|
|
2086
|
+
let staleData = null;
|
|
2087
|
+
|
|
2088
|
+
// Check L0_Cache if feature is enabled
|
|
2089
|
+
if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null) {
|
|
2090
|
+
const memResult = Cache.#inMemoryCache.get(this.#idHash);
|
|
2091
|
+
|
|
2092
|
+
if (memResult.cache === 1) {
|
|
2093
|
+
// Cache hit - return immediately
|
|
2094
|
+
this.#store = memResult.data;
|
|
2095
|
+
this.#status = Cache.STATUS_CACHE_IN_MEM;
|
|
2096
|
+
tools.DebugAndLog.debug(`In-memory cache hit: ${this.#idHash}`);
|
|
2097
|
+
resolve(this.#store);
|
|
2098
|
+
return;
|
|
2099
|
+
} else if (memResult.cache === -1) {
|
|
2100
|
+
// Expired - retain for potential fallback
|
|
2101
|
+
staleData = memResult.data;
|
|
2102
|
+
tools.DebugAndLog.debug(`In-memory cache expired, retaining stale data: ${this.#idHash}`);
|
|
2103
|
+
}
|
|
2104
|
+
// cache === 0 means miss, continue to DynamoDB
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Fetch from DynamoDB
|
|
1466
2108
|
this.#store = await CacheData.read(this.#idHash, this.#syncedLaterTimestampInSeconds);
|
|
1467
2109
|
this.#status = ( this.#store.cache.statusCode === null ) ? Cache.STATUS_NO_CACHE : Cache.STATUS_CACHE;
|
|
2110
|
+
|
|
2111
|
+
// Store in L0_Cache if successful and feature enabled
|
|
2112
|
+
if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null && this.#store.cache.statusCode !== null) {
|
|
2113
|
+
const expiresAt = this.#store.cache.expires * 1000; // Convert to milliseconds
|
|
2114
|
+
Cache.#inMemoryCache.set(this.#idHash, this.#store, expiresAt);
|
|
2115
|
+
tools.DebugAndLog.debug(`Stored in L0_Cache: ${this.#idHash}`);
|
|
2116
|
+
}
|
|
1468
2117
|
|
|
1469
2118
|
tools.DebugAndLog.debug(`Cache Read status: ${this.#status}`);
|
|
1470
2119
|
|
|
1471
2120
|
resolve(this.#store);
|
|
1472
2121
|
} catch (error) {
|
|
1473
|
-
|
|
1474
|
-
|
|
2122
|
+
// Error occurred - check if we have stale data to return
|
|
2123
|
+
if (staleData !== null) {
|
|
2124
|
+
// Calculate new expiration using error extension
|
|
2125
|
+
const newExpires = this.#syncedNowTimestampInSeconds + this.#defaultExpirationExtensionOnErrorInSeconds;
|
|
2126
|
+
const newExpiresAt = newExpires * 1000;
|
|
2127
|
+
|
|
2128
|
+
// Update stale data expiration
|
|
2129
|
+
staleData.cache.expires = newExpires;
|
|
2130
|
+
|
|
2131
|
+
// Store updated stale data back in L0_Cache
|
|
2132
|
+
if (Cache.#useInMemoryCache && Cache.#inMemoryCache !== null) {
|
|
2133
|
+
Cache.#inMemoryCache.set(this.#idHash, staleData, newExpiresAt);
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
this.#store = staleData;
|
|
2137
|
+
this.#status = Cache.STATUS_CACHE_ERROR;
|
|
2138
|
+
tools.DebugAndLog.warn(`Returning stale data due to error: ${this.#idHash}`);
|
|
2139
|
+
} else {
|
|
2140
|
+
this.#store = CacheData.format(this.#syncedLaterTimestampInSeconds);
|
|
2141
|
+
this.#status = Cache.STATUS_CACHE_ERROR;
|
|
2142
|
+
}
|
|
1475
2143
|
|
|
1476
2144
|
tools.DebugAndLog.error(`Cache Read: Cannot read cached data for ${this.#idHash}: ${error?.message || 'Unknown error'}`, error?.stack);
|
|
1477
2145
|
|
|
@@ -1483,6 +2151,39 @@ class Cache {
|
|
|
1483
2151
|
|
|
1484
2152
|
};
|
|
1485
2153
|
|
|
2154
|
+
/**
|
|
2155
|
+
* Get diagnostic test data for this Cache instance. Returns an object containing
|
|
2156
|
+
* the results of calling various getter methods, useful for debugging and
|
|
2157
|
+
* verifying cache state.
|
|
2158
|
+
*
|
|
2159
|
+
* This method is primarily for testing and debugging purposes.
|
|
2160
|
+
*
|
|
2161
|
+
* @returns {{
|
|
2162
|
+
* get: CacheDataFormat,
|
|
2163
|
+
* getStatus: string,
|
|
2164
|
+
* getETag: string,
|
|
2165
|
+
* getLastModified: string,
|
|
2166
|
+
* getExpires: number,
|
|
2167
|
+
* getExpiresGMT: string,
|
|
2168
|
+
* getHeaders: Object,
|
|
2169
|
+
* getSyncedNowTimestampInSeconds: number,
|
|
2170
|
+
* getBody: string,
|
|
2171
|
+
* getIdHash: string,
|
|
2172
|
+
* getClassification: string,
|
|
2173
|
+
* needsRefresh: boolean,
|
|
2174
|
+
* isExpired: boolean,
|
|
2175
|
+
* isEmpty: boolean,
|
|
2176
|
+
* isPrivate: boolean,
|
|
2177
|
+
* isPublic: boolean,
|
|
2178
|
+
* currentStatus: string,
|
|
2179
|
+
* calculateDefaultExpires: number
|
|
2180
|
+
* }} Object containing test data from various cache methods
|
|
2181
|
+
* @example
|
|
2182
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2183
|
+
* await cache.read();
|
|
2184
|
+
* const testData = cache.test();
|
|
2185
|
+
* console.log('Cache test data:', testData);
|
|
2186
|
+
*/
|
|
1486
2187
|
test() {
|
|
1487
2188
|
return {
|
|
1488
2189
|
get: this.get(),
|
|
@@ -1507,40 +2208,86 @@ class Cache {
|
|
|
1507
2208
|
};
|
|
1508
2209
|
|
|
1509
2210
|
/**
|
|
2211
|
+
* Get the complete cache data object. Returns the internal cache store
|
|
2212
|
+
* containing the cached body, headers, expiration, and status code.
|
|
1510
2213
|
*
|
|
1511
|
-
* @returns {CacheDataFormat}
|
|
2214
|
+
* @returns {CacheDataFormat} The complete cache data object
|
|
2215
|
+
* @example
|
|
2216
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2217
|
+
* await cache.read();
|
|
2218
|
+
* const data = cache.get();
|
|
2219
|
+
* console.log(data.cache.body);
|
|
2220
|
+
* console.log(data.cache.expires);
|
|
1512
2221
|
*/
|
|
1513
2222
|
get() {
|
|
1514
2223
|
return this.#store;
|
|
1515
2224
|
};
|
|
1516
2225
|
|
|
1517
2226
|
/**
|
|
2227
|
+
* Get the source status of the cache data. Returns a status string indicating
|
|
2228
|
+
* where the data came from and its current state.
|
|
2229
|
+
*
|
|
2230
|
+
* Possible values include:
|
|
2231
|
+
* - Cache.STATUS_NO_CACHE: No cached data exists
|
|
2232
|
+
* - Cache.STATUS_CACHE: Data from cache
|
|
2233
|
+
* - Cache.STATUS_CACHE_IN_MEM: Data from in-memory cache
|
|
2234
|
+
* - Cache.STATUS_EXPIRED: Cached data was expired
|
|
2235
|
+
* - Cache.STATUS_CACHE_SAME: Cache updated but content unchanged
|
|
2236
|
+
* - Cache.STATUS_ORIGINAL_NOT_MODIFIED: Origin returned 304 Not Modified
|
|
2237
|
+
* - Cache.STATUS_ORIGINAL_ERROR: Error fetching from origin
|
|
2238
|
+
* - Cache.STATUS_CACHE_ERROR: Error reading from cache
|
|
2239
|
+
* - Cache.STATUS_FORCED: Cache update was forced
|
|
1518
2240
|
*
|
|
1519
|
-
* @returns {string}
|
|
2241
|
+
* @returns {string} The source status string
|
|
2242
|
+
* @example
|
|
2243
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2244
|
+
* await cache.read();
|
|
2245
|
+
* console.log(`Data source: ${cache.getSourceStatus()}`);
|
|
1520
2246
|
*/
|
|
1521
2247
|
getSourceStatus() {
|
|
1522
2248
|
return this.#status;
|
|
1523
2249
|
};
|
|
1524
2250
|
|
|
1525
2251
|
/**
|
|
2252
|
+
* Get the ETag header value from the cached data. The ETag is used for
|
|
2253
|
+
* cache validation and conditional requests.
|
|
1526
2254
|
*
|
|
1527
|
-
* @returns {string}
|
|
2255
|
+
* @returns {string|null} The ETag value, or null if not present
|
|
2256
|
+
* @example
|
|
2257
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2258
|
+
* await cache.read();
|
|
2259
|
+
* const etag = cache.getETag();
|
|
2260
|
+
* console.log(`ETag: ${etag}`);
|
|
1528
2261
|
*/
|
|
1529
2262
|
getETag() {
|
|
1530
2263
|
return this.getHeader("etag");
|
|
1531
2264
|
};
|
|
1532
2265
|
|
|
1533
2266
|
/**
|
|
2267
|
+
* Get the Last-Modified header value from the cached data. This timestamp
|
|
2268
|
+
* indicates when the cached content was last modified at the origin.
|
|
1534
2269
|
*
|
|
1535
|
-
* @returns {string} The
|
|
2270
|
+
* @returns {string|null} The Last-Modified header value in HTTP date format, or null if not present
|
|
2271
|
+
* @example
|
|
2272
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2273
|
+
* await cache.read();
|
|
2274
|
+
* const lastModified = cache.getLastModified();
|
|
2275
|
+
* console.log(`Last modified: ${lastModified}`);
|
|
1536
2276
|
*/
|
|
1537
2277
|
getLastModified() {
|
|
1538
2278
|
return this.getHeader("last-modified");
|
|
1539
2279
|
};
|
|
1540
2280
|
|
|
1541
2281
|
/**
|
|
2282
|
+
* Get the expiration timestamp of the cached data. Returns the Unix timestamp
|
|
2283
|
+
* (in seconds) when the cached data will expire.
|
|
1542
2284
|
*
|
|
1543
|
-
* @returns {number} Expiration timestamp in seconds
|
|
2285
|
+
* @returns {number} Expiration timestamp in seconds since Unix epoch, or 0 if no cache data
|
|
2286
|
+
* @example
|
|
2287
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2288
|
+
* await cache.read();
|
|
2289
|
+
* const expires = cache.getExpires();
|
|
2290
|
+
* console.log(`Expires at: ${new Date(expires * 1000)}`);
|
|
1544
2291
|
*/
|
|
1545
2292
|
getExpires() {
|
|
1546
2293
|
let exp = (this.#store !== null) ? this.#store.cache.expires : 0;
|
|
@@ -1553,14 +2300,26 @@ class Cache {
|
|
|
1553
2300
|
* Example: "Wed, 28 Jul 2021 12:24:11 GMT"
|
|
1554
2301
|
*
|
|
1555
2302
|
* @returns {string} The expiration formated for use in headers. Same as expires header.
|
|
2303
|
+
* @example
|
|
2304
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2305
|
+
* await cache.read();
|
|
2306
|
+
* const expiresGMT = cache.getExpiresGMT();
|
|
2307
|
+
* console.log(expiresGMT); // "Wed, 28 Jul 2021 12:24:11 GMT"
|
|
1556
2308
|
*/
|
|
1557
2309
|
getExpiresGMT() {
|
|
1558
2310
|
return this.getHeader("expires");
|
|
1559
2311
|
};
|
|
1560
2312
|
|
|
1561
2313
|
/**
|
|
2314
|
+
* Calculate the number of seconds remaining until the cached data expires.
|
|
2315
|
+
* Returns 0 if the cache is already expired or if there is no cached data.
|
|
1562
2316
|
*
|
|
1563
|
-
* @returns {number} The
|
|
2317
|
+
* @returns {number} The number of seconds until expiration, or 0 if expired
|
|
2318
|
+
* @example
|
|
2319
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2320
|
+
* await cache.read();
|
|
2321
|
+
* const secondsLeft = cache.calculateSecondsLeftUntilExpires();
|
|
2322
|
+
* console.log(`Cache expires in ${secondsLeft} seconds`);
|
|
1564
2323
|
*/
|
|
1565
2324
|
calculateSecondsLeftUntilExpires() {
|
|
1566
2325
|
let secondsLeftUntilExpires = this.getExpires() - CacheData.convertTimestampFromMilliToSeconds( Date.now() );
|
|
@@ -1570,59 +2329,120 @@ class Cache {
|
|
|
1570
2329
|
};
|
|
1571
2330
|
|
|
1572
2331
|
/**
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
2332
|
+
* Generate the value for the Cache-Control HTTP header. Returns a string
|
|
2333
|
+
* containing the classification (public or private) and the max-age directive
|
|
2334
|
+
* based on the time remaining until expiration.
|
|
2335
|
+
*
|
|
2336
|
+
* Example output: "public, max-age=3600" or "private, max-age=300"
|
|
2337
|
+
*
|
|
2338
|
+
* @returns {string} The Cache-Control header value
|
|
2339
|
+
* @example
|
|
2340
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2341
|
+
* await cache.read();
|
|
2342
|
+
* const cacheControl = cache.getCacheControlHeaderValue();
|
|
2343
|
+
* console.log(cacheControl); // "public, max-age=3600"
|
|
1575
2344
|
*/
|
|
1576
2345
|
getCacheControlHeaderValue() {
|
|
1577
2346
|
return this.getClassification() +", max-age="+this.calculateSecondsLeftUntilExpires();
|
|
1578
2347
|
};
|
|
1579
2348
|
|
|
1580
2349
|
/**
|
|
2350
|
+
* Get all HTTP headers from the cached data. Returns an object containing
|
|
2351
|
+
* all header key-value pairs that were stored with the cached content.
|
|
1581
2352
|
*
|
|
1582
|
-
* @returns {
|
|
2353
|
+
* @returns {Object|null} Object containing all cached headers, or null if no cache data
|
|
2354
|
+
* @example
|
|
2355
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2356
|
+
* await cache.read();
|
|
2357
|
+
* const headers = cache.getHeaders();
|
|
2358
|
+
* console.log(headers['content-type']);
|
|
2359
|
+
* console.log(headers['etag']);
|
|
1583
2360
|
*/
|
|
1584
2361
|
getHeaders() {
|
|
1585
2362
|
return (this.#store !== null && "headers" in this.#store.cache) ? this.#store.cache.headers : null;
|
|
1586
2363
|
};
|
|
1587
2364
|
|
|
1588
2365
|
/**
|
|
2366
|
+
* Get the HTTP status code from the cached data. Returns the status code
|
|
2367
|
+
* that was stored when the data was originally cached.
|
|
1589
2368
|
*
|
|
1590
|
-
* @returns {string|null} The status code
|
|
2369
|
+
* @returns {string|null} The HTTP status code (e.g., "200", "404"), or null if no cache data
|
|
2370
|
+
* @example
|
|
2371
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2372
|
+
* await cache.read();
|
|
2373
|
+
* const statusCode = cache.getStatusCode();
|
|
2374
|
+
* console.log(`Status: ${statusCode}`); // "200"
|
|
1591
2375
|
*/
|
|
1592
2376
|
getStatusCode() {
|
|
1593
2377
|
return (this.#store !== null && "statusCode" in this.#store.cache) ? this.#store.cache.statusCode : null;
|
|
1594
2378
|
};
|
|
1595
2379
|
|
|
1596
2380
|
/**
|
|
2381
|
+
* Get the current error code for this cache instance. Returns a non-zero
|
|
2382
|
+
* error code if an error occurred during cache operations, or 0 if no error.
|
|
2383
|
+
*
|
|
2384
|
+
* Error codes are typically HTTP status codes (400+) that were encountered
|
|
2385
|
+
* when trying to refresh the cache from the origin.
|
|
1597
2386
|
*
|
|
1598
|
-
* @returns {number}
|
|
2387
|
+
* @returns {number} The error code, or 0 if no error
|
|
2388
|
+
* @example
|
|
2389
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2390
|
+
* await cache.read();
|
|
2391
|
+
* const errorCode = cache.getErrorCode();
|
|
2392
|
+
* if (errorCode >= 400) {
|
|
2393
|
+
* console.log(`Error occurred: ${errorCode}`);
|
|
2394
|
+
* }
|
|
1599
2395
|
*/
|
|
1600
2396
|
getErrorCode() {
|
|
1601
2397
|
return this.#errorCode;
|
|
1602
2398
|
};
|
|
1603
2399
|
|
|
1604
2400
|
/**
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
2401
|
+
* Get the classification of the cached data. Returns "private" if the data
|
|
2402
|
+
* is encrypted, or "public" if not encrypted.
|
|
2403
|
+
*
|
|
2404
|
+
* This classification is used in the Cache-Control header to indicate how
|
|
2405
|
+
* the data should be treated by intermediate caches and proxies.
|
|
2406
|
+
*
|
|
2407
|
+
* @returns {string} Either "private" (encrypted) or "public" (not encrypted)
|
|
2408
|
+
* @example
|
|
2409
|
+
* const cache = new Cache(connection, { encrypt: true });
|
|
2410
|
+
* console.log(cache.getClassification()); // "private"
|
|
2411
|
+
*
|
|
2412
|
+
* const publicCache = new Cache(connection, { encrypt: false });
|
|
2413
|
+
* console.log(publicCache.getClassification()); // "public"
|
|
1609
2414
|
*/
|
|
1610
2415
|
getClassification() {
|
|
1611
2416
|
return (this.#encrypt ? Cache.PRIVATE : Cache.PUBLIC );
|
|
1612
2417
|
};
|
|
1613
2418
|
|
|
1614
2419
|
/**
|
|
2420
|
+
* Get the synchronized "now" timestamp in seconds. This timestamp is set when
|
|
2421
|
+
* the Cache object is created and remains constant throughout the cache operation,
|
|
2422
|
+
* providing a consistent time base for expiration calculations.
|
|
1615
2423
|
*
|
|
1616
|
-
* @returns {number} The timestamp in seconds
|
|
2424
|
+
* @returns {number} The timestamp in seconds when this Cache object was created
|
|
2425
|
+
* @example
|
|
2426
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2427
|
+
* const now = cache.getSyncedNowTimestampInSeconds();
|
|
2428
|
+
* console.log(`Cache created at: ${new Date(now * 1000)}`);
|
|
1617
2429
|
*/
|
|
1618
2430
|
getSyncedNowTimestampInSeconds() {
|
|
1619
2431
|
return this.#syncedNowTimestampInSeconds;
|
|
1620
2432
|
};
|
|
1621
2433
|
|
|
1622
2434
|
/**
|
|
2435
|
+
* Get a specific header value from the cached data by key. Header keys are
|
|
2436
|
+
* case-insensitive (stored as lowercase).
|
|
1623
2437
|
*
|
|
1624
|
-
* @param {string} key The header key to
|
|
1625
|
-
* @returns {string|number|null} The value
|
|
2438
|
+
* @param {string} key The header key to retrieve (case-insensitive)
|
|
2439
|
+
* @returns {string|number|null} The header value, or null if the header doesn't exist
|
|
2440
|
+
* @example
|
|
2441
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2442
|
+
* await cache.read();
|
|
2443
|
+
* const contentType = cache.getHeader('content-type');
|
|
2444
|
+
* const etag = cache.getHeader('ETag'); // Case-insensitive
|
|
2445
|
+
* console.log(`Content-Type: ${contentType}`);
|
|
1626
2446
|
*/
|
|
1627
2447
|
getHeader(key) {
|
|
1628
2448
|
let headers = this.getHeaders();
|
|
@@ -1630,9 +2450,25 @@ class Cache {
|
|
|
1630
2450
|
};
|
|
1631
2451
|
|
|
1632
2452
|
/**
|
|
2453
|
+
* Get the cached body content. Optionally parses JSON content into an object.
|
|
1633
2454
|
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
2455
|
+
* By default, returns the body as a string (which may be JSON-encoded). If
|
|
2456
|
+
* parseBody is true, attempts to parse the body as JSON and return an object.
|
|
2457
|
+
* If parsing fails, returns the original string.
|
|
2458
|
+
*
|
|
2459
|
+
* @param {boolean} [parseBody=false] If true, parse JSON body into an object
|
|
2460
|
+
* @returns {string|Object|null} The body as a string, parsed object, or null if no cache data
|
|
2461
|
+
* @example
|
|
2462
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2463
|
+
* await cache.read();
|
|
2464
|
+
*
|
|
2465
|
+
* // Get body as string
|
|
2466
|
+
* const bodyStr = cache.getBody();
|
|
2467
|
+
* console.log(bodyStr); // '{"data":"value"}'
|
|
2468
|
+
*
|
|
2469
|
+
* // Get body as parsed object
|
|
2470
|
+
* const bodyObj = cache.getBody(true);
|
|
2471
|
+
* console.log(bodyObj.data); // 'value'
|
|
1636
2472
|
*/
|
|
1637
2473
|
getBody(parseBody = false) {
|
|
1638
2474
|
let body = (this.#store !== null) ? this.#store.cache.body : null;
|
|
@@ -1649,10 +2485,20 @@ class Cache {
|
|
|
1649
2485
|
};
|
|
1650
2486
|
|
|
1651
2487
|
/**
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
2488
|
+
* Get a plain data response object containing the status code, headers, and body.
|
|
2489
|
+
* This is a simplified response format suitable for internal use.
|
|
2490
|
+
*
|
|
2491
|
+
* For a full HTTP response suitable for API Gateway, use generateResponseForAPIGateway() instead.
|
|
2492
|
+
*
|
|
2493
|
+
* @param {boolean} [parseBody=false] If true, parse JSON body into an object
|
|
2494
|
+
* @returns {{statusCode: string, headers: Object, body: string|Object}|null} Response object, or null if no cache data
|
|
2495
|
+
* @example
|
|
2496
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2497
|
+
* await cache.read();
|
|
2498
|
+
* const response = cache.getResponse();
|
|
2499
|
+
* console.log(response.statusCode); // "200"
|
|
2500
|
+
* console.log(response.headers['content-type']);
|
|
2501
|
+
* console.log(response.body);
|
|
1656
2502
|
*/
|
|
1657
2503
|
getResponse(parseBody = false) {
|
|
1658
2504
|
let response = null;
|
|
@@ -1669,9 +2515,28 @@ class Cache {
|
|
|
1669
2515
|
};
|
|
1670
2516
|
|
|
1671
2517
|
/**
|
|
2518
|
+
* Generate a complete HTTP response suitable for AWS API Gateway. This method
|
|
2519
|
+
* adds standard headers (CORS, Cache-Control, data source), handles conditional
|
|
2520
|
+
* requests (If-None-Match, If-Modified-Since), and formats the response according
|
|
2521
|
+
* to API Gateway requirements.
|
|
2522
|
+
*
|
|
2523
|
+
* If the client's ETag or Last-Modified matches the cached data, returns a
|
|
2524
|
+
* 304 Not Modified response with no body.
|
|
2525
|
+
*
|
|
2526
|
+
* @param {Object} parameters Configuration parameters for the response
|
|
2527
|
+
* @param {string} [parameters.ifNoneMatch] The If-None-Match header value from the client request
|
|
2528
|
+
* @param {string} [parameters.ifModifiedSince] The If-Modified-Since header value from the client request
|
|
2529
|
+
* @returns {{statusCode: string, headers: Object, body: string|null}} Complete HTTP response for API Gateway
|
|
2530
|
+
* @example
|
|
2531
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2532
|
+
* await cache.read();
|
|
2533
|
+
*
|
|
2534
|
+
* const response = cache.generateResponseForAPIGateway({
|
|
2535
|
+
* ifNoneMatch: event.headers['if-none-match'],
|
|
2536
|
+
* ifModifiedSince: event.headers['if-modified-since']
|
|
2537
|
+
* });
|
|
1672
2538
|
*
|
|
1673
|
-
*
|
|
1674
|
-
* @returns {{statusCode: string, headers: object, body: string}}
|
|
2539
|
+
* return response; // Return to API Gateway
|
|
1675
2540
|
*/
|
|
1676
2541
|
generateResponseForAPIGateway( parameters ) {
|
|
1677
2542
|
|
|
@@ -1715,59 +2580,121 @@ class Cache {
|
|
|
1715
2580
|
};
|
|
1716
2581
|
|
|
1717
2582
|
/**
|
|
2583
|
+
* Get the unique identifier hash for this cache entry. This hash is generated
|
|
2584
|
+
* from the connection, data, and cache policy parameters and uniquely identifies
|
|
2585
|
+
* this specific cache entry.
|
|
1718
2586
|
*
|
|
1719
|
-
* @returns {string}
|
|
2587
|
+
* @returns {string} The unique cache identifier hash
|
|
2588
|
+
* @example
|
|
2589
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2590
|
+
* const idHash = cache.getIdHash();
|
|
2591
|
+
* console.log(`Cache ID: ${idHash}`);
|
|
1720
2592
|
*/
|
|
1721
2593
|
getIdHash() {
|
|
1722
2594
|
return this.#idHash;
|
|
1723
2595
|
};
|
|
1724
2596
|
|
|
1725
2597
|
/**
|
|
2598
|
+
* Check if the cache needs to be refreshed. Returns true if the cache is
|
|
2599
|
+
* expired or empty, indicating that fresh data should be fetched from the origin.
|
|
1726
2600
|
*
|
|
1727
|
-
* @returns {boolean}
|
|
2601
|
+
* @returns {boolean} True if cache needs refresh, false otherwise
|
|
2602
|
+
* @example
|
|
2603
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2604
|
+
* await cache.read();
|
|
2605
|
+
* if (cache.needsRefresh()) {
|
|
2606
|
+
* console.log('Fetching fresh data from origin');
|
|
2607
|
+
* // Fetch and update cache
|
|
2608
|
+
* }
|
|
1728
2609
|
*/
|
|
1729
2610
|
needsRefresh() {
|
|
1730
2611
|
return (this.isExpired() || this.isEmpty());
|
|
1731
2612
|
};
|
|
1732
2613
|
|
|
1733
2614
|
/**
|
|
2615
|
+
* Check if the cached data has expired. Compares the expiration timestamp
|
|
2616
|
+
* with the current time to determine if the cache is still valid.
|
|
1734
2617
|
*
|
|
1735
|
-
* @returns {boolean}
|
|
2618
|
+
* @returns {boolean} True if cache is expired, false if still valid
|
|
2619
|
+
* @example
|
|
2620
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2621
|
+
* await cache.read();
|
|
2622
|
+
* if (cache.isExpired()) {
|
|
2623
|
+
* console.log('Cache has expired');
|
|
2624
|
+
* }
|
|
1736
2625
|
*/
|
|
1737
2626
|
isExpired() {
|
|
1738
2627
|
return ( CacheData.convertTimestampFromSecondsToMilli(this.getExpires()) <= Date.now());
|
|
1739
2628
|
};
|
|
1740
2629
|
|
|
1741
2630
|
/**
|
|
2631
|
+
* Check if the cache is empty (no cached data exists). Returns true if there
|
|
2632
|
+
* is no cached data, indicating this is a cache miss.
|
|
1742
2633
|
*
|
|
1743
|
-
* @returns {boolean}
|
|
2634
|
+
* @returns {boolean} True if cache is empty, false if data exists
|
|
2635
|
+
* @example
|
|
2636
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2637
|
+
* await cache.read();
|
|
2638
|
+
* if (cache.isEmpty()) {
|
|
2639
|
+
* console.log('Cache miss - no data found');
|
|
2640
|
+
* }
|
|
1744
2641
|
*/
|
|
1745
2642
|
isEmpty() {
|
|
1746
2643
|
return (this.#store.cache.statusCode === null);
|
|
1747
2644
|
};
|
|
1748
2645
|
|
|
1749
2646
|
/**
|
|
2647
|
+
* Check if the cache is configured for private (encrypted) storage.
|
|
2648
|
+
* Private caches encrypt data at rest for sensitive content.
|
|
1750
2649
|
*
|
|
1751
|
-
* @returns {boolean}
|
|
2650
|
+
* @returns {boolean} True if cache is private/encrypted, false otherwise
|
|
2651
|
+
* @example
|
|
2652
|
+
* const cache = new Cache(connection, { encrypt: true });
|
|
2653
|
+
* if (cache.isPrivate()) {
|
|
2654
|
+
* console.log('Cache data is encrypted');
|
|
2655
|
+
* }
|
|
1752
2656
|
*/
|
|
1753
2657
|
isPrivate() {
|
|
1754
2658
|
return this.#encrypt;
|
|
1755
2659
|
};
|
|
1756
2660
|
|
|
1757
2661
|
/**
|
|
2662
|
+
* Check if the cache is configured for public (non-encrypted) storage.
|
|
2663
|
+
* Public caches do not encrypt data at rest.
|
|
1758
2664
|
*
|
|
1759
|
-
* @returns {boolean}
|
|
2665
|
+
* @returns {boolean} True if cache is public/non-encrypted, false otherwise
|
|
2666
|
+
* @example
|
|
2667
|
+
* const cache = new Cache(connection, { encrypt: false });
|
|
2668
|
+
* if (cache.isPublic()) {
|
|
2669
|
+
* console.log('Cache data is not encrypted');
|
|
2670
|
+
* }
|
|
1760
2671
|
*/
|
|
1761
2672
|
isPublic() {
|
|
1762
2673
|
return !this.#encrypt;
|
|
1763
2674
|
};
|
|
1764
2675
|
|
|
1765
2676
|
/**
|
|
2677
|
+
* Extend the expiration time of the cached data. This method is used when
|
|
2678
|
+
* the origin returns a 304 Not Modified response or when an error occurs
|
|
2679
|
+
* fetching fresh data.
|
|
2680
|
+
*
|
|
2681
|
+
* If an error occurred (reason is STATUS_ORIGINAL_ERROR), the cache is extended
|
|
2682
|
+
* by defaultExpirationExtensionOnErrorInSeconds. Otherwise, it's extended by
|
|
2683
|
+
* the specified seconds or the default expiration time.
|
|
2684
|
+
*
|
|
2685
|
+
* @param {string} reason Reason for extending: Cache.STATUS_ORIGINAL_ERROR or Cache.STATUS_ORIGINAL_NOT_MODIFIED
|
|
2686
|
+
* @param {number} [seconds=0] Number of seconds to extend (0 = use default)
|
|
2687
|
+
* @param {number} [errorCode=0] HTTP error code if extending due to error
|
|
2688
|
+
* @returns {Promise<boolean>} True if extension was successful, false otherwise
|
|
2689
|
+
* @example
|
|
2690
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2691
|
+
* await cache.read();
|
|
1766
2692
|
*
|
|
1767
|
-
*
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
1770
|
-
*
|
|
2693
|
+
* // Extend due to 304 Not Modified
|
|
2694
|
+
* await cache.extendExpires(Cache.STATUS_ORIGINAL_NOT_MODIFIED);
|
|
2695
|
+
*
|
|
2696
|
+
* // Extend due to error
|
|
2697
|
+
* await cache.extendExpires(Cache.STATUS_ORIGINAL_ERROR, 0, 500);
|
|
1771
2698
|
*/
|
|
1772
2699
|
async extendExpires(reason, seconds = 0, errorCode = 0) {
|
|
1773
2700
|
|
|
@@ -1821,28 +2748,65 @@ class Cache {
|
|
|
1821
2748
|
};
|
|
1822
2749
|
|
|
1823
2750
|
/**
|
|
2751
|
+
* Calculate the default expiration timestamp for cached data. If expiration
|
|
2752
|
+
* is configured to use intervals, calculates the next interval boundary.
|
|
2753
|
+
* Otherwise, returns the synced later timestamp (now + default expiration).
|
|
1824
2754
|
*
|
|
1825
|
-
* @returns {number}
|
|
2755
|
+
* @returns {number} The default expiration timestamp in seconds
|
|
2756
|
+
* @example
|
|
2757
|
+
* const cache = new Cache(connection, { expirationIsOnInterval: true, defaultExpirationInSeconds: 3600 });
|
|
2758
|
+
* const expires = cache.calculateDefaultExpires();
|
|
2759
|
+
* console.log(`Default expiration: ${new Date(expires * 1000)}`);
|
|
1826
2760
|
*/
|
|
1827
2761
|
calculateDefaultExpires() {
|
|
1828
2762
|
return (this.#expirationIsOnInterval) ? Cache.nextIntervalInSeconds(this.#defaultExpirationInSeconds, this.#syncedNowTimestampInSeconds) : this.#syncedLaterTimestampInSeconds;
|
|
1829
2763
|
};
|
|
1830
2764
|
|
|
1831
2765
|
/**
|
|
2766
|
+
* Get the current status of the cache. Returns a status string indicating
|
|
2767
|
+
* the source and state of the cached data.
|
|
1832
2768
|
*
|
|
1833
|
-
*
|
|
2769
|
+
* This is an alias for getSourceStatus().
|
|
2770
|
+
*
|
|
2771
|
+
* @returns {string} The cache status string
|
|
2772
|
+
* @example
|
|
2773
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2774
|
+
* await cache.read();
|
|
2775
|
+
* console.log(`Status: ${cache.getStatus()}`);
|
|
1834
2776
|
*/
|
|
1835
2777
|
getStatus() {
|
|
1836
2778
|
return this.#status;
|
|
1837
2779
|
};
|
|
1838
2780
|
|
|
1839
2781
|
/**
|
|
1840
|
-
* Store data in cache.
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
2782
|
+
* Store data in cache. Updates the cache with new content, headers, status code,
|
|
2783
|
+
* and expiration time. This method handles header normalization, expiration
|
|
2784
|
+
* calculation, and writes the data to both DynamoDB and S3 (if needed).
|
|
2785
|
+
*
|
|
2786
|
+
* The method automatically:
|
|
2787
|
+
* - Lowercases all header keys for consistency
|
|
2788
|
+
* - Retains specified headers from the origin response
|
|
2789
|
+
* - Generates ETag and Last-Modified headers if not present
|
|
2790
|
+
* - Calculates expiration based on origin headers or default settings
|
|
2791
|
+
* - Encrypts data if configured as private
|
|
2792
|
+
* - Routes large items to S3 storage
|
|
2793
|
+
*
|
|
2794
|
+
* @param {string|Object} body The content body to cache (will be stringified if object)
|
|
2795
|
+
* @param {Object} headers HTTP headers from the origin response
|
|
2796
|
+
* @param {string|number} [statusCode=200] HTTP status code from the origin
|
|
2797
|
+
* @param {number} [expires=0] Expiration Unix timestamp in seconds (0 = calculate default)
|
|
2798
|
+
* @param {string|null} [status=null] Optional status override (e.g., Cache.STATUS_FORCED)
|
|
1845
2799
|
* @returns {Promise<CacheDataFormat>} Representation of data stored in cache
|
|
2800
|
+
* @example
|
|
2801
|
+
* const cache = new Cache(connection, cacheProfile);
|
|
2802
|
+
* await cache.read();
|
|
2803
|
+
*
|
|
2804
|
+
* // Update cache with fresh data
|
|
2805
|
+
* const body = JSON.stringify({ data: 'value' });
|
|
2806
|
+
* const headers = { 'content-type': 'application/json' };
|
|
2807
|
+
* await cache.update(body, headers, 200);
|
|
2808
|
+
*
|
|
2809
|
+
* console.log(`Cache updated: ${cache.getStatus()}`);
|
|
1846
2810
|
*/
|
|
1847
2811
|
async update (body, headers, statusCode = 200, expires = 0, status = null) {
|
|
1848
2812
|
|
|
@@ -1959,16 +2923,59 @@ class Cache {
|
|
|
1959
2923
|
};
|
|
1960
2924
|
};
|
|
1961
2925
|
|
|
2926
|
+
/**
|
|
2927
|
+
* The CacheableDataAccess object provides an interface to
|
|
2928
|
+
* the cache. It is responsible for reading from and writing to cache.
|
|
2929
|
+
* All requests to data go through the cache
|
|
2930
|
+
*
|
|
2931
|
+
* Before using CacheableDataAccess, the Cache must be initialized.
|
|
2932
|
+
*
|
|
2933
|
+
* @example
|
|
2934
|
+
* // Init should be done outside the handler
|
|
2935
|
+
* Cache.init({parameters});
|
|
2936
|
+
*
|
|
2937
|
+
* // Then you can then make a request in the handler
|
|
2938
|
+
* // sending it through CacheableDataAccess:
|
|
2939
|
+
* const { cache } = require("@63klabs/cache-data");
|
|
2940
|
+
* const cacheObj = await cache.CacheableDataAccess.getData(
|
|
2941
|
+
* cacheCfg,
|
|
2942
|
+
* yourFetchFunction,
|
|
2943
|
+
* conn,
|
|
2944
|
+
* daoQuery
|
|
2945
|
+
* );
|
|
2946
|
+
*/
|
|
1962
2947
|
class CacheableDataAccess {
|
|
1963
2948
|
constructor() { };
|
|
1964
2949
|
|
|
1965
2950
|
static #prevId = -1;
|
|
1966
2951
|
|
|
2952
|
+
/**
|
|
2953
|
+
* Internal method to generate sequential IDs for logging and tracking cache requests.
|
|
2954
|
+
* Each call increments and returns the next ID in the sequence.
|
|
2955
|
+
*
|
|
2956
|
+
* @private
|
|
2957
|
+
* @returns {string} The next sequential ID as a string
|
|
2958
|
+
* @example
|
|
2959
|
+
* // Called internally by CacheableDataAccess.getData()
|
|
2960
|
+
* const id = CacheableDataAccess.#getNextId(); // "0", "1", "2", etc.
|
|
2961
|
+
*/
|
|
1967
2962
|
static #getNextId() {
|
|
1968
2963
|
this.#prevId++;
|
|
1969
2964
|
return ""+this.#prevId;
|
|
1970
2965
|
};
|
|
1971
2966
|
|
|
2967
|
+
/**
|
|
2968
|
+
* Prime (refresh) runtime environment variables and cached secrets. This method
|
|
2969
|
+
* can be called to update values that may have changed since initialization.
|
|
2970
|
+
*
|
|
2971
|
+
* This is a convenience wrapper around CacheData.prime().
|
|
2972
|
+
*
|
|
2973
|
+
* @returns {Promise<boolean>} True if priming was successful, false if an error occurred
|
|
2974
|
+
* @example
|
|
2975
|
+
* // Prime before making cache requests
|
|
2976
|
+
* await CacheableDataAccess.prime();
|
|
2977
|
+
* const cache = await CacheableDataAccess.getData(cachePolicy, fetchFn, connection, data);
|
|
2978
|
+
*/
|
|
1972
2979
|
static async prime() {
|
|
1973
2980
|
return CacheData.prime();
|
|
1974
2981
|
};
|
|
@@ -1977,49 +2984,40 @@ class CacheableDataAccess {
|
|
|
1977
2984
|
* Data access object that will evaluate the cache and make a request to
|
|
1978
2985
|
* an endpoint to refresh.
|
|
1979
2986
|
*
|
|
2987
|
+
* @param {object} cachePolicy A cache policy object with properties: overrideOriginHeaderExpiration (boolean), defaultExpirationInSeconds (number), expirationIsOnInterval (boolean), headersToRetain (Array|string), hostId (string), pathId (string), encrypt (boolean)
|
|
2988
|
+
* @param {Function} apiCallFunction The function to call in order to make the request. This function can call ANY datasource (file, http endpoint, etc) as long as it returns a DAO object
|
|
2989
|
+
* @param {object} connection A connection object that specifies an id, location, and connection details for the apiCallFunction to access data. If you have a Connection object pass conn.toObject(). Properties: method (string), protocol (string), host (string), path (string), parameters (object), headers (object), body (string, for POST requests), options (object with timeout in ms)
|
|
2990
|
+
* @param {object} [data=null] An object passed to the apiCallFunction as a parameter. Set to null if the apiCallFunction does not require a data param
|
|
2991
|
+
* @param {object} [tags={}] For logging. Do not include sensitive information.
|
|
2992
|
+
* @returns {Promise<Cache>} A Cache object with either cached or fresh data.
|
|
1980
2993
|
* @example
|
|
1981
|
-
* cachePolicy = {
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1989
|
-
*
|
|
2994
|
+
* const cachePolicy = {
|
|
2995
|
+
* overrideOriginHeaderExpiration: true,
|
|
2996
|
+
* defaultExpirationInSeconds: 60,
|
|
2997
|
+
* expirationIsOnInterval: true,
|
|
2998
|
+
* headersToRetain: [],
|
|
2999
|
+
* hostId: 'api.example.com',
|
|
3000
|
+
* pathId: '/users',
|
|
3001
|
+
* encrypt: true
|
|
3002
|
+
* };
|
|
1990
3003
|
*
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
1997
|
-
*
|
|
1998
|
-
*
|
|
1999
|
-
*
|
|
3004
|
+
* const connection = {
|
|
3005
|
+
* method: 'GET',
|
|
3006
|
+
* protocol: 'https',
|
|
3007
|
+
* host: 'api.example.com',
|
|
3008
|
+
* path: '/users/123',
|
|
3009
|
+
* parameters: {},
|
|
3010
|
+
* headers: {},
|
|
3011
|
+
* options: {timeout: 5000}
|
|
3012
|
+
* };
|
|
2000
3013
|
*
|
|
2001
|
-
*
|
|
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.
|
|
3014
|
+
* const cache = await CacheableDataAccess.getData(
|
|
3015
|
+
* cachePolicy,
|
|
3016
|
+
* endpoint.get,
|
|
3017
|
+
* connection,
|
|
3018
|
+
* null,
|
|
3019
|
+
* {path: 'users', id: '123'}
|
|
3020
|
+
* );
|
|
2023
3021
|
*/
|
|
2024
3022
|
static async getData(cachePolicy, apiCallFunction, connection, data = null, tags = {} ) {
|
|
2025
3023
|
|