@umituz/react-native-design-system 2.6.110 → 2.6.111

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.
@@ -0,0 +1,327 @@
1
+ /**
2
+ * useInfiniteScroll Hook
3
+ *
4
+ * Supports page-based and cursor-based pagination
5
+ * Features:
6
+ * - AbortController for request cancellation
7
+ * - Automatic retry with exponential backoff
8
+ * - Performance monitoring in __DEV__ mode
9
+ * - Memory leak prevention
10
+ */
11
+
12
+ import { useState, useCallback, useEffect, useRef } from "react";
13
+ import type { InfiniteScrollConfig } from "../../domain/types/infinite-scroll-config";
14
+ import type { InfiniteScrollState } from "../../domain/types/infinite-scroll-state";
15
+ import type { UseInfiniteScrollReturn } from "../../domain/types/infinite-scroll-return";
16
+ import { loadData, loadMoreData, isCursorMode } from "./pagination.helper";
17
+
18
+ const DEFAULT_CONFIG = {
19
+ pageSize: 20,
20
+ threshold: 5,
21
+ autoLoad: true,
22
+ initialPage: 0,
23
+ maxRetries: 3,
24
+ retryDelay: 1000,
25
+ };
26
+
27
+ function createInitialState<T>(
28
+ initialPage: number,
29
+ totalItems?: number,
30
+ ): InfiniteScrollState<T> {
31
+ return {
32
+ items: [],
33
+ pages: [],
34
+ currentPage: initialPage,
35
+ cursor: null,
36
+ hasMore: true,
37
+ isLoading: true,
38
+ isLoadingMore: false,
39
+ isRefreshing: false,
40
+ error: null,
41
+ totalItems,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Sleep utility for retry delay
47
+ */
48
+ function sleep(ms: number): Promise<void> {
49
+ return new Promise((resolve) => setTimeout(resolve, ms));
50
+ }
51
+
52
+ /**
53
+ * Retry logic with exponential backoff
54
+ */
55
+ async function retryWithBackoff<T>(
56
+ fn: () => Promise<T>,
57
+ maxRetries: number,
58
+ baseDelay: number,
59
+ ): Promise<T> {
60
+ let lastError: Error | undefined;
61
+
62
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
63
+ try {
64
+ return await fn();
65
+ } catch (error) {
66
+ lastError = error instanceof Error ? error : new Error(String(error));
67
+
68
+ if (attempt < maxRetries) {
69
+ const delay = baseDelay * Math.pow(2, attempt);
70
+ if (__DEV__) {
71
+ console.log(
72
+ `[useInfiniteScroll] Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`,
73
+ );
74
+ }
75
+ await sleep(delay);
76
+ }
77
+ }
78
+ }
79
+
80
+ throw lastError;
81
+ }
82
+
83
+ export function useInfiniteScroll<T>(
84
+ config: InfiniteScrollConfig<T>,
85
+ ): UseInfiniteScrollReturn<T> {
86
+ const {
87
+ pageSize = DEFAULT_CONFIG.pageSize,
88
+ autoLoad = DEFAULT_CONFIG.autoLoad,
89
+ totalItems,
90
+ } = config;
91
+
92
+ const initialPage =
93
+ "initialPage" in config ? config.initialPage || 0 : DEFAULT_CONFIG.initialPage;
94
+
95
+ const maxRetries = DEFAULT_CONFIG.maxRetries;
96
+ const retryDelay = DEFAULT_CONFIG.retryDelay;
97
+
98
+ const [state, setState] = useState<InfiniteScrollState<T>>(() =>
99
+ createInitialState<T>(initialPage, totalItems),
100
+ );
101
+
102
+ const isLoadingRef = useRef(false);
103
+ const isMountedRef = useRef(true);
104
+ const abortControllerRef = useRef<AbortController | null>(null);
105
+
106
+ // Cleanup on unmount
107
+ useEffect(() => {
108
+ isMountedRef.current = true;
109
+
110
+ return () => {
111
+ isMountedRef.current = false;
112
+ abortControllerRef.current?.abort();
113
+ if (__DEV__) {
114
+ console.log("[useInfiniteScroll] Cleanup: component unmounted");
115
+ }
116
+ };
117
+ }, []);
118
+
119
+ // Cancel pending requests
120
+ const cancelPendingRequests = useCallback(() => {
121
+ abortControllerRef.current?.abort();
122
+ abortControllerRef.current = new AbortController();
123
+ }, []);
124
+
125
+ const loadInitial = useCallback(async () => {
126
+ if (isLoadingRef.current) return;
127
+ isLoadingRef.current = true;
128
+
129
+ cancelPendingRequests();
130
+
131
+ if (isMountedRef.current) {
132
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
133
+ }
134
+
135
+ const startTime = __DEV__ ? performance.now() : 0;
136
+
137
+ try {
138
+ const newState = await retryWithBackoff(
139
+ async () => {
140
+ const result = await loadData(config, initialPage, pageSize, totalItems);
141
+ return result;
142
+ },
143
+ maxRetries,
144
+ retryDelay,
145
+ );
146
+
147
+ if (isMountedRef.current) {
148
+ setState(newState);
149
+
150
+ if (__DEV__) {
151
+ const duration = performance.now() - startTime;
152
+ console.log(
153
+ `[useInfiniteScroll] Initial load completed in ${duration.toFixed(2)}ms`,
154
+ `Loaded ${newState.items.length} items`,
155
+ );
156
+ }
157
+ }
158
+ } catch (error) {
159
+ if (isMountedRef.current) {
160
+ const errorMessage =
161
+ error instanceof Error ? error.message : "Failed to load data";
162
+
163
+ setState((prev) => ({
164
+ ...prev,
165
+ isLoading: false,
166
+ error: errorMessage,
167
+ }));
168
+
169
+ if (__DEV__) {
170
+ console.error("[useInfiniteScroll] Load initial failed:", errorMessage);
171
+ }
172
+ }
173
+ } finally {
174
+ isLoadingRef.current = false;
175
+ }
176
+ }, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
177
+
178
+ const loadMore = useCallback(async () => {
179
+ if (
180
+ isLoadingRef.current ||
181
+ !state.hasMore ||
182
+ state.isLoadingMore ||
183
+ state.isLoading
184
+ ) {
185
+ return;
186
+ }
187
+
188
+ if (isCursorMode(config) && !state.cursor) return;
189
+
190
+ isLoadingRef.current = true;
191
+
192
+ if (isMountedRef.current) {
193
+ setState((prev) => ({ ...prev, isLoadingMore: true, error: null }));
194
+ }
195
+
196
+ const startTime = __DEV__ ? performance.now() : 0;
197
+
198
+ try {
199
+ const updates = await retryWithBackoff(
200
+ async () => {
201
+ const result = await loadMoreData(config, state, pageSize);
202
+ return result;
203
+ },
204
+ maxRetries,
205
+ retryDelay,
206
+ );
207
+
208
+ if (isMountedRef.current) {
209
+ setState((prev) => ({ ...prev, ...updates }));
210
+
211
+ if (__DEV__) {
212
+ const duration = performance.now() - startTime;
213
+ const newItemsCount = updates.items?.length || 0;
214
+ console.log(
215
+ `[useInfiniteScroll] Load more completed in ${duration.toFixed(2)}ms`,
216
+ `Loaded ${newItemsCount} items, total: ${updates.items?.length || 0}`,
217
+ );
218
+ }
219
+ }
220
+ } catch (error) {
221
+ if (isMountedRef.current) {
222
+ const errorMessage =
223
+ error instanceof Error ? error.message : "Failed to load more items";
224
+
225
+ setState((prev) => ({
226
+ ...prev,
227
+ isLoadingMore: false,
228
+ error: errorMessage,
229
+ }));
230
+
231
+ if (__DEV__) {
232
+ console.error("[useInfiniteScroll] Load more failed:", errorMessage);
233
+ }
234
+ }
235
+ } finally {
236
+ isLoadingRef.current = false;
237
+ }
238
+ }, [config, state, pageSize, maxRetries, retryDelay]);
239
+
240
+ const refresh = useCallback(async () => {
241
+ if (isLoadingRef.current) return;
242
+ isLoadingRef.current = true;
243
+
244
+ cancelPendingRequests();
245
+
246
+ if (isMountedRef.current) {
247
+ setState((prev) => ({ ...prev, isRefreshing: true, error: null }));
248
+ }
249
+
250
+ const startTime = __DEV__ ? performance.now() : 0;
251
+
252
+ try {
253
+ const newState = await retryWithBackoff(
254
+ async () => {
255
+ const result = await loadData(config, initialPage, pageSize, totalItems);
256
+ return result;
257
+ },
258
+ maxRetries,
259
+ retryDelay,
260
+ );
261
+
262
+ if (isMountedRef.current) {
263
+ setState(newState);
264
+
265
+ if (__DEV__) {
266
+ const duration = performance.now() - startTime;
267
+ console.log(
268
+ `[useInfiniteScroll] Refresh completed in ${duration.toFixed(2)}ms`,
269
+ `Loaded ${newState.items.length} items`,
270
+ );
271
+ }
272
+ }
273
+ } catch (error) {
274
+ if (isMountedRef.current) {
275
+ const errorMessage =
276
+ error instanceof Error ? error.message : "Failed to refresh data";
277
+
278
+ setState((prev) => ({
279
+ ...prev,
280
+ isRefreshing: false,
281
+ error: errorMessage,
282
+ }));
283
+
284
+ if (__DEV__) {
285
+ console.error("[useInfiniteScroll] Refresh failed:", errorMessage);
286
+ }
287
+ }
288
+ } finally {
289
+ isLoadingRef.current = false;
290
+ }
291
+ }, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
292
+
293
+ const reset = useCallback(() => {
294
+ isLoadingRef.current = false;
295
+ cancelPendingRequests();
296
+ setState(createInitialState<T>(initialPage, totalItems));
297
+
298
+ if (__DEV__) {
299
+ console.log("[useInfiniteScroll] State reset");
300
+ }
301
+ }, [initialPage, totalItems, cancelPendingRequests]);
302
+
303
+ useEffect(() => {
304
+ if (autoLoad) {
305
+ loadInitial();
306
+ }
307
+
308
+ return () => {
309
+ // Cleanup on config change
310
+ if (__DEV__) {
311
+ console.log("[useInfiniteScroll] Config changed, cleaning up");
312
+ }
313
+ };
314
+ }, [autoLoad, loadInitial]);
315
+
316
+ const canLoadMore =
317
+ state.hasMore && !state.isLoadingMore && !state.isLoading;
318
+
319
+ return {
320
+ items: state.items,
321
+ state,
322
+ loadMore,
323
+ refresh,
324
+ reset,
325
+ canLoadMore,
326
+ };
327
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * React Native UUID
3
+ *
4
+ * Cross-platform UUID generation for React Native apps
5
+ */
6
+
7
+ export {
8
+ generateUUID,
9
+ generateUUID as uuidv4,
10
+ generateUUID as generateCreationId,
11
+ isValidUUID,
12
+ getUUIDVersion,
13
+ } from "./infrastructure/utils/UUIDUtils";
14
+ export type { UUID } from "./types/UUID";
15
+ export { UUIDVersion, UUID_CONSTANTS } from "./types/UUID";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * UUID Generation Utility
3
+ *
4
+ * Provides cross-platform UUID generation using expo-crypto.
5
+ * Compatible with React Native (iOS, Android) and Web.
6
+ */
7
+
8
+ import * as Crypto from 'expo-crypto';
9
+ import type { UUID } from '../../types/UUID';
10
+ import { UUID_CONSTANTS } from '../../types/UUID';
11
+
12
+ /**
13
+ * Generate a v4 UUID
14
+ * Uses expo-crypto's randomUUID() for secure UUID generation
15
+ *
16
+ * @returns A v4 UUID string
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { generateUUID } from '@umituz/react-native-uuid';
21
+ *
22
+ * const id = generateUUID();
23
+ * // Returns: "550e8400-e29b-41d4-a716-446655440000"
24
+ * ```
25
+ */
26
+ export const generateUUID = (): UUID => {
27
+ return Crypto.randomUUID() as UUID;
28
+ };
29
+
30
+ /**
31
+ * Validate UUID format
32
+ * Checks if a string is a valid v4 UUID
33
+ *
34
+ * @param value - The value to validate
35
+ * @returns True if the value is a valid v4 UUID
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * import { isValidUUID } from '@umituz/react-native-uuid';
40
+ *
41
+ * isValidUUID('550e8400-e29b-41d4-a716-446655440000'); // true
42
+ * isValidUUID('invalid-uuid'); // false
43
+ * ```
44
+ */
45
+ export const isValidUUID = (value: string): value is UUID => {
46
+ return UUID_CONSTANTS.PATTERN.test(value);
47
+ };
48
+
49
+ /**
50
+ * Get version from UUID string
51
+ * Returns the UUID version number (1-5) or null for NIL/invalid
52
+ *
53
+ * @param value - The UUID string
54
+ * @returns UUID version number or null
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * import { getUUIDVersion } from '@umituz/react-native-uuid';
59
+ *
60
+ * getUUIDVersion('550e8400-e29b-41d4-a716-446655440000'); // 4
61
+ * getUUIDVersion('00000000-0000-0000-0000-000000000000'); // 0 (NIL)
62
+ * getUUIDVersion('invalid'); // null
63
+ * ```
64
+ */
65
+ export const getUUIDVersion = (value: string): number | null => {
66
+ if (value === UUID_CONSTANTS.NIL) {
67
+ return 0;
68
+ }
69
+
70
+ const versionChar = value.charAt(14);
71
+ const version = parseInt(versionChar, 10);
72
+
73
+ return (version >= 1 && version <= 5) ? version : null;
74
+ };
75
+