@feardread/feature-factory 5.1.2 → 6.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": "5.1.2",
3
+ "version": "6.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": {
package/rollup.config.mjs CHANGED
@@ -4,40 +4,53 @@ import commonjs from "@rollup/plugin-commonjs";
4
4
  import json from "@rollup/plugin-json";
5
5
  import peerDepsExternal from 'rollup-plugin-peer-deps-external';
6
6
 
7
+ // Shared globals map — used by the IIFE bundle to locate peer deps
8
+ // on the global scope instead of trying to require() them at runtime.
9
+ const globals = {
10
+ 'react': 'React',
11
+ 'react-dom': 'ReactDOM',
12
+ 'react-native': 'ReactNative',
13
+ 'react-redux': 'ReactRedux',
14
+ '@reduxjs/toolkit': 'ReduxToolkit',
15
+ 'rsuite': 'RSuite',
16
+ 'axios': 'axios',
17
+ 'qs': 'Qs',
18
+ };
19
+
7
20
  const jsconfig = [{
8
- input: 'src/index.js',
9
- output: [
10
- {
11
- file: 'dist/index.js',
12
- format: 'cjs',
13
- exports: 'named',
14
- sourcemap: true,
15
- },
16
- {
17
- file: 'dist/index.esm.js',
18
- format: "esm",
19
- exports: 'named',
20
- sourcemap: true,
21
- },
22
- {
23
- file: 'dist/bundle.min.js',
24
- format: 'iife',
25
- name: 'version',
26
- plugins: [terser()]
27
- },
28
- ],
29
- plugins: [
30
- peerDepsExternal(),
31
- resolve({
32
- browser: true,
33
- preferBuiltins: false,
34
- }),
35
- commonjs(),
36
- json(),
37
- terser()
38
- ],
39
- context: "this",
40
- },
41
- ];
21
+ input: 'src/index.js',
22
+ output: [
23
+ {
24
+ file: 'dist/index.js',
25
+ format: 'cjs',
26
+ exports: 'named',
27
+ sourcemap: true,
28
+ },
29
+ {
30
+ file: 'dist/index.esm.js',
31
+ format: 'esm',
32
+ exports: 'named',
33
+ sourcemap: true,
34
+ },
35
+ {
36
+ file: 'dist/bundle.min.js',
37
+ format: 'iife',
38
+ name: 'FeatureFactory', // exposes the bundle as window.MyLib
39
+ globals, // tells Rollup which globals map to which peer dep
40
+ plugins: [terser()],
41
+ },
42
+ ],
43
+ plugins: [
44
+ peerDepsExternal(), // marks all peerDependencies as external
45
+ resolve({
46
+ browser: true,
47
+ preferBuiltins: false,
48
+ }),
49
+ commonjs(),
50
+ json(),
51
+ terser(),
52
+ ],
53
+ context: 'this',
54
+ }];
42
55
 
43
- export default jsconfig;
56
+ export default jsconfig;
@@ -1,5 +1,6 @@
1
1
  import axios from 'axios';
2
2
  import qs from 'qs';
3
+ import { Platform } from 'react-native';
3
4
  import CacheFactory from './cache';
4
5
 
5
6
  /**
@@ -7,24 +8,19 @@ import CacheFactory from './cache';
7
8
  */
8
9
  const CONFIG = {
9
10
  BASE_URL: null,
11
+ environment: process.env.NODE_ENV,
10
12
  // API Base URLs
11
13
  baseUrls: {
12
- production: process.env.API_BASE_URL_PROD || 'http://fear.master.com/fear/api/',
14
+ production: process.env.API_BASE_URL_PROD || 'https://fear.dedyn.io/fear/api/',
13
15
  development: process.env.API_BASE_URL_DEV || 'http://localhost:4000/fear/api/',
14
16
  test: process.env.API_BASE_URL_TEST || 'https://fear.dedyn.io/fear/api/',
15
17
  },
16
- tokenNames: {
17
- bearer: 'Authorization',
18
- custom: process.env.REACT_APP_JWT_TOKEN_HEADER || 'x-token',
19
- },
20
18
  cacheKeys: {
21
- auth: 'auth',
22
- refreshToken: 'refresh_token',
23
19
  userPrefs: 'user_preferences',
24
20
  },
25
- timeout: parseInt(process.env.REACT_APP_API_TIMEOUT) || 30000,
26
- retryAttempts: parseInt(process.env.REACT_APP_API_RETRY_ATTEMPTS) || 3,
27
- retryDelay: parseInt(process.env.REACT_APP_API_RETRY_DELAY) || 1000,
21
+ timeout: parseInt(process.env.API_TIMEOUT) || 30000,
22
+ retryAttempts: parseInt(process.env.API_RETRY_ATTEMPTS) || 3,
23
+ retryDelay: parseInt(process.env.API_RETRY_DELAY) || 1000,
28
24
  };
29
25
 
30
26
  const setBaseUrl = (uri) => {
@@ -38,86 +34,10 @@ const setBaseUrl = (uri) => {
38
34
  */
39
35
  const getBaseUrl = () => {
40
36
  const env = process.env.NODE_ENV || 'development';
41
- if (!CONFIG.BASE_URL) CONFIG.BASE_URL = CONFIG.baseUrls[env] || CONFIG.baseUrls.development
37
+ if (!CONFIG.BASE_URL) CONFIG.BASE_URL = CONFIG.baseUrls[env] || CONFIG.baseUrls.development;
42
38
  return CONFIG.BASE_URL;
43
39
  };
44
40
 
45
- /**
46
- * Gets authentication data from cache
47
- * @returns {Object|null} Auth data or null
48
- */
49
- const getAuthData = () => {
50
- try {
51
- const authData = CacheFactory.local.get(CONFIG.cacheKeys.auth);
52
- return authData && typeof authData === 'object' ? authData : null;
53
- } catch (error) {
54
- console.warn('Failed to retrieve auth data:', error);
55
- return null;
56
- }
57
- };
58
-
59
- /**
60
- * Checks if token is expired
61
- * @param {string} token - JWT token
62
- * @returns {boolean} Whether token is expired
63
- */
64
- const isTokenExpired = (token) => {
65
- if (!token) return true;
66
-
67
- try {
68
- const payload = JSON.parse(atob(token.split('.')[1]));
69
- const currentTime = Date.now() / 1000;
70
- return payload.exp < currentTime;
71
- } catch (error) {
72
- return true;
73
- }
74
- };
75
-
76
- /**
77
- * Refreshes the authentication token
78
- * @returns {Promise<string|null>} New token or null if refresh failed
79
- */
80
- const refreshAuthToken = async () => {
81
- try {
82
- const authData = getAuthData();
83
- const refreshToken = authData?.refreshToken || CacheFactory.local.get(CONFIG.cacheKeys.refreshToken);
84
-
85
- if (!refreshToken) {
86
- throw new Error('No refresh token available');
87
- }
88
-
89
- const response = await axios.post(`${getBaseUrl()}auth/refresh`, {
90
- refreshToken,
91
- });
92
-
93
- const newAuthData = {
94
- token: response.data.token,
95
- refreshToken: response.data.refreshToken || refreshToken,
96
- user: response.data.user,
97
- expiresAt: response.data.expiresAt,
98
- };
99
-
100
- CacheFactory.local.set(CONFIG.cacheKeys.auth, newAuthData);
101
- return newAuthData.token;
102
- } catch (error) {
103
- console.error('Token refresh failed:', error);
104
- clearAuthData();
105
- return null;
106
- }
107
- };
108
-
109
- /**
110
- * Clears authentication data from cache
111
- */
112
- const clearAuthData = () => {
113
- try {
114
- CacheFactory.local.remove(CONFIG.cacheKeys.auth);
115
- CacheFactory.local.remove(CONFIG.cacheKeys.refreshToken);
116
- } catch (error) {
117
- console.warn('Failed to clear auth data:', error);
118
- }
119
- };
120
-
121
41
  /**
122
42
  * Creates retry delay with exponential backoff
123
43
  * @param {number} attempt - Current attempt number
@@ -142,7 +62,6 @@ const formatError = (error) => {
142
62
  };
143
63
 
144
64
  if (error.response) {
145
- // Server responded with error status
146
65
  return {
147
66
  ...baseError,
148
67
  message: error.response.data?.message || `HTTP Error ${error.response.status}`,
@@ -151,17 +70,15 @@ const formatError = (error) => {
151
70
  data: error.response.data,
152
71
  };
153
72
  }
154
-
73
+
155
74
  if (error.request) {
156
- // Request made but no response received
157
75
  return {
158
76
  ...baseError,
159
77
  message: 'Network error - no response received',
160
78
  code: 'NETWORK_ERROR',
161
79
  };
162
80
  }
163
-
164
- // Something else happened
81
+
165
82
  return {
166
83
  ...baseError,
167
84
  message: error.message || 'Request configuration error',
@@ -170,37 +87,18 @@ const formatError = (error) => {
170
87
  };
171
88
 
172
89
  /**
173
- * Request interceptor with retry logic
90
+ * Request interceptor adds tracking headers
174
91
  * @param {Object} config - Axios config
175
92
  * @returns {Promise<Object>} Modified config
176
93
  */
177
94
  const requestInterceptor = async (config) => {
178
95
  try {
179
- // Set retry metadata
180
96
  config.metadata = { startTime: Date.now() };
181
97
  config._retry = config._retry || 0;
182
98
 
183
- // Get authentication data
184
- const authData = getAuthData();
185
- let token = authData?.token;
186
-
187
- // Check if token needs refresh
188
- if (token && isTokenExpired(token) && config._retry === 0) {
189
- token = await refreshAuthToken();
190
- }
191
-
192
- // Set authorization headers
193
- if (token) {
194
- config.headers[CONFIG.tokenNames.bearer] = `Bearer ${token}`;
195
- config.headers[CONFIG.tokenNames.custom] = token;
196
- }
197
-
198
- // Add request ID for tracking
199
99
  config.headers['X-Request-ID'] = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
200
-
201
- // Add client info
202
- config.headers['X-Client-Version'] = process.env.REACT_APP_VERSION || '1.0.0';
203
- config.headers['X-Client-Platform'] = 'web';
100
+ config.headers['X-Client-Version'] = process.env.API_VERSION || '1.0.0';
101
+ config.headers['X-Client-Platform'] = Platform.OS; // 'ios' | 'android' | 'web'
204
102
 
205
103
  return config;
206
104
  } catch (error) {
@@ -225,16 +123,12 @@ const requestErrorInterceptor = (error) => {
225
123
  * @returns {Object} Processed response
226
124
  */
227
125
  const responseInterceptor = (response) => {
228
- // Add response metadata
229
126
  response.metadata = {
230
127
  responseTime: Date.now() - (response.config.metadata?.startTime || Date.now()),
231
128
  requestId: response.config.headers['X-Request-ID'],
232
129
  };
233
130
 
234
- console.log('Loaded Env = ', process.env);
235
-
236
- // Log successful responses in development
237
- if (process.env.NODE_ENV === 'development') {
131
+ if (CONFIG.environment === 'development') {
238
132
  console.log(`✅ API Success [${response.status}]:`, {
239
133
  url: response.config.url,
240
134
  data: response.data,
@@ -244,20 +138,16 @@ const responseInterceptor = (response) => {
244
138
  });
245
139
  }
246
140
 
247
- // Validate response structure
248
141
  if (response.data && typeof response.data === 'object') {
249
- // Handle different success status codes
250
142
  if ([200, 201, 202, 204].includes(response.status)) {
251
143
  return response;
252
144
  }
253
145
  }
254
146
 
255
- // Handle edge cases
256
147
  if (response.status >= 200 && response.status < 300) {
257
148
  return response;
258
149
  }
259
150
 
260
- // Unexpected status code
261
151
  return Promise.reject(formatError({
262
152
  response,
263
153
  message: `Unexpected response status: ${response.status}`,
@@ -265,18 +155,15 @@ const responseInterceptor = (response) => {
265
155
  };
266
156
 
267
157
  /**
268
- * Response error interceptor with retry and auth handling
158
+ * Response error interceptor with retry logic
269
159
  * @param {Object} error - Response error
270
160
  * @returns {Promise<Object>} Retry attempt or rejected promise
271
161
  */
272
162
  const responseErrorInterceptor = async (error) => {
273
163
  const originalRequest = error.config;
274
-
275
- // Format error for consistent handling
276
164
  const formattedError = formatError(error);
277
-
278
- // Log errors in development
279
- if (process.env.NODE_ENV === 'development') {
165
+
166
+ if (CONFIG.environment === 'development') {
280
167
  console.error(`❌ API Error [${formattedError.status}]:`, {
281
168
  url: originalRequest?.url,
282
169
  error: formattedError,
@@ -286,48 +173,19 @@ const responseErrorInterceptor = async (error) => {
286
173
  });
287
174
  }
288
175
 
289
- // Handle authentication errors
290
- if (formattedError.status === 401 && !originalRequest._isRetryingAuth) {
291
- originalRequest._isRetryingAuth = true;
292
-
293
- try {
294
- const newToken = await refreshAuthToken();
295
- if (newToken) {
296
- // Retry original request with new token
297
- originalRequest.headers[CONFIG.tokenNames.bearer] = `Bearer ${newToken}`;
298
- originalRequest.headers[CONFIG.tokenNames.custom] = newToken;
299
- return apiInstance(originalRequest);
300
- }
301
- } catch (refreshError) {
302
- console.error('Auth refresh failed:', refreshError);
303
- }
304
-
305
- // Clear auth data and redirect to login
306
- clearAuthData();
307
-
308
- // Dispatch auth failure event
309
- if (typeof window !== 'undefined') {
310
- window.dispatchEvent(new CustomEvent('auth:failure', {
311
- detail: { error: formattedError }
312
- }));
313
- }
314
- }
315
-
316
- // Handle retryable errors
176
+ // Handle retryable errors with exponential backoff
317
177
  const isRetryable = [408, 429, 500, 502, 503, 504].includes(formattedError.status) ||
318
- formattedError.code === 'NETWORK_ERROR';
319
-
178
+ formattedError.code === 'NETWORK_ERROR';
179
+
320
180
  if (isRetryable && originalRequest._retry < CONFIG.retryAttempts) {
321
181
  originalRequest._retry += 1;
322
-
323
182
  const delay = getRetryDelay(originalRequest._retry);
324
183
  console.log(`⏳ Retrying request (${originalRequest._retry}/${CONFIG.retryAttempts}) in ${delay}ms`);
325
-
326
184
  await new Promise(resolve => setTimeout(resolve, delay));
327
185
  return apiInstance(originalRequest);
328
186
  }
329
187
 
330
- // Handle rate limiting
188
+ // Attach retry-after duration when rate-limited
331
189
  if (formattedError.status === 429) {
332
190
  const retryAfter = error.response?.headers['retry-after'];
333
191
  if (retryAfter) {
@@ -350,26 +208,24 @@ const createApiInstance = () => {
350
208
  'Content-Type': 'application/json',
351
209
  },
352
210
  paramsSerializer: {
353
- serialize: (params) => qs.stringify(params, {
211
+ serialize: (params) => qs.stringify(params, {
354
212
  indices: false,
355
213
  skipNulls: true,
356
214
  arrayFormat: 'brackets',
357
215
  }),
358
216
  },
359
- withCredentials: true,
360
- // Disable automatic JSON parsing for better error handling
217
+ // withCredentials intentionally omitted — cookies are not supported in React Native
361
218
  transformResponse: [
362
219
  (data) => {
363
220
  try {
364
221
  return JSON.parse(data);
365
- } catch (error) {
222
+ } catch {
366
223
  return data;
367
224
  }
368
- }
225
+ },
369
226
  ],
370
227
  });
371
228
 
372
- // Add interceptors
373
229
  instance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
374
230
  instance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
375
231
 
@@ -385,17 +241,16 @@ const apiInstance = createApiInstance();
385
241
  const API = {
386
242
  // Main axios instance
387
243
  instance: apiInstance,
388
-
244
+
389
245
  // Direct access to axios methods
390
- get: apiInstance.get.bind(apiInstance),
391
- post: apiInstance.post.bind(apiInstance),
392
- put: apiInstance.put.bind(apiInstance),
393
- patch: apiInstance.patch.bind(apiInstance),
394
- delete: apiInstance.delete.bind(apiInstance),
395
- head: apiInstance.head.bind(apiInstance),
246
+ get: apiInstance.get.bind(apiInstance),
247
+ post: apiInstance.post.bind(apiInstance),
248
+ put: apiInstance.put.bind(apiInstance),
249
+ patch: apiInstance.patch.bind(apiInstance),
250
+ delete: apiInstance.delete.bind(apiInstance),
251
+ head: apiInstance.head.bind(apiInstance),
396
252
  options: apiInstance.options.bind(apiInstance),
397
253
 
398
- // Utility methods
399
254
  /**
400
255
  * Makes a request with custom configuration
401
256
  * @param {Object} config - Request configuration
@@ -413,65 +268,11 @@ const API = {
413
268
  ...apiInstance.defaults,
414
269
  ...customConfig,
415
270
  });
416
-
417
271
  customInstance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
418
272
  customInstance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
419
-
420
273
  return customInstance;
421
274
  },
422
275
 
423
- /**
424
- * Sets authentication token
425
- * @param {string} token - Auth token
426
- * @param {Object} userData - User data
427
- * @returns {boolean} Success status
428
- */
429
- setAuth: (token, userData = {}) => {
430
- try {
431
- const authData = {
432
- token,
433
- user: userData,
434
- timestamp: Date.now(),
435
- expiresAt: userData.expiresAt,
436
- };
437
-
438
- return CacheFactory.local.set(CONFIG.cacheKeys.auth, authData);
439
- } catch (error) {
440
- console.error('Failed to set auth:', error);
441
- return false;
442
- }
443
- },
444
-
445
- /**
446
- * Clears authentication
447
- * @returns {boolean} Success status
448
- */
449
- clearAuth: () => {
450
- try {
451
- clearAuthData();
452
- return true;
453
- } catch (error) {
454
- console.error('Failed to clear auth:', error);
455
- return false;
456
- }
457
- },
458
-
459
- /**
460
- * Gets current auth status
461
- * @returns {Object} Auth status
462
- */
463
- getAuthStatus: () => {
464
- const authData = getAuthData();
465
- const isAuthenticated = !!(authData?.token && !isTokenExpired(authData.token));
466
-
467
- return {
468
- isAuthenticated,
469
- user: authData?.user || null,
470
- token: authData?.token || null,
471
- expiresAt: authData?.expiresAt || null,
472
- };
473
- },
474
-
475
276
  /**
476
277
  * Configures global request defaults
477
278
  * @param {Object} config - Configuration object
@@ -487,17 +288,9 @@ const API = {
487
288
  healthCheck: async () => {
488
289
  try {
489
290
  const response = await apiInstance.get('health', { timeout: 5000 });
490
- return {
491
- status: 'healthy',
492
- response: response.data,
493
- timestamp: Date.now(),
494
- };
291
+ return { status: 'healthy', response: response.data, timestamp: Date.now() };
495
292
  } catch (error) {
496
- return {
497
- status: 'unhealthy',
498
- error: formatError(error),
499
- timestamp: Date.now(),
500
- };
293
+ return { status: 'unhealthy', error: formatError(error), timestamp: Date.now() };
501
294
  }
502
295
  },
503
296
 
@@ -510,9 +303,7 @@ const API = {
510
303
  */
511
304
  uploadFile: (url, formData, onProgress) => {
512
305
  return apiInstance.post(url, formData, {
513
- headers: {
514
- 'Content-Type': 'multipart/form-data',
515
- },
306
+ headers: { 'Content-Type': 'multipart/form-data' },
516
307
  onUploadProgress: (progressEvent) => {
517
308
  if (onProgress && progressEvent.total) {
518
309
  const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
@@ -523,96 +314,24 @@ const API = {
523
314
  },
524
315
 
525
316
  // Cache utility methods
526
- /**
527
- * Gets cached data
528
- * @param {string} key - Cache key
529
- * @param {any} defaultValue - Default value if not found
530
- * @returns {any} Cached data or default value
531
- */
532
- getCached: (key, defaultValue = null) => {
533
- return CacheFactory.local.get(key, defaultValue);
534
- },
317
+ getCached: (key, defaultValue = null) => CacheFactory.local.get(key, defaultValue),
318
+ setCached: (key, value) => CacheFactory.local.set(key, value),
319
+ removeCached: (key) => CacheFactory.local.remove(key),
320
+ clearCache: () => CacheFactory.local.clear(),
321
+ getCacheStats: () => CacheFactory.local.getStats(),
535
322
 
536
- /**
537
- * Sets cached data
538
- * @param {string} key - Cache key
539
- * @param {any} value - Value to cache
540
- * @returns {boolean} Success status
541
- */
542
- setCached: (key, value) => {
543
- return CacheFactory.local.set(key, value);
544
- },
545
-
546
- /**
547
- * Removes cached data
548
- * @param {string} key - Cache key
549
- * @returns {boolean} Success status
550
- */
551
- removeCached: (key) => {
552
- return CacheFactory.local.remove(key);
553
- },
554
-
555
- /**
556
- * Clears all cached data
557
- * @returns {boolean} Success status
558
- */
559
- clearCache: () => {
560
- return CacheFactory.local.clear();
561
- },
562
-
563
- /**
564
- * Gets cache statistics
565
- * @returns {Object} Cache stats
566
- */
567
- getCacheStats: () => {
568
- return CacheFactory.local.getStats();
569
- },
570
-
571
- /**
572
- * Bulk cache operations
573
- */
323
+ /** Bulk cache operations */
574
324
  cache: {
575
- /**
576
- * Gets multiple cached items
577
- * @param {string[]} keys - Cache keys
578
- * @returns {Object} Key-value pairs
579
- */
580
- getMultiple: (keys) => {
581
- return CacheFactory.local.bulk.get(keys);
582
- },
583
-
584
- /**
585
- * Sets multiple cached items
586
- * @param {Object} items - Key-value pairs
587
- * @returns {Object} Success status for each key
588
- */
589
- setMultiple: (items) => {
590
- return CacheFactory.local.bulk.set(items);
591
- },
592
-
593
- /**
594
- * Removes multiple cached items
595
- * @param {string[]} keys - Cache keys
596
- * @returns {Object} Success status for each key
597
- */
598
- removeMultiple: (keys) => {
599
- return CacheFactory.local.bulk.remove(keys);
600
- },
325
+ getMultiple: (keys) => CacheFactory.local.bulk.get(keys),
326
+ setMultiple: (items) => CacheFactory.local.bulk.set(items),
327
+ removeMultiple: (keys) => CacheFactory.local.bulk.remove(keys),
601
328
  },
602
329
 
603
330
  // Configuration access
604
331
  config: CONFIG,
605
332
  getBaseUrl,
606
- setBaseUrl
333
+ setBaseUrl,
607
334
  };
608
335
 
609
- // Add event listener for auth failures (optional)
610
- if (typeof window !== 'undefined') {
611
- window.addEventListener('auth:failure', (event) => {
612
- console.warn('Authentication failed:', event.detail.error);
613
- // Could trigger a redirect to login page here
614
- });
615
- }
616
-
617
336
  export { API };
618
337
  export default API;