@umituz/react-native-ai-generation-content 1.89.60 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.89.60",
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: 1000,
18
- maxIntervalMs: 3000,
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
- const { initialIntervalMs, maxIntervalMs, backoffMultiplier } = config;
15
-
16
- if (attempt === 0) {
17
- return 0;
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 from TanStack Query state (not ref) for reactivity
94
- // Active jobs are those currently processing or queued
95
- const activeJobs = useMemo(
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: 30 * 60 * 1000,
43
+ gcTime: 5 * 60 * 1000, // 5 minutes - reduced from 30min to prevent memory bloat
44
44
  enabled: options.enabled !== false,
45
45
  });
46
46
 
@@ -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
- if (violations.length === 0) return 1.0;
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
- const onDataCallback = useCallback((creations: Creation[]) => {
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
- const onErrorCallback = useCallback((err: Error) => {
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
- onDataCallback(creations);
79
+ if (timeoutId) clearTimeout(timeoutId);
80
+ onDataCallbackRef.current?.(creations);
77
81
  };
78
82
 
79
83
  const handleError = (err: Error) => {
80
- clearTimeout(timeoutId);
81
- onErrorCallback(err);
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 10s, stop loading
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
- }, 10000);
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]); // onDataCallback/onErrorCallback intentionally omitted - stable memoized refs
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
- const ageMs = Date.now() - creation.createdAt.getTime();
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, { ageMs });
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
- const ageMs = Date.now() - creation.createdAt.getTime();
184
- return ageMs >= DEFAULT_MAX_POLL_TIME_MS;
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
- return filters.filtered.slice(0, pageLimit);
117
- }, [filters.filtered, pageLimit]);
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
- if (pageLimit < filters.filtered.length) {
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.5}
237
- initialNumToRender={3}
238
- maxToRenderPerBatch={3}
239
- windowSize={5}
240
- removeClippedSubviews={true}
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", 3000) as number,
37
- pollGalleryIntervalMs: getEnvValue("POLL_GALLERY_INTERVAL_MS", 5000) as number,
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", 100) as number,
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", 1000) as number,
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.5) as number,
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 = Date.now() - startTime;
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
- const cleaned = cleanBase64(base64);
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 getBase64Size(base64) / (1024 * 1024);
89
+ return calculateBase64SizeMB(base64);
84
90
  };
@@ -3,3 +3,4 @@
3
3
  */
4
4
 
5
5
  export * from "./factories";
6
+ export * from "./useStableCallback";
@@ -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
+ }
@@ -12,3 +12,5 @@ export * from "./hooks";
12
12
  // Utils
13
13
  export * from "./utils/date";
14
14
  export * from "./utils/filters";
15
+ export * from "./utils/debounce.util";
16
+ export * from "./utils/calculations.util";
@@ -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
+ }