@umituz/react-native-ai-generation-content 1.89.59 → 1.89.61
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 +1 -1
- package/src/domain/entities/polling.types.ts +4 -4
- package/src/domains/background/infrastructure/utils/polling-interval.util.ts +11 -8
- package/src/domains/background/presentation/hooks/use-background-generation.ts +4 -5
- package/src/domains/background/presentation/hooks/use-pending-jobs.ts +1 -1
- package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts +2 -10
- package/src/domains/creations/presentation/components/CreationCard.tsx +2 -2
- package/src/domains/creations/presentation/components/CreationsFilterBar.tsx +2 -2
- package/src/domains/creations/presentation/hooks/useCreations.ts +17 -13
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +8 -7
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +16 -9
- package/src/infrastructure/config/env.config.ts +5 -5
- package/src/infrastructure/services/generation-orchestrator.service.ts +3 -2
- package/src/infrastructure/utils/photo-generation/photo-preparation.util.ts +9 -3
- package/src/shared/hooks/index.ts +1 -0
- package/src/shared/hooks/useStableCallback.ts +75 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/utils/calculations.util.ts +366 -0
- package/src/shared/utils/credit.ts +17 -0
- package/src/shared/utils/debounce.util.ts +56 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.89.
|
|
3
|
+
"version": "1.89.61",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -13,10 +13,10 @@ export interface PollingConfig {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const DEFAULT_POLLING_CONFIG: PollingConfig = {
|
|
16
|
-
maxAttempts: 60
|
|
17
|
-
initialIntervalMs:
|
|
18
|
-
maxIntervalMs:
|
|
19
|
-
backoffMultiplier: 1.2
|
|
16
|
+
maxAttempts: 40, // Reduced from 60 - 40 attempts with backoff = ~5 minutes total
|
|
17
|
+
initialIntervalMs: 1500, // Increased from 1000ms - less aggressive initial polling
|
|
18
|
+
maxIntervalMs: 5000, // Increased from 3000ms - longer intervals between retries
|
|
19
|
+
backoffMultiplier: 1.3, // Increased from 1.2 - faster exponential backoff
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
export interface PollingState {
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Polling Interval Calculator
|
|
3
|
+
* Re-exports centralized calculation utility
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import type { PollingConfig } from "../../../../domain/entities/polling.types";
|
|
7
|
+
import { calculatePollingInterval as calculateInterval } from "../../../../shared/utils/calculations.util";
|
|
6
8
|
|
|
7
9
|
export interface IntervalOptions {
|
|
8
10
|
attempt: number;
|
|
9
11
|
config: PollingConfig;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Calculate polling interval using centralized utility
|
|
16
|
+
*/
|
|
12
17
|
export function calculatePollingInterval(options: IntervalOptions): number {
|
|
13
18
|
const { attempt, config } = options;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const interval = initialIntervalMs * Math.pow(backoffMultiplier, attempt - 1);
|
|
21
|
-
return Math.min(interval, maxIntervalMs);
|
|
19
|
+
return calculateInterval({
|
|
20
|
+
attempt,
|
|
21
|
+
initialIntervalMs: config.initialIntervalMs,
|
|
22
|
+
maxIntervalMs: config.maxIntervalMs,
|
|
23
|
+
backoffMultiplier: config.backoffMultiplier,
|
|
24
|
+
});
|
|
22
25
|
}
|
|
@@ -2,6 +2,7 @@ import { useCallback, useRef, useState, useMemo } from "react";
|
|
|
2
2
|
import { usePendingJobs } from "./use-pending-jobs";
|
|
3
3
|
import { executeDirectGeneration, executeQueuedJob } from "../../infrastructure/executors/backgroundJobExecutor";
|
|
4
4
|
import { DEFAULT_QUEUE_CONFIG } from "../../domain/entities/job.types";
|
|
5
|
+
import { calculateFilteredCount } from "../../../../shared/utils/calculations.util";
|
|
5
6
|
import type {
|
|
6
7
|
UseBackgroundGenerationOptions,
|
|
7
8
|
UseBackgroundGenerationReturn,
|
|
@@ -90,14 +91,12 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
|
|
|
90
91
|
[removeJob, onAllComplete],
|
|
91
92
|
);
|
|
92
93
|
|
|
93
|
-
// Calculate active jobs
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
() => jobs.filter((job) => job.status === "processing" || job.status === "queued"),
|
|
94
|
+
// Calculate active jobs more efficiently - use centralized utility
|
|
95
|
+
const activeJobCount = useMemo(
|
|
96
|
+
() => calculateFilteredCount(jobs, (job) => job.status === "processing" || job.status === "queued"),
|
|
97
97
|
[jobs]
|
|
98
98
|
);
|
|
99
99
|
|
|
100
|
-
const activeJobCount = activeJobs.length;
|
|
101
100
|
const hasActiveJobs = activeJobCount > 0;
|
|
102
101
|
|
|
103
102
|
return {
|
|
@@ -40,7 +40,7 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
40
40
|
queryKey,
|
|
41
41
|
queryFn: () => [],
|
|
42
42
|
staleTime: Infinity,
|
|
43
|
-
gcTime:
|
|
43
|
+
gcTime: 5 * 60 * 1000, // 5 minutes - reduced from 30min to prevent memory bloat
|
|
44
44
|
enabled: options.enabled !== false,
|
|
45
45
|
});
|
|
46
46
|
|
package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { imageModerator } from "./moderators/image.moderator";
|
|
|
17
17
|
import { videoModerator } from "./moderators/video.moderator";
|
|
18
18
|
import { voiceModerator } from "./moderators/voice.moderator";
|
|
19
19
|
import { rulesRegistry } from "../rules/rules-registry";
|
|
20
|
+
import { calculateConfidenceScore } from "../../../../shared/utils/calculations.util";
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
interface ServiceConfig {
|
|
@@ -95,16 +96,7 @@ class ContentModerationService {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
private calculateConfidence(violations: Violation[]): number {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const weights = { critical: 1.0, high: 0.75, medium: 0.5, low: 0.25 };
|
|
101
|
-
const score = violations.reduce(
|
|
102
|
-
(sum, v) => sum + (weights[v.severity] || 0.25),
|
|
103
|
-
0
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// Only divide by 2 if we have violations, otherwise return 1.0
|
|
107
|
-
return violations.length > 0 ? Math.min(1.0, score / Math.max(1, violations.length)) : 1.0;
|
|
99
|
+
return calculateConfidenceScore(violations);
|
|
108
100
|
}
|
|
109
101
|
|
|
110
102
|
private determineAction(
|
|
@@ -19,7 +19,7 @@ import { getPreviewUrl, getCreationTitle } from "../../domain/utils";
|
|
|
19
19
|
|
|
20
20
|
const EMPTY_CALLBACKS: CreationCardCallbacks = {};
|
|
21
21
|
|
|
22
|
-
export function CreationCard({
|
|
22
|
+
export const CreationCard = React.memo<CreationCardProps>(function CreationCard({
|
|
23
23
|
creation,
|
|
24
24
|
callbacks = EMPTY_CALLBACKS,
|
|
25
25
|
showBadges = true,
|
|
@@ -161,4 +161,4 @@ export function CreationCard({
|
|
|
161
161
|
</View>
|
|
162
162
|
</TouchableOpacity>
|
|
163
163
|
);
|
|
164
|
-
}
|
|
164
|
+
});
|
|
@@ -10,7 +10,7 @@ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
|
10
10
|
import { createFilterButtons } from "../../../../shared/utils/filters";
|
|
11
11
|
import type { CreationsFilterBarProps, MediaFilterLabels, StatusFilterLabels, FilterButton } from "./CreationsFilterBar.types";
|
|
12
12
|
|
|
13
|
-
export function CreationsFilterBar({
|
|
13
|
+
export const CreationsFilterBar = React.memo<CreationsFilterBarProps>(function CreationsFilterBar({
|
|
14
14
|
filters,
|
|
15
15
|
showClearButton = true,
|
|
16
16
|
clearLabel = "Clear",
|
|
@@ -128,7 +128,7 @@ export function CreationsFilterBar({
|
|
|
128
128
|
</ScrollView>
|
|
129
129
|
</View>
|
|
130
130
|
);
|
|
131
|
-
}
|
|
131
|
+
});
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
@@ -37,22 +37,26 @@ export function useCreations({
|
|
|
37
37
|
}
|
|
38
38
|
}, []);
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// Use refs for callbacks to maintain stable function references
|
|
41
|
+
const onDataCallbackRef = useRef<(creations: Creation[]) => void>();
|
|
42
|
+
const onErrorCallbackRef = useRef<(err: Error) => void>();
|
|
43
|
+
|
|
44
|
+
onDataCallbackRef.current = (creations: Creation[]) => {
|
|
41
45
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
42
46
|
console.log("[useCreations] Realtime update:", creations.length);
|
|
43
47
|
}
|
|
44
48
|
setData(creations);
|
|
45
49
|
setIsLoading(false);
|
|
46
50
|
setError(null);
|
|
47
|
-
}
|
|
51
|
+
};
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
onErrorCallbackRef.current = (err: Error) => {
|
|
50
54
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
55
|
console.error("[useCreations] Realtime listener error:", err);
|
|
52
56
|
}
|
|
53
57
|
setError(err);
|
|
54
58
|
setIsLoading(false);
|
|
55
|
-
}
|
|
59
|
+
};
|
|
56
60
|
|
|
57
61
|
useEffect(() => {
|
|
58
62
|
if (!userId || !enabled) {
|
|
@@ -69,21 +73,21 @@ export function useCreations({
|
|
|
69
73
|
setIsLoading(true);
|
|
70
74
|
setError(null);
|
|
71
75
|
|
|
72
|
-
let timeoutId: ReturnType<typeof setTimeout
|
|
76
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
73
77
|
|
|
74
78
|
const handleData = (creations: Creation[]) => {
|
|
75
|
-
clearTimeout(timeoutId);
|
|
76
|
-
|
|
79
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
80
|
+
onDataCallbackRef.current?.(creations);
|
|
77
81
|
};
|
|
78
82
|
|
|
79
83
|
const handleError = (err: Error) => {
|
|
80
|
-
clearTimeout(timeoutId);
|
|
81
|
-
|
|
84
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
85
|
+
onErrorCallbackRef.current?.(err);
|
|
82
86
|
};
|
|
83
87
|
|
|
84
88
|
const unsubscribe = repository.subscribeToAll(userId, handleData, handleError);
|
|
85
89
|
|
|
86
|
-
// Fallback timeout: if Firestore doesn't respond in
|
|
90
|
+
// Fallback timeout: if Firestore doesn't respond in 8s, stop loading
|
|
87
91
|
timeoutId = setTimeout(() => {
|
|
88
92
|
if (!isMounted) return;
|
|
89
93
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -91,17 +95,17 @@ export function useCreations({
|
|
|
91
95
|
}
|
|
92
96
|
setData((currentData) => currentData ?? []);
|
|
93
97
|
setIsLoading(false);
|
|
94
|
-
},
|
|
98
|
+
}, 8000);
|
|
95
99
|
|
|
96
100
|
return () => {
|
|
97
101
|
isMounted = false;
|
|
98
|
-
clearTimeout(timeoutId);
|
|
102
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
99
103
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
104
|
console.log("[useCreations] Cleaning up realtime listener");
|
|
101
105
|
}
|
|
102
106
|
unsubscribe();
|
|
103
107
|
};
|
|
104
|
-
}, [userId, repository, enabled]);
|
|
108
|
+
}, [userId, repository, enabled]);
|
|
105
109
|
|
|
106
110
|
return { data, isLoading, error, refetch };
|
|
107
111
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../../../generation/wizard/presentation/hooks/generation-result.utils";
|
|
16
16
|
import type { Creation } from "../../domain/entities/Creation";
|
|
17
17
|
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
18
|
+
import { isOlderThan, calculateAgeMs } from "../../../../shared/utils/calculations.util";
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
export interface UseProcessingJobsPollerConfig {
|
|
@@ -84,10 +85,11 @@ export function useProcessingJobsPoller(
|
|
|
84
85
|
pollingRef.current.add(creation.id);
|
|
85
86
|
|
|
86
87
|
// Stale detection: if creation is older than max poll time, mark as failed
|
|
87
|
-
|
|
88
|
-
if (ageMs > DEFAULT_MAX_POLL_TIME_MS) {
|
|
88
|
+
if (isOlderThan(creation.createdAt, DEFAULT_MAX_POLL_TIME_MS)) {
|
|
89
89
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
90
|
-
console.log("[ProcessingJobsPoller] Stale job detected, marking as failed:", creation.id, {
|
|
90
|
+
console.log("[ProcessingJobsPoller] Stale job detected, marking as failed:", creation.id, {
|
|
91
|
+
ageMs: calculateAgeMs(creation.createdAt),
|
|
92
|
+
});
|
|
91
93
|
}
|
|
92
94
|
try {
|
|
93
95
|
await repository.update(userId, creation.id, {
|
|
@@ -179,10 +181,9 @@ export function useProcessingJobsPoller(
|
|
|
179
181
|
if (!enabled || !userId || orphanJobs.length === 0) return;
|
|
180
182
|
|
|
181
183
|
const cleanupOrphans = async () => {
|
|
182
|
-
const staleOrphans = orphanJobs.filter((creation) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
});
|
|
184
|
+
const staleOrphans = orphanJobs.filter((creation) =>
|
|
185
|
+
isOlderThan(creation.createdAt, DEFAULT_MAX_POLL_TIME_MS)
|
|
186
|
+
);
|
|
186
187
|
|
|
187
188
|
if (staleOrphans.length === 0) return;
|
|
188
189
|
|
|
@@ -14,6 +14,7 @@ import { GalleryResultPreview } from "../components/GalleryResultPreview";
|
|
|
14
14
|
import { GalleryScreenHeader } from "../components/GalleryScreenHeader";
|
|
15
15
|
import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
|
|
16
16
|
import { createFilterButtons, createItemTitle } from "../utils/filter-buttons.util";
|
|
17
|
+
import { calculatePaginationSlice, calculateHasMore } from "../../../../shared/utils/calculations.util";
|
|
17
18
|
import type { Creation } from "../../domain/entities/Creation";
|
|
18
19
|
import type { CreationsGalleryScreenProps } from "./creations-gallery.types";
|
|
19
20
|
import { creationsGalleryStyles as styles } from "./creations-gallery.styles";
|
|
@@ -111,17 +112,20 @@ export function CreationsGalleryScreen({
|
|
|
111
112
|
}), [callbacks, onCreationPress, onShareToFeed, galleryState]);
|
|
112
113
|
|
|
113
114
|
const [pageLimit, setPageLimit] = useState(6);
|
|
115
|
+
const currentPage = Math.ceil(pageLimit / 6);
|
|
114
116
|
|
|
115
117
|
const paginatedCreations = useMemo(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
const { end } = calculatePaginationSlice(filters.filtered.length, currentPage, pageLimit);
|
|
119
|
+
return filters.filtered.slice(0, end);
|
|
120
|
+
}, [filters.filtered, currentPage, pageLimit]);
|
|
118
121
|
|
|
119
122
|
const handleLoadMore = useCallback(() => {
|
|
120
|
-
|
|
123
|
+
const hasMore = calculateHasMore(filters.filtered.length, currentPage, 6);
|
|
124
|
+
if (hasMore) {
|
|
121
125
|
if (__DEV__) console.log("[CreationsGallery] Loading more...", { current: pageLimit, total: filters.filtered.length });
|
|
122
126
|
setPageLimit(prev => prev + 3);
|
|
123
127
|
}
|
|
124
|
-
}, [pageLimit, filters.filtered.length]);
|
|
128
|
+
}, [pageLimit, filters.filtered.length, currentPage]);
|
|
125
129
|
|
|
126
130
|
const renderItem = useCallback(({ item }: { item: Creation }) => {
|
|
127
131
|
if (viewMode === "grid") {
|
|
@@ -233,12 +237,15 @@ export function CreationsGalleryScreen({
|
|
|
233
237
|
numColumns={viewMode === "grid" ? 2 : 1}
|
|
234
238
|
contentContainerStyle={viewMode === "grid" ? styles.gridContent : styles.listContent}
|
|
235
239
|
onEndReached={handleLoadMore}
|
|
236
|
-
onEndReachedThreshold={0.
|
|
237
|
-
initialNumToRender={3
|
|
238
|
-
maxToRenderPerBatch={3
|
|
239
|
-
windowSize={5
|
|
240
|
-
removeClippedSubviews={
|
|
240
|
+
onEndReachedThreshold={0.3}
|
|
241
|
+
initialNumToRender={6} // Increased from 3 - reduces blank space during scroll
|
|
242
|
+
maxToRenderPerBatch={8} // Increased from 3 - smoother scrolling
|
|
243
|
+
windowSize={7} // Increased from 5 - better off-screen rendering buffer
|
|
244
|
+
removeClippedSubviews={false} // Changed to false - prevents Android rendering issues
|
|
241
245
|
showsVerticalScrollIndicator={false}
|
|
246
|
+
scrollEventThrottle={32} // Throttle scroll events for better performance
|
|
247
|
+
updateCellsBatchingPeriod={50} // Batch updates more frequently
|
|
248
|
+
legacyImplementation={false} // Use new FlatList implementation
|
|
242
249
|
/>
|
|
243
250
|
)}
|
|
244
251
|
<FilterSheet visible={filters.statusFilterVisible} onClose={filters.closeStatusFilter} options={filters.statusFilter.filterOptions} selectedIds={[filters.statusFilter.selectedId]} onFilterPress={filters.statusFilter.selectFilter} onClearFilters={filters.statusFilter.clearFilter} title={t(config.translations.statusFilterTitle ?? "creations.filter.status")} clearLabel={t(config.translations.clearFilter ?? "common.clear")} />
|
|
@@ -33,14 +33,14 @@ export const env = {
|
|
|
33
33
|
generationMultiImageTimeoutMs: getEnvValue("GENERATION_MULTI_IMAGE_TIMEOUT_MS", 120000) as number,
|
|
34
34
|
|
|
35
35
|
// Polling Configuration
|
|
36
|
-
pollDefaultIntervalMs: getEnvValue("POLL_DEFAULT_INTERVAL_MS",
|
|
37
|
-
pollGalleryIntervalMs: getEnvValue("POLL_GALLERY_INTERVAL_MS",
|
|
36
|
+
pollDefaultIntervalMs: getEnvValue("POLL_DEFAULT_INTERVAL_MS", 4000) as number, // Increased from 3000ms - less aggressive polling
|
|
37
|
+
pollGalleryIntervalMs: getEnvValue("POLL_GALLERY_INTERVAL_MS", 6000) as number, // Increased from 5000ms - gallery doesn't need frequent updates
|
|
38
38
|
pollMaxTimeMs: getEnvValue("POLL_MAX_TIME_MS", 300000) as number,
|
|
39
|
-
pollMaxAttempts: getEnvValue("POLL_MAX_ATTEMPTS",
|
|
39
|
+
pollMaxAttempts: getEnvValue("POLL_MAX_ATTEMPTS", 75) as number, // Reduced from 100 - fewer attempts
|
|
40
40
|
pollMaxConsecutiveErrors: getEnvValue("POLL_MAX_CONSECUTIVE_ERRORS", 5) as number,
|
|
41
|
-
pollMinBackoffDelayMs: getEnvValue("POLL_MIN_BACKOFF_DELAY_MS",
|
|
41
|
+
pollMinBackoffDelayMs: getEnvValue("POLL_MIN_BACKOFF_DELAY_MS", 1500) as number, // Increased from 1000ms
|
|
42
42
|
pollMaxBackoffDelayMs: getEnvValue("POLL_MAX_BACKOFF_DELAY_MS", 30000) as number,
|
|
43
|
-
pollBackoffMultiplier: getEnvValue("POLL_BACKOFF_MULTIPLIER", 1.
|
|
43
|
+
pollBackoffMultiplier: getEnvValue("POLL_BACKOFF_MULTIPLIER", 1.6) as number, // Increased from 1.5 - faster backoff
|
|
44
44
|
|
|
45
45
|
// Validation Limits
|
|
46
46
|
validationMaxPromptLength: getEnvValue("VALIDATION_MAX_PROMPT_LENGTH", 10000) as number,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import { classifyError } from "../utils/error-classification";
|
|
13
13
|
import { pollJob } from "../../domains/background/infrastructure/services/job-poller.service";
|
|
14
14
|
import { ProviderValidator } from "./provider-validator";
|
|
15
|
+
import { calculateDurationMs } from "../../shared/utils/calculations.util";
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
export interface OrchestratorConfig {
|
|
@@ -91,7 +92,7 @@ class GenerationOrchestratorService {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
const result = pollResult.data as T;
|
|
94
|
-
const duration =
|
|
95
|
+
const duration = calculateDurationMs(startTime);
|
|
95
96
|
|
|
96
97
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
98
|
console.log("[Orchestrator] Generate completed:", {
|
|
@@ -111,7 +112,7 @@ class GenerationOrchestratorService {
|
|
|
111
112
|
capability: request.capability,
|
|
112
113
|
startTime,
|
|
113
114
|
endTime: Date.now(),
|
|
114
|
-
duration,
|
|
115
|
+
duration: calculateDurationMs(startTime),
|
|
115
116
|
},
|
|
116
117
|
};
|
|
117
118
|
} catch (error) {
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Utilities for preparing photos for AI generation.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
calculateBase64Size,
|
|
9
|
+
calculateBase64SizeMB,
|
|
10
|
+
} from "../../../../shared/utils/calculations.util";
|
|
11
|
+
|
|
7
12
|
export interface PhotoInput {
|
|
8
13
|
base64: string;
|
|
9
14
|
mimeType?: string;
|
|
@@ -70,15 +75,16 @@ export const isValidBase64 = (base64: string): boolean => {
|
|
|
70
75
|
|
|
71
76
|
/**
|
|
72
77
|
* Get image size estimate in bytes from base64
|
|
78
|
+
* Uses centralized calculation utility
|
|
73
79
|
*/
|
|
74
80
|
export const getBase64Size = (base64: string): number => {
|
|
75
|
-
|
|
76
|
-
return (cleaned.length * 3) / 4;
|
|
81
|
+
return calculateBase64Size(base64);
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
/**
|
|
80
85
|
* Get image size estimate in MB from base64
|
|
86
|
+
* Uses centralized calculation utility
|
|
81
87
|
*/
|
|
82
88
|
export const getBase64SizeMB = (base64: string): number => {
|
|
83
|
-
return
|
|
89
|
+
return calculateBase64SizeMB(base64);
|
|
84
90
|
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStableCallback Hook
|
|
3
|
+
* Returns a memoized callback that only changes when dependencies change
|
|
4
|
+
* Prevents unnecessary re-renders and child component updates
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
export function useStableCallback<T extends (...args: unknown[]) => unknown>(
|
|
10
|
+
callback: T,
|
|
11
|
+
deps: React.DependencyList
|
|
12
|
+
): T {
|
|
13
|
+
const callbackRef = useRef(callback);
|
|
14
|
+
const depsRef = useRef(deps);
|
|
15
|
+
|
|
16
|
+
// Check if dependencies actually changed
|
|
17
|
+
const depsChanged = deps.length !== depsRef.current.length ||
|
|
18
|
+
deps.some((dep, i) => dep !== depsRef.current[i]);
|
|
19
|
+
|
|
20
|
+
if (depsChanged) {
|
|
21
|
+
callbackRef.current = callback;
|
|
22
|
+
depsRef.current = [...deps];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Return stable reference
|
|
26
|
+
return useCallback((...args: Parameters<T>) => {
|
|
27
|
+
return callbackRef.current(...args);
|
|
28
|
+
}, []) as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* useThrottledCallback Hook
|
|
33
|
+
* Returns a throttled version of the callback
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { throttle } from "../utils/debounce.util";
|
|
37
|
+
|
|
38
|
+
export function useThrottledCallback<T extends (...args: unknown[]) => unknown>(
|
|
39
|
+
callback: T,
|
|
40
|
+
wait: number,
|
|
41
|
+
deps: React.DependencyList
|
|
42
|
+
): T {
|
|
43
|
+
const throttledRef = useRef<ReturnType<typeof throttle<T>> | undefined>(undefined);
|
|
44
|
+
|
|
45
|
+
// Recreate throttled function when dependencies change
|
|
46
|
+
if (!throttledRef.current || deps.some((dep, i) => dep !== (throttledRef.current as unknown as { deps: React.DependencyList }).deps?.[i])) {
|
|
47
|
+
throttledRef.current = throttle(callback, wait) as unknown as ReturnType<typeof throttle<T>>;
|
|
48
|
+
(throttledRef.current as unknown as { deps: React.DependencyList }).deps = [...deps];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (throttledRef.current as unknown as T);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* useDebouncedCallback Hook
|
|
56
|
+
* Returns a debounced version of the callback
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { debounce } from "../utils/debounce.util";
|
|
60
|
+
|
|
61
|
+
export function useDebouncedCallback<T extends (...args: unknown[]) => unknown>(
|
|
62
|
+
callback: T,
|
|
63
|
+
wait: number,
|
|
64
|
+
deps: React.DependencyList
|
|
65
|
+
): T {
|
|
66
|
+
const debouncedRef = useRef<ReturnType<typeof debounce<T>> | undefined>(undefined);
|
|
67
|
+
|
|
68
|
+
// Recreate debounced function when dependencies change
|
|
69
|
+
if (!debouncedRef.current || deps.some((dep, i) => dep !== (debouncedRef.current as unknown as { deps: React.DependencyList }).deps?.[i])) {
|
|
70
|
+
debouncedRef.current = debounce(callback, wait) as unknown as ReturnType<typeof debounce<T>>;
|
|
71
|
+
(debouncedRef.current as unknown as { deps: React.DependencyList }).deps = [...deps];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (debouncedRef.current as unknown as T);
|
|
75
|
+
}
|
package/src/shared/index.ts
CHANGED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculation Utilities
|
|
3
|
+
* Centralized calculation operations for better performance and maintainability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Time & Duration Calculations
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Calculate age in milliseconds from a timestamp
|
|
12
|
+
*/
|
|
13
|
+
export function calculateAgeMs(timestamp: Date | number): number {
|
|
14
|
+
const time = typeof timestamp === "number" ? timestamp : timestamp.getTime();
|
|
15
|
+
return Date.now() - time;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Calculate age in seconds from a timestamp
|
|
20
|
+
*/
|
|
21
|
+
export function calculateAgeSeconds(timestamp: Date | number): number {
|
|
22
|
+
return Math.floor(calculateAgeMs(timestamp) / 1000);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate age in minutes from a timestamp
|
|
27
|
+
*/
|
|
28
|
+
export function calculateAgeMinutes(timestamp: Date | number): number {
|
|
29
|
+
return Math.floor(calculateAgeSeconds(timestamp) / 60);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if timestamp is older than specified milliseconds
|
|
34
|
+
*/
|
|
35
|
+
export function isOlderThan(timestamp: Date | number, maxAgeMs: number): boolean {
|
|
36
|
+
return calculateAgeMs(timestamp) > maxAgeMs;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculate duration between two timestamps in milliseconds
|
|
41
|
+
*/
|
|
42
|
+
export function calculateDurationMs(
|
|
43
|
+
startTime: Date | number,
|
|
44
|
+
endTime: Date | number = Date.now()
|
|
45
|
+
): number {
|
|
46
|
+
const start = typeof startTime === "number" ? startTime : startTime.getTime();
|
|
47
|
+
const end = typeof endTime === "number" ? endTime : endTime.getTime();
|
|
48
|
+
return end - start;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format duration in milliseconds to human-readable string
|
|
53
|
+
*/
|
|
54
|
+
export function formatDuration(ms: number): string {
|
|
55
|
+
const seconds = Math.floor(ms / 1000);
|
|
56
|
+
const minutes = Math.floor(seconds / 60);
|
|
57
|
+
const hours = Math.floor(minutes / 60);
|
|
58
|
+
|
|
59
|
+
if (hours > 0) {
|
|
60
|
+
return `${hours}h ${minutes % 60}m`;
|
|
61
|
+
}
|
|
62
|
+
if (minutes > 0) {
|
|
63
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
64
|
+
}
|
|
65
|
+
return `${seconds}s`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Polling Calculations
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Calculate polling interval with exponential backoff
|
|
74
|
+
*/
|
|
75
|
+
export function calculatePollingInterval(options: {
|
|
76
|
+
attempt: number;
|
|
77
|
+
initialIntervalMs: number;
|
|
78
|
+
maxIntervalMs: number;
|
|
79
|
+
backoffMultiplier: number;
|
|
80
|
+
}): number {
|
|
81
|
+
const { attempt, initialIntervalMs, maxIntervalMs, backoffMultiplier } = options;
|
|
82
|
+
|
|
83
|
+
if (attempt === 0) {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const interval = initialIntervalMs * Math.pow(backoffMultiplier, attempt - 1);
|
|
88
|
+
return Math.min(interval, maxIntervalMs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Calculate estimated total polling time
|
|
93
|
+
*/
|
|
94
|
+
export function calculateEstimatedPollingTime(options: {
|
|
95
|
+
maxAttempts: number;
|
|
96
|
+
initialIntervalMs: number;
|
|
97
|
+
maxIntervalMs: number;
|
|
98
|
+
backoffMultiplier: number;
|
|
99
|
+
}): number {
|
|
100
|
+
let total = 0;
|
|
101
|
+
for (let attempt = 1; attempt < options.maxAttempts; attempt++) {
|
|
102
|
+
total += calculatePollingInterval({ ...options, attempt });
|
|
103
|
+
if (total >= options.maxIntervalMs * options.maxAttempts) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return total;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Progress & Percentage Calculations
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Calculate percentage with bounds checking (0-100)
|
|
116
|
+
*/
|
|
117
|
+
export function calculatePercentage(value: number, total: number): number {
|
|
118
|
+
if (total === 0) return 0;
|
|
119
|
+
const percentage = (value / total) * 100;
|
|
120
|
+
return Math.max(0, Math.min(100, percentage));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Calculate progress with min/max bounds
|
|
125
|
+
*/
|
|
126
|
+
export function calculateProgress(
|
|
127
|
+
current: number,
|
|
128
|
+
total: number,
|
|
129
|
+
min: number = 0,
|
|
130
|
+
max: number = 100
|
|
131
|
+
): number {
|
|
132
|
+
if (total === 0) return min;
|
|
133
|
+
const percentage = (current / total) * (max - min) + min;
|
|
134
|
+
return Math.max(min, Math.min(max, percentage));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Calculate remaining percentage
|
|
139
|
+
*/
|
|
140
|
+
export function calculateRemaining(current: number, total: number): number {
|
|
141
|
+
return Math.max(0, total - current);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Array & Collection Calculations
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Calculate filtered count with predicate
|
|
150
|
+
*/
|
|
151
|
+
export function calculateFilteredCount<T>(
|
|
152
|
+
items: readonly T[],
|
|
153
|
+
predicate: (item: T) => boolean
|
|
154
|
+
): number {
|
|
155
|
+
return items.reduce((count, item) => (predicate(item) ? count + 1 : count), 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Calculate pagination slice
|
|
160
|
+
*/
|
|
161
|
+
export function calculatePaginationSlice(
|
|
162
|
+
totalItems: number,
|
|
163
|
+
page: number,
|
|
164
|
+
pageSize: number
|
|
165
|
+
): { start: number; end: number; count: number } {
|
|
166
|
+
const start = page * pageSize;
|
|
167
|
+
const end = Math.min(start + pageSize, totalItems);
|
|
168
|
+
const count = end - start;
|
|
169
|
+
return { start, end, count };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calculate if more items exist for pagination
|
|
174
|
+
*/
|
|
175
|
+
export function calculateHasMore(
|
|
176
|
+
currentCount: number,
|
|
177
|
+
currentPage: number,
|
|
178
|
+
pageSize: number
|
|
179
|
+
): boolean {
|
|
180
|
+
return currentCount >= currentPage * pageSize;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Base64 & Size Calculations
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Calculate base64 size in bytes
|
|
189
|
+
*/
|
|
190
|
+
export function calculateBase64Size(base64: string): number {
|
|
191
|
+
// Remove data URI prefix if present
|
|
192
|
+
const cleanBase64 = base64.replace(/^data:[^;]+;base64,/, "");
|
|
193
|
+
return (cleanBase64.length * 3) / 4;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Calculate base64 size in megabytes
|
|
198
|
+
*/
|
|
199
|
+
export function calculateBase64SizeMB(base64: string): number {
|
|
200
|
+
return calculateBase64Size(base64) / (1024 * 1024);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Calculate if base64 size is within limit
|
|
205
|
+
*/
|
|
206
|
+
export function isBase64SizeWithinLimit(base64: string, maxSizeMB: number): boolean {
|
|
207
|
+
return calculateBase64SizeMB(base64) <= maxSizeMB;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Confidence Score Calculations
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Calculate confidence score from violations with weights
|
|
216
|
+
*/
|
|
217
|
+
export function calculateConfidenceScore(
|
|
218
|
+
violations: readonly { severity: "critical" | "high" | "medium" | "low" }[]
|
|
219
|
+
): number {
|
|
220
|
+
if (violations.length === 0) return 1.0;
|
|
221
|
+
|
|
222
|
+
const weights = { critical: 1.0, high: 0.75, medium: 0.5, low: 0.25 };
|
|
223
|
+
const score = violations.reduce(
|
|
224
|
+
(sum, v) => sum + (weights[v.severity] || 0.25),
|
|
225
|
+
0
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Normalize by number of violations, capped at 1.0
|
|
229
|
+
return Math.min(1.0, score / Math.max(1, violations.length));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Credit & Cost Calculations
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Calculate cost in credits based on duration and resolution
|
|
238
|
+
*/
|
|
239
|
+
export function calculateCredits(
|
|
240
|
+
durationSeconds: number,
|
|
241
|
+
resolutionMultiplier: number = 1,
|
|
242
|
+
baseCost: number = 1
|
|
243
|
+
): number {
|
|
244
|
+
return Math.ceil((durationSeconds / 60) * resolutionMultiplier * baseCost);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Calculate resolution multiplier for credits
|
|
249
|
+
*/
|
|
250
|
+
export function calculateResolutionMultiplier(width: number, height: number): number {
|
|
251
|
+
const totalPixels = width * height;
|
|
252
|
+
const basePixels = 720 * 1280; // HD baseline
|
|
253
|
+
|
|
254
|
+
if (totalPixels <= basePixels) return 1;
|
|
255
|
+
if (totalPixels <= basePixels * 2) return 1.5;
|
|
256
|
+
if (totalPixels <= basePixels * 4) return 2;
|
|
257
|
+
return 3;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Calculate cost to credits conversion
|
|
262
|
+
*/
|
|
263
|
+
export function convertCostToCredits(
|
|
264
|
+
cost: number,
|
|
265
|
+
creditsPerDollar: number = 100
|
|
266
|
+
): number {
|
|
267
|
+
return Math.ceil(cost * creditsPerDollar);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Aspect Ratio Calculations
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Calculate aspect ratio from dimensions
|
|
276
|
+
*/
|
|
277
|
+
export function calculateAspectRatio(width: number, height: number): number {
|
|
278
|
+
return width / height;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Calculate height from width and aspect ratio
|
|
283
|
+
*/
|
|
284
|
+
export function calculateHeightFromAspectRatio(
|
|
285
|
+
width: number,
|
|
286
|
+
aspectRatio: number
|
|
287
|
+
): number {
|
|
288
|
+
return Math.round(width / aspectRatio);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Calculate width from height and aspect ratio
|
|
293
|
+
*/
|
|
294
|
+
export function calculateWidthFromAspectRatio(
|
|
295
|
+
height: number,
|
|
296
|
+
aspectRatio: number
|
|
297
|
+
): number {
|
|
298
|
+
return Math.round(height * aspectRatio);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Memory & Performance Calculations
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Calculate estimated memory usage for image
|
|
307
|
+
*/
|
|
308
|
+
export function calculateImageMemoryUsage(
|
|
309
|
+
width: number,
|
|
310
|
+
height: number,
|
|
311
|
+
bytesPerPixel: number = 4 // RGBA
|
|
312
|
+
): number {
|
|
313
|
+
return width * height * bytesPerPixel;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Calculate estimated memory usage in MB
|
|
318
|
+
*/
|
|
319
|
+
export function calculateMemoryMB(bytes: number): number {
|
|
320
|
+
return bytes / (1024 * 1024);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Calculate safe batch size for processing
|
|
325
|
+
*/
|
|
326
|
+
export function calculateSafeBatchSize(
|
|
327
|
+
availableMemoryMB: number,
|
|
328
|
+
itemSizeMB: number,
|
|
329
|
+
safetyFactor: number = 0.7
|
|
330
|
+
): number {
|
|
331
|
+
const safeMemory = availableMemoryMB * safetyFactor;
|
|
332
|
+
return Math.max(1, Math.floor(safeMemory / itemSizeMB));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Utility Functions
|
|
337
|
+
*/
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Clamp value between min and max
|
|
341
|
+
*/
|
|
342
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
343
|
+
return Math.max(min, Math.min(max, value));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Linear interpolation between two values
|
|
348
|
+
*/
|
|
349
|
+
export function lerp(start: number, end: number, progress: number): number {
|
|
350
|
+
return start + (end - start) * clamp(progress, 0, 1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Map value from one range to another
|
|
355
|
+
*/
|
|
356
|
+
export function mapRange(
|
|
357
|
+
value: number,
|
|
358
|
+
inMin: number,
|
|
359
|
+
inMax: number,
|
|
360
|
+
outMin: number,
|
|
361
|
+
outMax: number
|
|
362
|
+
): number {
|
|
363
|
+
const inRange = inMax - inMin;
|
|
364
|
+
const outRange = outMax - outMin;
|
|
365
|
+
return outMin + ((value - inMin) / inRange) * outRange;
|
|
366
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Provides consistent credit calculation operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { calculateCredits as calculateCreditsFromDuration } from "./calculations.util";
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Validates if a value is a valid credit amount
|
|
8
10
|
* @param credits - Credit value to validate
|
|
@@ -46,6 +48,21 @@ export function calculateTotalCost(costPerUnit: number, units: number): number {
|
|
|
46
48
|
return costPerUnit * units;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Calculates credits based on duration and resolution
|
|
53
|
+
* @param durationSeconds - Duration in seconds
|
|
54
|
+
* @param resolutionMultiplier - Resolution multiplier (default: 1)
|
|
55
|
+
* @param baseCost - Base cost per minute (default: 1)
|
|
56
|
+
* @returns Required credits
|
|
57
|
+
*/
|
|
58
|
+
export function calculateCredits(
|
|
59
|
+
durationSeconds: number,
|
|
60
|
+
resolutionMultiplier: number = 1,
|
|
61
|
+
baseCost: number = 1
|
|
62
|
+
): number {
|
|
63
|
+
return calculateCreditsFromDuration(durationSeconds, resolutionMultiplier, baseCost);
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
/**
|
|
50
67
|
* Formats credits for display
|
|
51
68
|
* @param credits - Credit amount
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce Utility
|
|
3
|
+
* Delays function execution until after wait milliseconds have elapsed
|
|
4
|
+
* since the last time the debounced function was invoked
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
8
|
+
func: T,
|
|
9
|
+
wait: number
|
|
10
|
+
): (...args: Parameters<T>) => void {
|
|
11
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
12
|
+
|
|
13
|
+
return function debounced(...args: Parameters<T>) {
|
|
14
|
+
if (timeoutId) {
|
|
15
|
+
clearTimeout(timeoutId);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
timeoutId = setTimeout(() => {
|
|
19
|
+
func(...args);
|
|
20
|
+
timeoutId = undefined;
|
|
21
|
+
}, wait);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Throttle Utility
|
|
27
|
+
* Ensures function execution at most once every wait milliseconds
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export function throttle<T extends (...args: unknown[]) => unknown>(
|
|
31
|
+
func: T,
|
|
32
|
+
wait: number
|
|
33
|
+
): (...args: Parameters<T>) => void {
|
|
34
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
let lastCallTime = 0;
|
|
36
|
+
|
|
37
|
+
return function throttled(...args: Parameters<T>) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const timeSinceLastCall = now - lastCallTime;
|
|
40
|
+
|
|
41
|
+
if (timeSinceLastCall >= wait) {
|
|
42
|
+
lastCallTime = now;
|
|
43
|
+
func(...args);
|
|
44
|
+
} else {
|
|
45
|
+
if (timeoutId) {
|
|
46
|
+
clearTimeout(timeoutId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
timeoutId = setTimeout(() => {
|
|
50
|
+
lastCallTime = Date.now();
|
|
51
|
+
func(...args);
|
|
52
|
+
timeoutId = undefined;
|
|
53
|
+
}, wait - timeSinceLastCall);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|