@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 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": "1.4.0",
4
- "description": "Domain-Driven Design storage system for React Native apps with type-safe AsyncStorage operations",
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.success) {
55
+ if (isSuccess(result)) {
42
56
  return result.data;
43
57
  }
44
- // result is now narrowed to failure type
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.success) {
68
+ if (isSuccess(result)) {
57
69
  return success(fn(result.data));
58
70
  }
59
- // result is now narrowed to failure type
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, StorageKey } from '@umituz/react-native-storage';
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
@@ -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
+ }