@feardread/feature-factory 3.0.2 → 4.0.2

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.
@@ -1,57 +1,509 @@
1
+ /**
2
+ * Storage types supported by the CacheFactory
3
+ */
4
+ const STORAGE_TYPES = {
5
+ LOCAL: 'localStorage',
6
+ SESSION: 'sessionStorage',
7
+ };
1
8
 
9
+ /**
10
+ * Default configuration options
11
+ */
12
+ const DEFAULT_OPTIONS = {
13
+ type: 'session',
14
+ prefix: '',
15
+ enableLogging: false,
16
+ fallbackToMemory: true,
17
+ maxRetries: 3,
18
+ };
2
19
 
3
- // cache
20
+ /**
21
+ * In-memory fallback storage for when browser storage is unavailable
22
+ */
23
+ class MemoryStorage {
24
+ constructor() {
25
+ this.data = new Map();
26
+ }
27
+
28
+ setItem(key, value) {
29
+ this.data.set(key, value);
30
+ }
31
+
32
+ getItem(key) {
33
+ return this.data.get(key) || null;
34
+ }
35
+
36
+ removeItem(key) {
37
+ this.data.delete(key);
38
+ }
39
+
40
+ clear() {
41
+ this.data.clear();
42
+ }
43
+
44
+ get length() {
45
+ return this.data.size;
46
+ }
47
+
48
+ key(index) {
49
+ const keys = Array.from(this.data.keys());
50
+ return keys[index] || null;
51
+ }
52
+
53
+ keys() {
54
+ return Array.from(this.data.keys());
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Validates storage availability and functionality
60
+ * @param {Storage} storage - The storage object to test
61
+ * @returns {boolean} Whether storage is available and functional
62
+ */
63
+ const isStorageAvailable = (storage) => {
64
+ if (!storage) return false;
65
+
66
+ try {
67
+ const testKey = '__cache_factory_test__';
68
+ const testValue = 'test';
69
+
70
+ storage.setItem(testKey, testValue);
71
+ const retrieved = storage.getItem(testKey);
72
+ storage.removeItem(testKey);
73
+
74
+ return retrieved === testValue;
75
+ } catch (error) {
76
+ return false;
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Safely serializes data to JSON string
82
+ * @param {any} value - Value to serialize
83
+ * @returns {string|null} Serialized value or null if serialization fails
84
+ */
85
+ const safeStringify = (value) => {
86
+ try {
87
+ return JSON.stringify(value);
88
+ } catch (error) {
89
+ console.error('CacheFactory: Failed to serialize value', error);
90
+ return null;
91
+ }
92
+ };
93
+
94
+ /**
95
+ * Safely parses JSON string
96
+ * @param {string} value - JSON string to parse
97
+ * @returns {any|null} Parsed value or null if parsing fails
98
+ */
99
+ const safeParse = (value) => {
100
+ if (value === null || value === undefined) return null;
101
+
102
+ try {
103
+ return JSON.parse(value);
104
+ } catch (error) {
105
+ // Return the original value if it's not valid JSON
106
+ return value;
107
+ }
108
+ };
109
+
110
+ /**
111
+ * Creates a cache instance with the specified configuration
112
+ * @param {Object} options - Configuration options
113
+ * @param {string} options.type - Storage type ('local' or 'session')
114
+ * @param {string} options.prefix - Key prefix for namespacing
115
+ * @param {boolean} options.enableLogging - Enable debug logging
116
+ * @param {boolean} options.fallbackToMemory - Use memory storage as fallback
117
+ * @param {number} options.maxRetries - Maximum retry attempts for operations
118
+ * @returns {Object} Cache instance with storage methods
119
+ */
4
120
  export const CacheFactory = (options = {}) => {
5
- var engine = options.type == 'local' ? 'localStorage' : 'sessionStorage';
121
+ const config = { ...DEFAULT_OPTIONS, ...options };
122
+ const storageType = config.type === 'local' ? STORAGE_TYPES.LOCAL : STORAGE_TYPES.SESSION;
123
+
124
+ let storage = null;
125
+ let usingMemoryFallback = false;
126
+
127
+ // Initialize storage
128
+ const initializeStorage = () => {
129
+ if (typeof window === 'undefined') {
130
+ // Server-side rendering or Node.js environment
131
+ if (config.fallbackToMemory) {
132
+ storage = new MemoryStorage();
133
+ usingMemoryFallback = true;
134
+ if (config.enableLogging) {
135
+ console.warn('CacheFactory: Browser storage unavailable, using memory fallback');
136
+ }
137
+ }
138
+ return;
139
+ }
140
+
141
+ const browserStorage = window[storageType];
142
+
143
+ if (isStorageAvailable(browserStorage)) {
144
+ storage = browserStorage;
145
+ } else if (config.fallbackToMemory) {
146
+ storage = new MemoryStorage();
147
+ usingMemoryFallback = true;
148
+ if (config.enableLogging) {
149
+ console.warn(`CacheFactory: ${storageType} unavailable, using memory fallback`);
150
+ }
151
+ }
152
+ };
153
+
154
+ initializeStorage();
155
+
156
+ /**
157
+ * Generates a prefixed key
158
+ * @param {string} key - Original key
159
+ * @returns {string} Prefixed key
160
+ */
161
+ const getPrefixedKey = (key) => {
162
+ return config.prefix ? `${config.prefix}${key}` : key;
163
+ };
164
+
165
+ /**
166
+ * Removes prefix from key
167
+ * @param {string} prefixedKey - Prefixed key
168
+ * @returns {string} Original key
169
+ */
170
+ const removePrefixFromKey = (prefixedKey) => {
171
+ if (!config.prefix) return prefixedKey;
172
+ return prefixedKey.startsWith(config.prefix)
173
+ ? prefixedKey.slice(config.prefix.length)
174
+ : prefixedKey;
175
+ };
176
+
177
+ /**
178
+ * Logs debug information if logging is enabled
179
+ * @param {string} operation - Operation name
180
+ * @param {string} key - Storage key
181
+ * @param {any} data - Additional data to log
182
+ */
183
+ const log = (operation, key, data = null) => {
184
+ if (config.enableLogging) {
185
+ console.debug(`CacheFactory[${storageType}]: ${operation}`, { key, data, usingMemoryFallback });
186
+ }
187
+ };
188
+
189
+ /**
190
+ * Retries an operation with exponential backoff
191
+ * @param {Function} operation - Operation to retry
192
+ * @param {number} attempts - Current attempt number
193
+ * @returns {Promise<any>} Operation result
194
+ */
195
+ const retryOperation = async (operation, attempts = 0) => {
196
+ try {
197
+ return await operation();
198
+ } catch (error) {
199
+ if (attempts < config.maxRetries) {
200
+ const delay = Math.pow(2, attempts) * 100; // Exponential backoff
201
+ await new Promise(resolve => setTimeout(resolve, delay));
202
+ return retryOperation(operation, attempts + 1);
203
+ }
204
+ throw error;
205
+ }
206
+ };
6
207
 
7
208
  return {
8
- check: () => {
9
- if (!window[engine]) {
209
+ /**
210
+ * Checks if storage is available
211
+ * @returns {boolean} Storage availability status
212
+ */
213
+ isAvailable: () => {
214
+ return storage !== null;
215
+ },
216
+
217
+ /**
218
+ * Checks if using memory fallback
219
+ * @returns {boolean} Whether using memory storage
220
+ */
221
+ isUsingMemoryFallback: () => {
222
+ return usingMemoryFallback;
223
+ },
224
+
225
+ /**
226
+ * Gets storage type being used
227
+ * @returns {string} Storage type name
228
+ */
229
+ getStorageType: () => {
230
+ return usingMemoryFallback ? 'memory' : storageType;
231
+ },
232
+
233
+ /**
234
+ * Sets an item in storage
235
+ * @param {string} key - Storage key
236
+ * @param {any} value - Value to store
237
+ * @returns {Promise<boolean>} Success status
238
+ */
239
+ set: async (key, value) => {
240
+ if (!storage) {
241
+ console.error('CacheFactory: Storage not available');
242
+ return false;
243
+ }
244
+
245
+ if (!key || typeof key !== 'string') {
246
+ throw new Error('CacheFactory: Invalid key provided');
247
+ }
248
+
249
+ const prefixedKey = getPrefixedKey(key);
250
+ const serializedValue = safeStringify(value);
251
+
252
+ if (serializedValue === null) {
253
+ return false;
254
+ }
255
+
256
+ try {
257
+ await retryOperation(() => {
258
+ storage.setItem(prefixedKey, serializedValue);
259
+ });
260
+
261
+ log('SET', key, value);
262
+ return true;
263
+ } catch (error) {
264
+ console.error(`CacheFactory: Error setting item ${key}:`, error);
10
265
  return false;
11
266
  }
12
- return true;
13
267
  },
14
- set: (key, value) => {
15
- if (!key) throw Error('Error:> Invalid key');
268
+
269
+ /**
270
+ * Gets an item from storage
271
+ * @param {string} key - Storage key
272
+ * @param {any} defaultValue - Default value if key doesn't exist
273
+ * @returns {Promise<any>} Retrieved value or default
274
+ */
275
+ get: async (key, defaultValue = null) => {
276
+ if (!storage) {
277
+ console.warn('CacheFactory: Storage not available');
278
+ return defaultValue;
279
+ }
280
+
281
+ if (!key || typeof key !== 'string' || key === 'undefined') {
282
+ console.warn('CacheFactory: Invalid key provided for get operation');
283
+ return defaultValue;
284
+ }
285
+
286
+ const prefixedKey = getPrefixedKey(key);
16
287
 
17
288
  try {
18
- window[engine].setItem(key, JSON.stringify(value));
289
+ const data = await retryOperation(() => {
290
+ return storage.getItem(prefixedKey);
291
+ });
292
+
293
+ if (data === null || data === 'undefined') {
294
+ log('GET_MISS', key);
295
+ return defaultValue;
296
+ }
297
+
298
+ const parsedData = safeParse(data);
299
+ log('GET_HIT', key, parsedData);
300
+ return parsedData;
301
+ } catch (error) {
302
+ console.error(`CacheFactory: Error getting item ${key}:`, error);
303
+ return defaultValue;
304
+ }
305
+ },
306
+
307
+ /**
308
+ * Removes an item from storage
309
+ * @param {string} key - Storage key
310
+ * @returns {Promise<boolean>} Success status
311
+ */
312
+ remove: async (key) => {
313
+ if (!storage) {
314
+ console.error('CacheFactory: Storage not available');
315
+ return false;
316
+ }
19
317
 
318
+ if (!key || typeof key !== 'string') {
319
+ console.warn('CacheFactory: Invalid key provided for remove operation');
320
+ return false;
321
+ }
322
+
323
+ const prefixedKey = getPrefixedKey(key);
324
+
325
+ try {
326
+ await retryOperation(() => {
327
+ storage.removeItem(prefixedKey);
328
+ });
329
+
330
+ log('REMOVE', key);
331
+ return true;
20
332
  } catch (error) {
21
- console.error(`Error setting item ${key}:`, error);
333
+ console.error(`CacheFactory: Error removing item ${key}:`, error);
22
334
  return false;
23
335
  }
24
- return true;
25
336
  },
26
- get: (key) => {
337
+
338
+ /**
339
+ * Clears all items from storage (respects prefix)
340
+ * @returns {Promise<boolean>} Success status
341
+ */
342
+ clear: async () => {
343
+ if (!storage) {
344
+ console.error('CacheFactory: Storage not available');
345
+ return false;
346
+ }
347
+
27
348
  try {
28
- if (key !== "undefined") {
29
- const data = window[engine].getItem(key);
30
- return data != 'undefined' ? JSON.parse(data) : null;
349
+ if (config.prefix) {
350
+ // Clear only prefixed items
351
+ const keys = await this.keys();
352
+ await Promise.all(keys.map(key => this.remove(key)));
353
+ } else {
354
+ await retryOperation(() => {
355
+ storage.clear();
356
+ });
357
+ }
358
+
359
+ log('CLEAR');
360
+ return true;
361
+ } catch (error) {
362
+ console.error('CacheFactory: Error clearing storage:', error);
363
+ return false;
364
+ }
365
+ },
366
+
367
+ /**
368
+ * Gets all keys from storage (without prefix)
369
+ * @returns {Promise<string[]>} Array of keys
370
+ */
371
+ keys: async () => {
372
+ if (!storage) {
373
+ console.warn('CacheFactory: Storage not available');
374
+ return [];
375
+ }
376
+
377
+ try {
378
+ let keys = [];
379
+
380
+ if (usingMemoryFallback) {
381
+ keys = storage.keys();
382
+ } else {
383
+ keys = Object.keys(storage);
31
384
  }
32
385
 
386
+ // Filter by prefix and remove prefix from keys
387
+ const filteredKeys = config.prefix
388
+ ? keys.filter(key => key.startsWith(config.prefix))
389
+ .map(key => removePrefixFromKey(key))
390
+ : keys;
33
391
 
392
+ log('KEYS', null, filteredKeys);
393
+ return filteredKeys;
34
394
  } catch (error) {
35
- console.error(`Error getting item ${key}:`, error);
36
- return null;
395
+ console.error('CacheFactory: Error getting keys:', error);
396
+ return [];
37
397
  }
38
398
  },
39
- remove: function (key) {
40
- window[engine].removeItem(key);
41
- },
42
- clear: () => {
43
- window[engine].clear();
399
+
400
+ /**
401
+ * Checks if a key exists in storage
402
+ * @param {string} key - Storage key
403
+ * @returns {Promise<boolean>} Whether key exists
404
+ */
405
+ has: async (key) => {
406
+ if (!storage || !key || typeof key !== 'string') {
407
+ return false;
408
+ }
409
+
410
+ const prefixedKey = getPrefixedKey(key);
411
+
412
+ try {
413
+ const exists = await retryOperation(() => {
414
+ return storage.getItem(prefixedKey) !== null;
415
+ });
416
+
417
+ log('HAS', key, exists);
418
+ return exists;
419
+ } catch (error) {
420
+ console.error(`CacheFactory: Error checking if key ${key} exists:`, error);
421
+ return false;
422
+ }
44
423
  },
45
- keys: () => {
46
- return Object.keys(window[engine]);
424
+
425
+ /**
426
+ * Gets storage usage information
427
+ * @returns {Promise<Object>} Storage usage stats
428
+ */
429
+ getStats: async () => {
430
+ const keys = await this.keys();
431
+
432
+ return {
433
+ keyCount: keys.length,
434
+ storageType: this.getStorageType(),
435
+ usingMemoryFallback: usingMemoryFallback,
436
+ prefix: config.prefix || 'none',
437
+ keys: keys,
438
+ };
47
439
  },
48
- has: (key) => {
49
- return window[engine].getItem(key) !== null;
440
+
441
+ /**
442
+ * Bulk operations
443
+ */
444
+ bulk: {
445
+ /**
446
+ * Sets multiple items at once
447
+ * @param {Object} items - Key-value pairs to set
448
+ * @returns {Promise<Object>} Results of each operation
449
+ */
450
+ set: async (items) => {
451
+ const results = {};
452
+
453
+ await Promise.all(
454
+ Object.entries(items).map(async ([key, value]) => {
455
+ results[key] = await this.set(key, value);
456
+ })
457
+ );
458
+
459
+ return results;
460
+ },
461
+
462
+ /**
463
+ * Gets multiple items at once
464
+ * @param {string[]} keys - Keys to retrieve
465
+ * @returns {Promise<Object>} Retrieved key-value pairs
466
+ */
467
+ get: async (keys) => {
468
+ const results = {};
469
+
470
+ await Promise.all(
471
+ keys.map(async (key) => {
472
+ results[key] = await this.get(key);
473
+ })
474
+ );
475
+
476
+ return results;
477
+ },
478
+
479
+ /**
480
+ * Removes multiple items at once
481
+ * @param {string[]} keys - Keys to remove
482
+ * @returns {Promise<Object>} Results of each operation
483
+ */
484
+ remove: async (keys) => {
485
+ const results = {};
486
+
487
+ await Promise.all(
488
+ keys.map(async (key) => {
489
+ results[key] = await this.remove(key);
490
+ })
491
+ );
492
+
493
+ return results;
494
+ },
50
495
  },
51
496
  };
52
- }
497
+ };
53
498
 
499
+ // Pre-configured instances for convenience
54
500
  CacheFactory.local = CacheFactory({ type: 'local' });
55
501
  CacheFactory.session = CacheFactory({ type: 'session' });
56
502
 
57
- export default CacheFactory;
503
+ // Memory-only instance for testing or server-side use
504
+ CacheFactory.memory = CacheFactory({
505
+ type: 'session',
506
+ fallbackToMemory: true
507
+ });
508
+
509
+ export default CacheFactory;