@umituz/react-native-storage 1.4.0 → 2.0.0
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/LICENSE +0 -0
- package/README.md +0 -0
- package/package.json +9 -7
- package/src/application/ports/IStorageRepository.ts +0 -0
- package/src/domain/constants/CacheDefaults.ts +64 -0
- package/src/domain/entities/CachedValue.ts +86 -0
- package/src/domain/entities/StorageResult.ts +18 -8
- package/src/domain/errors/StorageError.ts +0 -0
- package/src/domain/factories/StoreFactory.ts +33 -0
- package/src/domain/types/Store.ts +18 -0
- package/src/domain/utils/CacheKeyGenerator.ts +66 -0
- package/src/domain/value-objects/StorageKey.ts +0 -0
- package/src/index.ts +53 -1
- package/src/infrastructure/adapters/StorageService.ts +0 -0
- package/src/infrastructure/repositories/AsyncStorageRepository.ts +0 -0
- package/src/presentation/hooks/usePersistedState.ts +34 -0
- package/src/presentation/hooks/usePersistentCache.ts +163 -0
- package/src/presentation/hooks/useStorage.ts +0 -0
- package/src/presentation/hooks/useStorageState.ts +0 -0
- package/src/presentation/hooks/useStore.ts +13 -0
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-storage",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Zustand state management with AsyncStorage persistence and type-safe storage operations for React Native",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
9
|
-
"lint": "tsc --noEmit"
|
|
10
|
-
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
11
|
-
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
12
|
-
"version:major": "npm version major -m 'chore: release v%s'"
|
|
9
|
+
"lint": "tsc --noEmit"
|
|
13
10
|
},
|
|
14
11
|
"keywords": [
|
|
15
12
|
"react-native",
|
|
16
13
|
"storage",
|
|
17
14
|
"async-storage",
|
|
15
|
+
"zustand",
|
|
16
|
+
"state-management",
|
|
17
|
+
"persist",
|
|
18
18
|
"ddd",
|
|
19
19
|
"domain-driven-design",
|
|
20
20
|
"type-safe"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"@react-native-async-storage/async-storage": "^1.21.0",
|
|
30
|
+
"zustand": "^5.0.0",
|
|
30
31
|
"react": ">=18.2.0",
|
|
31
32
|
"react-native": ">=0.74.0"
|
|
32
33
|
},
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
"@types/react-native": "^0.73.0",
|
|
37
38
|
"react": "^18.2.0",
|
|
38
39
|
"react-native": "^0.74.0",
|
|
39
|
-
"typescript": "^5.3.3"
|
|
40
|
+
"typescript": "^5.3.3",
|
|
41
|
+
"zustand": "^5.0.0"
|
|
40
42
|
},
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"access": "public"
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Default Constants
|
|
3
|
+
* Domain layer - Default values for caching
|
|
4
|
+
*
|
|
5
|
+
* General-purpose constants for any app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Time constants in milliseconds
|
|
10
|
+
*/
|
|
11
|
+
export const TIME_MS = {
|
|
12
|
+
SECOND: 1000,
|
|
13
|
+
MINUTE: 60 * 1000,
|
|
14
|
+
HOUR: 60 * 60 * 1000,
|
|
15
|
+
DAY: 24 * 60 * 60 * 1000,
|
|
16
|
+
WEEK: 7 * 24 * 60 * 60 * 1000,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default TTL values for different cache types
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_TTL = {
|
|
23
|
+
/**
|
|
24
|
+
* Very short cache (1 minute)
|
|
25
|
+
* Use for: Real-time data, live updates
|
|
26
|
+
*/
|
|
27
|
+
VERY_SHORT: TIME_MS.MINUTE,
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Short cache (5 minutes)
|
|
31
|
+
* Use for: Frequently changing data
|
|
32
|
+
*/
|
|
33
|
+
SHORT: 5 * TIME_MS.MINUTE,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Medium cache (30 minutes)
|
|
37
|
+
* Use for: Moderately changing data, user-specific content
|
|
38
|
+
*/
|
|
39
|
+
MEDIUM: 30 * TIME_MS.MINUTE,
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Long cache (2 hours)
|
|
43
|
+
* Use for: Slowly changing data, public content
|
|
44
|
+
*/
|
|
45
|
+
LONG: 2 * TIME_MS.HOUR,
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Very long cache (24 hours)
|
|
49
|
+
* Use for: Rarely changing data, master data
|
|
50
|
+
*/
|
|
51
|
+
VERY_LONG: TIME_MS.DAY,
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Permanent cache (7 days)
|
|
55
|
+
* Use for: Static content, app configuration
|
|
56
|
+
*/
|
|
57
|
+
PERMANENT: TIME_MS.WEEK,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Cache version for global invalidation
|
|
62
|
+
* Increment this to invalidate all caches across the app
|
|
63
|
+
*/
|
|
64
|
+
export const CACHE_VERSION = 1;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cached Value Entity
|
|
3
|
+
* Domain layer - Represents a cached value with TTL metadata
|
|
4
|
+
*
|
|
5
|
+
* General-purpose cache entity for any app that needs persistent caching
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cached value with time-to-live metadata
|
|
10
|
+
* Generic type T can be any serializable data
|
|
11
|
+
*/
|
|
12
|
+
export interface CachedValue<T> {
|
|
13
|
+
/**
|
|
14
|
+
* The actual cached data
|
|
15
|
+
*/
|
|
16
|
+
value: T;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Timestamp when the value was cached (milliseconds)
|
|
20
|
+
*/
|
|
21
|
+
cachedAt: number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Timestamp when the cache expires (milliseconds)
|
|
25
|
+
*/
|
|
26
|
+
expiresAt: number;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optional version for cache invalidation
|
|
30
|
+
* Increment version to invalidate all caches
|
|
31
|
+
*/
|
|
32
|
+
version?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a new cached value with TTL
|
|
37
|
+
* @param value - Data to cache
|
|
38
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
39
|
+
* @param version - Optional version number
|
|
40
|
+
*/
|
|
41
|
+
export function createCachedValue<T>(
|
|
42
|
+
value: T,
|
|
43
|
+
ttlMs: number,
|
|
44
|
+
version?: number,
|
|
45
|
+
): CachedValue<T> {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
return {
|
|
48
|
+
value,
|
|
49
|
+
cachedAt: now,
|
|
50
|
+
expiresAt: now + ttlMs,
|
|
51
|
+
version,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if cached value is expired
|
|
57
|
+
* @param cached - Cached value to check
|
|
58
|
+
* @param currentVersion - Optional current version to check against
|
|
59
|
+
*/
|
|
60
|
+
export function isCacheExpired<T>(
|
|
61
|
+
cached: CachedValue<T>,
|
|
62
|
+
currentVersion?: number,
|
|
63
|
+
): boolean {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const timeExpired = now > cached.expiresAt;
|
|
66
|
+
const versionMismatch = currentVersion !== undefined && cached.version !== currentVersion;
|
|
67
|
+
return timeExpired || versionMismatch;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get remaining TTL in milliseconds
|
|
72
|
+
* Returns 0 if expired
|
|
73
|
+
*/
|
|
74
|
+
export function getRemainingTTL<T>(cached: CachedValue<T>): number {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const remaining = cached.expiresAt - now;
|
|
77
|
+
return Math.max(0, remaining);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get cache age in milliseconds
|
|
82
|
+
*/
|
|
83
|
+
export function getCacheAge<T>(cached: CachedValue<T>): number {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
return now - cached.cachedAt;
|
|
86
|
+
}
|
|
@@ -34,16 +34,28 @@ export const failure = <T>(error: StorageError, fallback?: T): StorageResult<T>
|
|
|
34
34
|
fallback,
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Type guard for success result
|
|
39
|
+
*/
|
|
40
|
+
export const isSuccess = <T>(result: StorageResult<T>): result is { success: true; data: T } => {
|
|
41
|
+
return result.success === true;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Type guard for failure result
|
|
46
|
+
*/
|
|
47
|
+
export const isFailure = <T>(result: StorageResult<T>): result is { success: false; error: StorageError; fallback?: T } => {
|
|
48
|
+
return result.success === false;
|
|
49
|
+
};
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
52
|
* Unwrap result with default value
|
|
39
53
|
*/
|
|
40
54
|
export const unwrap = <T>(result: StorageResult<T>, defaultValue: T): T => {
|
|
41
|
-
if (result
|
|
55
|
+
if (isSuccess(result)) {
|
|
42
56
|
return result.data;
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
const failedResult = result;
|
|
46
|
-
return failedResult.fallback !== undefined ? failedResult.fallback : defaultValue;
|
|
58
|
+
return result.fallback !== undefined ? result.fallback : defaultValue;
|
|
47
59
|
};
|
|
48
60
|
|
|
49
61
|
/**
|
|
@@ -53,10 +65,8 @@ export const map = <T, U>(
|
|
|
53
65
|
result: StorageResult<T>,
|
|
54
66
|
fn: (data: T) => U
|
|
55
67
|
): StorageResult<U> => {
|
|
56
|
-
if (result
|
|
68
|
+
if (isSuccess(result)) {
|
|
57
69
|
return success(fn(result.data));
|
|
58
70
|
}
|
|
59
|
-
|
|
60
|
-
const failedResult = result;
|
|
61
|
-
return failure(failedResult.error, failedResult.fallback !== undefined ? fn(failedResult.fallback) : undefined);
|
|
71
|
+
return failure(result.error, result.fallback !== undefined ? fn(result.fallback) : undefined);
|
|
62
72
|
};
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store Factory
|
|
3
|
+
* Create Zustand stores with AsyncStorage persistence
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { create } from 'zustand';
|
|
7
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
8
|
+
import type { StoreConfig } from '../types/Store';
|
|
9
|
+
import { storageService } from '../../infrastructure/adapters/StorageService';
|
|
10
|
+
|
|
11
|
+
export function createStore<T extends object>(config: StoreConfig<T>) {
|
|
12
|
+
if (!config.persist) {
|
|
13
|
+
return create<T>(() => config.initialState);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return create<T>()(
|
|
17
|
+
persist(
|
|
18
|
+
() => config.initialState,
|
|
19
|
+
{
|
|
20
|
+
name: config.name,
|
|
21
|
+
storage: createJSONStorage(() => storageService),
|
|
22
|
+
version: config.version || 1,
|
|
23
|
+
partialize: config.partialize,
|
|
24
|
+
onRehydrateStorage: () => (state) => {
|
|
25
|
+
if (state && config.onRehydrate) {
|
|
26
|
+
config.onRehydrate(state);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
migrate: config.migrate as any,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface StoreConfig<T> {
|
|
6
|
+
name: string;
|
|
7
|
+
initialState: T;
|
|
8
|
+
persist?: boolean;
|
|
9
|
+
version?: number;
|
|
10
|
+
partialize?: (state: T) => Partial<T>;
|
|
11
|
+
onRehydrate?: (state: T) => void;
|
|
12
|
+
migrate?: (persistedState: unknown, version: number) => T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PersistedState<T> {
|
|
16
|
+
state: T;
|
|
17
|
+
version: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Key Generator
|
|
3
|
+
* Domain layer - Utility for generating consistent cache keys
|
|
4
|
+
*
|
|
5
|
+
* General-purpose key generation for any app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a cache key from prefix and identifier
|
|
10
|
+
* @param prefix - Cache key prefix (e.g., 'posts', 'products', 'user')
|
|
11
|
+
* @param id - Unique identifier
|
|
12
|
+
* @returns Formatted cache key
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* generateCacheKey('posts', '123') // 'cache:posts:123'
|
|
16
|
+
* generateCacheKey('user', 'profile') // 'cache:user:profile'
|
|
17
|
+
*/
|
|
18
|
+
export function generateCacheKey(prefix: string, id: string | number): string {
|
|
19
|
+
return `cache:${prefix}:${id}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate a cache key for a list/collection
|
|
24
|
+
* @param prefix - Cache key prefix
|
|
25
|
+
* @param params - Optional query parameters
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* generateListCacheKey('posts') // 'cache:posts:list'
|
|
29
|
+
* generateListCacheKey('posts', { page: 1 }) // 'cache:posts:list:page=1'
|
|
30
|
+
*/
|
|
31
|
+
export function generateListCacheKey(
|
|
32
|
+
prefix: string,
|
|
33
|
+
params?: Record<string, string | number>,
|
|
34
|
+
): string {
|
|
35
|
+
const base = `cache:${prefix}:list`;
|
|
36
|
+
if (!params) return base;
|
|
37
|
+
|
|
38
|
+
const paramString = Object.entries(params)
|
|
39
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
40
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
41
|
+
.join(':');
|
|
42
|
+
|
|
43
|
+
return `${base}:${paramString}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a cache key to extract prefix and id
|
|
48
|
+
* @param key - Cache key to parse
|
|
49
|
+
* @returns Object with prefix and id, or null if invalid
|
|
50
|
+
*/
|
|
51
|
+
export function parseCacheKey(key: string): { prefix: string; id: string } | null {
|
|
52
|
+
const parts = key.split(':');
|
|
53
|
+
if (parts.length < 3 || parts[0] !== 'cache') return null;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
prefix: parts[1],
|
|
57
|
+
id: parts.slice(2).join(':'),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a key is a cache key
|
|
63
|
+
*/
|
|
64
|
+
export function isCacheKey(key: string): boolean {
|
|
65
|
+
return key.startsWith('cache:');
|
|
66
|
+
}
|
|
File without changes
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - presentation: Hooks (React integration)
|
|
14
14
|
*
|
|
15
15
|
* Usage:
|
|
16
|
-
* import { useStorage, useStorageState,
|
|
16
|
+
* import { useStorage, useStorageState, createStore } from '@umituz/react-native-storage';
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
// =============================================================================
|
|
@@ -44,8 +44,52 @@ export {
|
|
|
44
44
|
failure,
|
|
45
45
|
unwrap,
|
|
46
46
|
map,
|
|
47
|
+
isSuccess,
|
|
48
|
+
isFailure,
|
|
47
49
|
} from './domain/entities/StorageResult';
|
|
48
50
|
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// DOMAIN LAYER - Cached Value Entity
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
export type { CachedValue } from './domain/entities/CachedValue';
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
createCachedValue,
|
|
59
|
+
isCacheExpired,
|
|
60
|
+
getRemainingTTL,
|
|
61
|
+
getCacheAge,
|
|
62
|
+
} from './domain/entities/CachedValue';
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// DOMAIN LAYER - Cache Utilities
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
generateCacheKey,
|
|
70
|
+
generateListCacheKey,
|
|
71
|
+
parseCacheKey,
|
|
72
|
+
isCacheKey,
|
|
73
|
+
} from './domain/utils/CacheKeyGenerator';
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// DOMAIN LAYER - Cache Constants
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
export { TIME_MS, DEFAULT_TTL, CACHE_VERSION } from './domain/constants/CacheDefaults';
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// DOMAIN LAYER - Store Types
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
export type { StoreConfig, PersistedState } from './domain/types/Store';
|
|
86
|
+
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// DOMAIN LAYER - Store Factory
|
|
89
|
+
// =============================================================================
|
|
90
|
+
|
|
91
|
+
export { createStore } from './domain/factories/StoreFactory';
|
|
92
|
+
|
|
49
93
|
// =============================================================================
|
|
50
94
|
// APPLICATION LAYER - Ports
|
|
51
95
|
// =============================================================================
|
|
@@ -72,3 +116,11 @@ export {
|
|
|
72
116
|
|
|
73
117
|
export { useStorage } from './presentation/hooks/useStorage';
|
|
74
118
|
export { useStorageState } from './presentation/hooks/useStorageState';
|
|
119
|
+
export { useStore } from './presentation/hooks/useStore';
|
|
120
|
+
export { usePersistedState } from './presentation/hooks/usePersistedState';
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
usePersistentCache,
|
|
124
|
+
type PersistentCacheOptions,
|
|
125
|
+
type PersistentCacheResult,
|
|
126
|
+
} from './presentation/hooks/usePersistentCache';
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePersistedState Hook
|
|
3
|
+
* Like useState but persisted to AsyncStorage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
7
|
+
import { storageRepository } from '../../infrastructure/repositories/AsyncStorageRepository';
|
|
8
|
+
|
|
9
|
+
export function usePersistedState<T>(
|
|
10
|
+
key: string,
|
|
11
|
+
initialValue: T
|
|
12
|
+
): [T, (value: T) => void, boolean] {
|
|
13
|
+
const [state, setState] = useState<T>(initialValue);
|
|
14
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
storageRepository.getItem(key, initialValue).then((result) => {
|
|
18
|
+
if (result.success) {
|
|
19
|
+
setState(result.data);
|
|
20
|
+
}
|
|
21
|
+
setIsLoaded(true);
|
|
22
|
+
});
|
|
23
|
+
}, [key]);
|
|
24
|
+
|
|
25
|
+
const setPersistedState = useCallback(
|
|
26
|
+
(value: T) => {
|
|
27
|
+
setState(value);
|
|
28
|
+
storageRepository.setItem(key, value);
|
|
29
|
+
},
|
|
30
|
+
[key]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return [state, setPersistedState, isLoaded];
|
|
34
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePersistentCache Hook
|
|
3
|
+
* Presentation layer - Hook for persistent caching with TTL
|
|
4
|
+
*
|
|
5
|
+
* General-purpose cache hook for any app
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
9
|
+
import { storageRepository } from '../../infrastructure/repositories/AsyncStorageRepository';
|
|
10
|
+
import type { CachedValue } from '../../domain/entities/CachedValue';
|
|
11
|
+
import { createCachedValue, isCacheExpired } from '../../domain/entities/CachedValue';
|
|
12
|
+
import { DEFAULT_TTL } from '../../domain/constants/CacheDefaults';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for persistent cache
|
|
16
|
+
*/
|
|
17
|
+
export interface PersistentCacheOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Time-to-live in milliseconds
|
|
20
|
+
* @default DEFAULT_TTL.MEDIUM (30 minutes)
|
|
21
|
+
*/
|
|
22
|
+
ttl?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cache version for invalidation
|
|
26
|
+
* Increment to invalidate existing caches
|
|
27
|
+
*/
|
|
28
|
+
version?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether cache is enabled
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Result from usePersistentCache hook
|
|
39
|
+
*/
|
|
40
|
+
export interface PersistentCacheResult<T> {
|
|
41
|
+
/**
|
|
42
|
+
* Cached data (null if not loaded or expired)
|
|
43
|
+
*/
|
|
44
|
+
data: T | null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether data is being loaded from storage
|
|
48
|
+
*/
|
|
49
|
+
isLoading: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether cached data is expired
|
|
53
|
+
*/
|
|
54
|
+
isExpired: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set data to cache
|
|
58
|
+
*/
|
|
59
|
+
setData: (value: T) => Promise<void>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear cached data
|
|
63
|
+
*/
|
|
64
|
+
clearData: () => Promise<void>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Refresh cache (reload from storage)
|
|
68
|
+
*/
|
|
69
|
+
refresh: () => Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Hook for persistent caching with TTL support
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const { data, setData, isExpired } = usePersistentCache<Post[]>('posts-page-1', {
|
|
78
|
+
* ttl: TIME_MS.HOUR,
|
|
79
|
+
* version: 1,
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* // Check if need to fetch
|
|
83
|
+
* if (!data || isExpired) {
|
|
84
|
+
* const freshData = await fetchPosts();
|
|
85
|
+
* await setData(freshData);
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function usePersistentCache<T>(
|
|
90
|
+
key: string,
|
|
91
|
+
options: PersistentCacheOptions = {},
|
|
92
|
+
): PersistentCacheResult<T> {
|
|
93
|
+
const { ttl = DEFAULT_TTL.MEDIUM, version, enabled = true } = options;
|
|
94
|
+
|
|
95
|
+
const [data, setDataState] = useState<T | null>(null);
|
|
96
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
97
|
+
const [isExpired, setIsExpired] = useState(false);
|
|
98
|
+
|
|
99
|
+
const loadFromStorage = useCallback(async () => {
|
|
100
|
+
if (!enabled) {
|
|
101
|
+
setIsLoading(false);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const result = await storageRepository.getString(key, '');
|
|
107
|
+
|
|
108
|
+
if (result.success && result.data) {
|
|
109
|
+
const cached = JSON.parse(result.data) as CachedValue<T>;
|
|
110
|
+
const expired = isCacheExpired(cached, version);
|
|
111
|
+
|
|
112
|
+
setDataState(cached.value);
|
|
113
|
+
setIsExpired(expired);
|
|
114
|
+
} else {
|
|
115
|
+
setDataState(null);
|
|
116
|
+
setIsExpired(true);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
setDataState(null);
|
|
120
|
+
setIsExpired(true);
|
|
121
|
+
} finally {
|
|
122
|
+
setIsLoading(false);
|
|
123
|
+
}
|
|
124
|
+
}, [key, version, enabled]);
|
|
125
|
+
|
|
126
|
+
const setData = useCallback(
|
|
127
|
+
async (value: T) => {
|
|
128
|
+
if (!enabled) return;
|
|
129
|
+
|
|
130
|
+
const cached = createCachedValue(value, ttl, version);
|
|
131
|
+
await storageRepository.setString(key, JSON.stringify(cached));
|
|
132
|
+
setDataState(value);
|
|
133
|
+
setIsExpired(false);
|
|
134
|
+
},
|
|
135
|
+
[key, ttl, version, enabled],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const clearData = useCallback(async () => {
|
|
139
|
+
if (!enabled) return;
|
|
140
|
+
|
|
141
|
+
await storageRepository.removeItem(key);
|
|
142
|
+
setDataState(null);
|
|
143
|
+
setIsExpired(true);
|
|
144
|
+
}, [key, enabled]);
|
|
145
|
+
|
|
146
|
+
const refresh = useCallback(async () => {
|
|
147
|
+
setIsLoading(true);
|
|
148
|
+
await loadFromStorage();
|
|
149
|
+
}, [loadFromStorage]);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
loadFromStorage();
|
|
153
|
+
}, [loadFromStorage]);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
data,
|
|
157
|
+
isLoading,
|
|
158
|
+
isExpired,
|
|
159
|
+
setData,
|
|
160
|
+
clearData,
|
|
161
|
+
refresh,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStore Hook
|
|
3
|
+
* Helper for creating stores in components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { createStore } from '../../domain/factories/StoreFactory';
|
|
8
|
+
import type { StoreConfig } from '../../domain/types/Store';
|
|
9
|
+
|
|
10
|
+
export function useStore<T extends object>(config: StoreConfig<T>) {
|
|
11
|
+
const store = useMemo(() => createStore(config), [config.name]);
|
|
12
|
+
return store;
|
|
13
|
+
}
|