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