@feardread/feature-factory 3.0.1 → 4.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feardread/feature-factory",
3
- "version": "3.0.1",
3
+ "version": "4.0.1",
4
4
  "description": "Library to interact with redux toolkit and reduce boilerplate / repeated code",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  import axios from "axios";
2
2
  import qs from "qs";
3
- import cache from "./cache";
3
+ import CacheFactory from "./cache";
4
4
 
5
5
 
6
6
  const API_BASE_URL = (process.env.NODE_ENV === "production")
@@ -26,7 +26,7 @@ const instance = axios.create({
26
26
 
27
27
  instance.interceptors.request.use(
28
28
  (config) => {
29
- const isAuth = cache.local.get("auth") ? cache.local.get("auth") : null;
29
+ const isAuth = CacheFactory.local.get("auth") ? CacheFactory.local.get("auth") : null;
30
30
  let token = isAuth !== null ? isAuth.token : "";
31
31
 
32
32
  config.headers = {
@@ -55,7 +55,7 @@ instance.interceptors.response.use(
55
55
  console.log("API ERROR :: ", error);
56
56
  if (error.response) {
57
57
  if (error.response.status === 401) {
58
- cache.local.remove("auth");
58
+ CacheFactory.local.remove("auth");
59
59
  return Promise.reject(error.response);
60
60
  }
61
61
  if (error.response.status === 500) {
@@ -1,71 +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
4
- export const cache = (options = {}) => {
5
- var engine = options.type == 'local' ? 'localStorage' : 'sessionStorage';
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
+ */
120
+ export const CacheFactory = (options = {}) => {
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');
10
242
  return false;
11
243
  }
12
- return true;
13
- },
14
- set: (key, value) => {
15
- if (!key) throw Error('Error:> Invalid key');
16
244
 
17
- try {
18
- window[engine].setItem(key, JSON.stringify(value));
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
+ }
19
255
 
256
+ try {
257
+ await retryOperation(() => {
258
+ storage.setItem(prefixedKey, serializedValue);
259
+ });
260
+
261
+ log('SET', key, value);
262
+ return true;
20
263
  } catch (error) {
21
- console.error(`Error setting item ${key}:`, error);
264
+ console.error(`CacheFactory: Error setting item ${key}:`, error);
22
265
  return false;
23
266
  }
24
- return true;
25
267
  },
26
- get: (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);
287
+
27
288
  try {
28
- if (key !== "undefined") {
29
- const data = window[engine].getItem(key);
30
- return data != 'undefined' ? JSON.parse(data) : null;
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;
31
296
  }
32
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
+ },
33
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
+ }
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;
34
332
  } catch (error) {
35
- console.error(`Error getting item ${key}:`, error);
36
- return null;
333
+ console.error(`CacheFactory: Error removing item ${key}:`, error);
334
+ return false;
37
335
  }
38
336
  },
39
- remove: function (key) {
40
- window[engine].removeItem(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
+
348
+ try {
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
+ }
41
365
  },
42
- clear: () => {
43
- window[engine].clear();
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);
384
+ }
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;
391
+
392
+ log('KEYS', null, filteredKeys);
393
+ return filteredKeys;
394
+ } catch (error) {
395
+ console.error('CacheFactory: Error getting keys:', error);
396
+ return [];
397
+ }
44
398
  },
45
- keys: () => {
46
- return Object.keys(window[engine]);
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
+ }
47
423
  },
48
- has: (key) => {
49
- return window[engine].getItem(key) !== null;
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
+ };
50
439
  },
51
- /*
52
- _extend: () => {
53
- const destination = typeof arguments[0] === 'object' ? arguments[0] : {};
54
-
55
- for (var i = 1; i < arguments.length; i++) {
56
- if (arguments[i] && typeof arguments[i] === 'object') {
57
- for (var property in arguments[i])
58
- destination[property] = arguments[i][property];
59
- }
60
- }
61
440
 
62
- return destination;
63
- }
64
- */
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
+ },
495
+ },
65
496
  };
66
- }
497
+ };
498
+
499
+ // Pre-configured instances for convenience
500
+ CacheFactory.local = CacheFactory({ type: 'local' });
501
+ CacheFactory.session = CacheFactory({ type: 'session' });
67
502
 
68
- cache.local = cache({ type: 'local' });
69
- cache.session = cache({ type: 'session' });
503
+ // Memory-only instance for testing or server-side use
504
+ CacheFactory.memory = CacheFactory({
505
+ type: 'session',
506
+ fallbackToMemory: true
507
+ });
70
508
 
71
- export default cache;
509
+ export default CacheFactory;