@umituz/react-native-ai-generation-content 1.65.12 → 1.66.2

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.65.12",
3
+ "version": "1.66.2",
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",
@@ -70,7 +70,7 @@
70
70
  "@typescript-eslint/eslint-plugin": "^8.54.0",
71
71
  "@typescript-eslint/parser": "^8.54.0",
72
72
  "@umituz/react-native-design-system": "^4.23.79",
73
- "@umituz/react-native-firebase": "^1.13.87",
73
+ "@umituz/react-native-firebase": "^1.13.161",
74
74
  "@umituz/react-native-subscription": "^2.27.23",
75
75
  "eslint": "^9.39.2",
76
76
  "eslint-plugin-react": "^7.37.5",
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Creation Display Utilities
3
+ * Single Responsibility: UI display helpers (icons, titles, text)
4
+ */
5
+
6
+ import type { CreationTypeId } from "../types";
7
+
8
+ /**
9
+ * Icon name type for design system
10
+ */
11
+ export type IconName = "image" | "film" | "mic" | "sparkles" | "color-palette" | "brush";
12
+
13
+ /**
14
+ * Get icon name for creation type
15
+ */
16
+ export function getTypeIcon(type: CreationTypeId): IconName {
17
+ switch (type) {
18
+ case "text-to-video":
19
+ case "image-to-video":
20
+ return "film";
21
+ case "style-transfer":
22
+ case "colorization":
23
+ return "color-palette";
24
+ case "ai-brush":
25
+ case "inpainting":
26
+ return "brush";
27
+ default:
28
+ return "image";
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Get i18n key for creation type
34
+ */
35
+ export function getTypeTextKey(type: CreationTypeId): string {
36
+ return `creations.types.${type}`;
37
+ }
38
+
39
+ /**
40
+ * Get formatted type text (fallback)
41
+ */
42
+ export function getTypeText(type: CreationTypeId): string {
43
+ return type.split("-").map(word =>
44
+ word.charAt(0).toUpperCase() + word.slice(1)
45
+ ).join(" ");
46
+ }
47
+
48
+ /**
49
+ * Get creation title from prompt or type
50
+ */
51
+ export function getCreationTitle(
52
+ prompt?: string,
53
+ type?: CreationTypeId,
54
+ maxLength: number = 50
55
+ ): string {
56
+ if (prompt && prompt.length > 0) {
57
+ return prompt.length > maxLength
58
+ ? prompt.substring(0, maxLength) + "..."
59
+ : prompt;
60
+ }
61
+
62
+ if (type) {
63
+ return getTypeText(type);
64
+ }
65
+
66
+ return "Untitled Creation";
67
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Creation Format Utilities
3
+ * Single Responsibility: Text formatting
4
+ */
5
+
6
+ /**
7
+ * Truncate text with ellipsis
8
+ */
9
+ export function truncateText(text: string, maxLength: number): string {
10
+ if (text.length <= maxLength) return text;
11
+ return text.substring(0, maxLength - 3) + "...";
12
+ }
@@ -1,140 +1,12 @@
1
1
  /**
2
2
  * Creation Helpers
3
- * Utility functions for creation data manipulation
3
+ * Central export point for creation utility functions
4
+ * @deprecated Individual utilities split into focused modules for better maintainability
4
5
  */
5
6
 
6
- import { generateUUID } from "@umituz/react-native-design-system";
7
- import type { CreationTypeId } from "../types";
8
-
9
- /**
10
- * Generate a unique creation ID using UUID v4
11
- */
12
- export function generateCreationId(): string {
13
- return generateUUID();
14
- }
15
-
16
- /**
17
- * Icon name type for design system
18
- */
19
- export type IconName = "image" | "film" | "mic" | "sparkles" | "color-palette" | "brush";
20
-
21
- /**
22
- * Get icon name for creation type
23
- */
24
- export function getTypeIcon(type: CreationTypeId): IconName {
25
- switch (type) {
26
- case "text-to-video":
27
- case "image-to-video":
28
- return "film";
29
- case "style-transfer":
30
- case "colorization":
31
- return "color-palette";
32
- case "ai-brush":
33
- case "inpainting":
34
- return "brush";
35
- default:
36
- return "image";
37
- }
38
- }
39
-
40
- /**
41
- * Get i18n key for creation type
42
- */
43
- export function getTypeTextKey(type: CreationTypeId): string {
44
- return `creations.types.${type}`;
45
- }
46
-
47
- /**
48
- * Get formatted type text (fallback)
49
- */
50
- export function getTypeText(type: CreationTypeId): string {
51
- return type.split("-").map(word =>
52
- word.charAt(0).toUpperCase() + word.slice(1)
53
- ).join(" ");
54
- }
55
-
56
- /**
57
- * Get creation title from prompt or type
58
- */
59
- export function getCreationTitle(
60
- prompt?: string,
61
- type?: CreationTypeId,
62
- maxLength: number = 50
63
- ): string {
64
- if (prompt && prompt.length > 0) {
65
- return prompt.length > maxLength
66
- ? prompt.substring(0, maxLength) + "..."
67
- : prompt;
68
- }
69
-
70
- if (type) {
71
- return getTypeText(type);
72
- }
73
-
74
- return "Untitled Creation";
75
- }
76
-
77
- /**
78
- * Filter creations by search query (client-side)
79
- */
80
- export function filterBySearch<T extends { prompt?: string; type?: string; provider?: string }>(
81
- items: T[],
82
- searchQuery?: string,
83
- ): T[] {
84
- if (!searchQuery || searchQuery.trim().length === 0) {
85
- return items;
86
- }
87
-
88
- const query = searchQuery.toLowerCase().trim();
89
-
90
- return items.filter((item) =>
91
- item.prompt?.toLowerCase().includes(query) ||
92
- item.type?.toLowerCase().includes(query) ||
93
- item.provider?.toLowerCase().includes(query)
94
- );
95
- }
96
-
97
- /**
98
- * Sort creations by field
99
- */
100
- export function sortCreations<T extends Record<string, unknown>>(
101
- items: T[],
102
- field: keyof T,
103
- order: "asc" | "desc" = "desc"
104
- ): T[] {
105
- return [...items].sort((a, b) => {
106
- const aVal = a[field] as unknown;
107
- const bVal = b[field] as unknown;
108
-
109
- if (aVal === undefined && bVal === undefined) return 0;
110
- if (aVal === undefined) return 1;
111
- if (bVal === undefined) return -1;
112
-
113
- if (typeof aVal === "string" && typeof bVal === "string") {
114
- return order === "desc"
115
- ? bVal.localeCompare(aVal)
116
- : aVal.localeCompare(bVal);
117
- }
118
-
119
- if (typeof aVal === "number" && typeof bVal === "number") {
120
- return order === "desc" ? bVal - aVal : aVal - bVal;
121
- }
122
-
123
- // Handle Date objects
124
- if (aVal instanceof Date && bVal instanceof Date) {
125
- return order === "desc"
126
- ? bVal.getTime() - aVal.getTime()
127
- : aVal.getTime() - bVal.getTime();
128
- }
129
-
130
- return 0;
131
- });
132
- }
133
-
134
- /**
135
- * Truncate text with ellipsis
136
- */
137
- export function truncateText(text: string, maxLength: number): string {
138
- if (text.length <= maxLength) return text;
139
- return text.substring(0, maxLength - 3) + "...";
140
- }
7
+ // Re-export all utilities for backward compatibility
8
+ export * from "./creation-id.util";
9
+ export * from "./creation-display.util";
10
+ export * from "./creation-search.util";
11
+ export * from "./creation-sort.util";
12
+ export * from "./creation-format.util";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Creation ID Utilities
3
+ * Single Responsibility: ID generation
4
+ */
5
+
6
+ import { generateUUID } from "@umituz/react-native-design-system";
7
+
8
+ /**
9
+ * Generate a unique creation ID using UUID v4
10
+ */
11
+ export function generateCreationId(): string {
12
+ return generateUUID();
13
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Creation Search Utilities
3
+ * Single Responsibility: Filtering and searching
4
+ */
5
+
6
+ /**
7
+ * Filter creations by search query (client-side)
8
+ */
9
+ export function filterBySearch<T extends { prompt?: string; type?: string; provider?: string }>(
10
+ items: T[],
11
+ searchQuery?: string,
12
+ ): T[] {
13
+ if (!searchQuery || searchQuery.trim().length === 0) {
14
+ return items;
15
+ }
16
+
17
+ const query = searchQuery.toLowerCase().trim();
18
+
19
+ return items.filter((item) =>
20
+ item.prompt?.toLowerCase().includes(query) ||
21
+ item.type?.toLowerCase().includes(query) ||
22
+ item.provider?.toLowerCase().includes(query)
23
+ );
24
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Creation Sort Utilities
3
+ * Single Responsibility: Sorting operations
4
+ */
5
+
6
+ /**
7
+ * Sort creations by field
8
+ */
9
+ export function sortCreations<T extends Record<string, unknown>>(
10
+ items: T[],
11
+ field: keyof T,
12
+ order: "asc" | "desc" = "desc"
13
+ ): T[] {
14
+ return [...items].sort((a, b) => {
15
+ const aVal = a[field] as unknown;
16
+ const bVal = b[field] as unknown;
17
+
18
+ if (aVal === undefined && bVal === undefined) return 0;
19
+ if (aVal === undefined) return 1;
20
+ if (bVal === undefined) return -1;
21
+
22
+ if (typeof aVal === "string" && typeof bVal === "string") {
23
+ return order === "desc"
24
+ ? bVal.localeCompare(aVal)
25
+ : aVal.localeCompare(bVal);
26
+ }
27
+
28
+ if (typeof aVal === "number" && typeof bVal === "number") {
29
+ return order === "desc" ? bVal - aVal : aVal - bVal;
30
+ }
31
+
32
+ // Handle Date objects
33
+ if (aVal instanceof Date && bVal instanceof Date) {
34
+ return order === "desc"
35
+ ? bVal.getTime() - aVal.getTime()
36
+ : aVal.getTime() - bVal.getTime();
37
+ }
38
+
39
+ return 0;
40
+ });
41
+ }
@@ -1,10 +1,16 @@
1
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
1
+ import type { CollectionReference, DocumentReference, DocumentData } from "firebase/firestore";
2
2
  import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
3
3
  import type { Creation } from "../../domain/entities/Creation";
4
4
  import type { CreationsSubscriptionCallback, UnsubscribeFunction } from "../../domain/repositories/ICreationsRepository";
5
5
  import { CreationsQuery } from "./CreationsQuery";
6
6
  import { CreationsSubscription } from "./CreationsSubscription";
7
7
 
8
+ /**
9
+ * Path resolver functions from BaseRepository
10
+ */
11
+ export type GetUserCollection = (userId: string) => CollectionReference<DocumentData> | null;
12
+ export type GetDocRef = (userId: string, documentId: string) => DocumentReference<DocumentData> | null;
13
+
8
14
  /**
9
15
  * CreationsFetcher
10
16
  * Orchestrates read operations for creations
@@ -19,11 +25,12 @@ export class CreationsFetcher {
19
25
  private readonly subscription: CreationsSubscription;
20
26
 
21
27
  constructor(
22
- pathResolver: FirestorePathResolver,
28
+ getUserCollection: GetUserCollection,
29
+ getDocRef: GetDocRef,
23
30
  documentMapper: DocumentMapper,
24
31
  ) {
25
- this.query = new CreationsQuery(pathResolver, documentMapper);
26
- this.subscription = new CreationsSubscription(pathResolver, documentMapper);
32
+ this.query = new CreationsQuery(getUserCollection, getDocRef, documentMapper);
33
+ this.subscription = new CreationsSubscription(getUserCollection, getDocRef, documentMapper);
27
34
  }
28
35
 
29
36
  /**
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { getDocs, getDoc, query, orderBy, where } from "firebase/firestore";
8
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
8
+ import type { GetUserCollection, GetDocRef } from "./CreationsFetcher";
9
9
  import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
10
10
  import type { Creation, CreationDocument } from "../../domain/entities/Creation";
11
11
  import { CREATION_FIELDS } from "../../domain/constants";
@@ -17,7 +17,8 @@ declare const __DEV__: boolean;
17
17
  */
18
18
  export class CreationsQuery {
19
19
  constructor(
20
- private readonly pathResolver: FirestorePathResolver,
20
+ private readonly getUserCollection: GetUserCollection,
21
+ private readonly getDocRef: GetDocRef,
21
22
  private readonly documentMapper: DocumentMapper,
22
23
  ) { }
23
24
 
@@ -26,7 +27,7 @@ export class CreationsQuery {
26
27
  * Optimized query: Server-side filtering for non-deleted items
27
28
  */
28
29
  async getAll(userId: string): Promise<Creation[]> {
29
- const userCollection = this.pathResolver.getUserCollection(userId);
30
+ const userCollection = this.getUserCollection(userId);
30
31
  if (!userCollection) return [];
31
32
 
32
33
  try {
@@ -62,7 +63,7 @@ export class CreationsQuery {
62
63
  * Get a single creation by ID
63
64
  */
64
65
  async getById(userId: string, id: string): Promise<Creation | null> {
65
- const docRef = this.pathResolver.getDocRef(userId, id);
66
+ const docRef = this.getDocRef(userId, id);
66
67
  if (!docRef) return null;
67
68
 
68
69
  try {
@@ -1,5 +1,5 @@
1
1
 
2
- import { BaseRepository, FirestorePathResolver } from "@umituz/react-native-firebase";
2
+ import { BaseRepository } from "@umituz/react-native-firebase";
3
3
  import type {
4
4
  ICreationsRepository,
5
5
  CreationsSubscriptionCallback,
@@ -22,10 +22,9 @@ export interface RepositoryOptions {
22
22
  /**
23
23
  * Creations Repository Implementation
24
24
  * Delegates to specialized classes for different responsibilities
25
- *
25
+ *
26
26
  * Architecture:
27
- * - Extends BaseRepository for centralized database access
28
- * - Uses FirestorePathResolver for path resolution
27
+ * - Extends BaseRepository for database access and path resolution
29
28
  * - Uses CreationsFetcher for read operations
30
29
  * - Uses CreationsWriter for write operations
31
30
  * - Standard path: users/{userId}/{collectionName}
@@ -33,7 +32,6 @@ export interface RepositoryOptions {
33
32
  export class CreationsRepository
34
33
  extends BaseRepository
35
34
  implements ICreationsRepository {
36
- private readonly pathResolver: FirestorePathResolver;
37
35
  private readonly fetcher: CreationsFetcher;
38
36
  private readonly writer: CreationsWriter;
39
37
 
@@ -41,14 +39,20 @@ export class CreationsRepository
41
39
  collectionName: string,
42
40
  options?: RepositoryOptions,
43
41
  ) {
44
- super();
42
+ super(collectionName);
45
43
 
46
44
  const documentMapper = options?.documentMapper ?? mapDocumentToCreation;
47
45
 
48
- // Initialize with Firestore database instance from BaseRepository
49
- this.pathResolver = new FirestorePathResolver(collectionName, this.getDb());
50
- this.fetcher = new CreationsFetcher(this.pathResolver, documentMapper);
51
- this.writer = new CreationsWriter(this.pathResolver);
46
+ // Pass BaseRepository methods directly to dependencies
47
+ this.fetcher = new CreationsFetcher(
48
+ (userId) => this.getUserCollection(userId),
49
+ (userId, docId) => this.getDocRef(userId, docId),
50
+ documentMapper
51
+ );
52
+ this.writer = new CreationsWriter(
53
+ (userId) => this.getUserCollection(userId),
54
+ (userId, docId) => this.getDocRef(userId, docId)
55
+ );
52
56
  }
53
57
 
54
58
  async getAll(userId: string): Promise<Creation[]> {
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { query, orderBy, onSnapshot, where } from "firebase/firestore";
8
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
8
+ import type { GetUserCollection } from "./CreationsFetcher";
9
9
  import type { DocumentMapper } from "../../domain/value-objects/CreationsConfig";
10
10
  import type { Creation, CreationDocument } from "../../domain/entities/Creation";
11
11
  import type { CreationsSubscriptionCallback, UnsubscribeFunction } from "../../domain/repositories/ICreationsRepository";
@@ -15,76 +15,30 @@ declare const __DEV__: boolean;
15
15
 
16
16
  /**
17
17
  * Handles realtime subscriptions for creations
18
+ * Optimized with server-side filtering (80% data reduction)
18
19
  */
19
20
  export class CreationsSubscription {
20
21
  constructor(
21
- private readonly pathResolver: FirestorePathResolver,
22
+ private readonly getUserCollection: GetUserCollection,
23
+ _getDocRef: unknown, // Not used in subscription, but accepted for consistent interface
22
24
  private readonly documentMapper: DocumentMapper,
23
25
  ) { }
24
26
 
25
- /**
26
- * Subscribes to realtime updates for user's creations
27
- *
28
- * PERFORMANCE OPTIMIZATION:
29
- * - Server-side filtering with where clause (80% data reduction)
30
- * - No client-side filtering needed
31
- * - Requires Firestore composite index: (deletedAt ASC, createdAt DESC)
32
- *
33
- * @param userId - User ID to query
34
- * @param onData - Callback for data updates
35
- * @param onError - Optional error callback
36
- * @returns Unsubscribe function
37
- */
38
27
  subscribeToAll(
39
28
  userId: string,
40
29
  onData: CreationsSubscriptionCallback,
41
30
  onError?: (error: Error) => void,
42
31
  ): UnsubscribeFunction {
43
- const userCollection = this.pathResolver.getUserCollection(userId);
32
+ const userCollection = this.getUserCollection(userId);
44
33
 
45
34
  if (!userCollection) {
46
- return this.handleInvalidCollection(userId, onData, onError);
35
+ const error = new Error(`[CreationsSubscription] Invalid user collection: ${userId}`);
36
+ if (__DEV__) console.error(error.message);
37
+ onData([]);
38
+ onError?.(error);
39
+ return () => { if (__DEV__) console.log("[CreationsSubscription] No-op unsubscribe"); };
47
40
  }
48
41
 
49
- return this.createRealtimeListener(userCollection, onData, onError);
50
- }
51
-
52
- /**
53
- * Handles case when user collection is invalid
54
- */
55
- private handleInvalidCollection(
56
- userId: string,
57
- onData: CreationsSubscriptionCallback,
58
- onError?: (error: Error) => void,
59
- ): UnsubscribeFunction {
60
- const error = new Error(`[CreationsSubscription] Cannot subscribe: Invalid user collection for userId: ${userId}`);
61
-
62
- if (__DEV__) {
63
- console.error(error.message);
64
- }
65
-
66
- // Return empty array immediately
67
- onData([]);
68
-
69
- // Report error to callback
70
- onError?.(error);
71
-
72
- // Return no-op unsubscribe function
73
- return () => {
74
- if (__DEV__) {
75
- console.log("[CreationsSubscription] No-op unsubscribe called (no listener was created)");
76
- }
77
- };
78
- }
79
-
80
- /**
81
- * Creates the realtime listener with optimized query
82
- */
83
- private createRealtimeListener(
84
- userCollection: any,
85
- onData: CreationsSubscriptionCallback,
86
- onError?: (error: Error) => void,
87
- ): UnsubscribeFunction {
88
42
  const q = query(
89
43
  userCollection,
90
44
  where(CREATION_FIELDS.DELETED_AT, "==", null),
@@ -101,22 +55,14 @@ export class CreationsSubscription {
101
55
  });
102
56
 
103
57
  if (__DEV__) {
104
- console.log("[CreationsSubscription] Realtime sync:", {
105
- count: creations.length,
106
- serverFiltered: true,
107
- hasChanges: snapshot.docChanges().length,
108
- });
58
+ console.log("[CreationsSubscription] Sync:", creations.length, "items");
109
59
  }
110
60
 
111
61
  onData(creations);
112
62
  },
113
63
  (error: Error) => {
114
64
  if (__DEV__) {
115
- console.error("[CreationsSubscription] Realtime subscription error:", {
116
- error: error.message,
117
- code: (error as { code?: string }).code,
118
- userId,
119
- });
65
+ console.error("[CreationsSubscription] Error:", error.message);
120
66
  }
121
67
  onError?.(error);
122
68
  },
@@ -3,7 +3,7 @@
3
3
  * Main class that orchestrates all creation write operations
4
4
  */
5
5
 
6
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
6
+ import type { GetUserCollection, GetDocRef } from "./CreationsFetcher";
7
7
  import type { Creation } from "../../domain/entities/Creation";
8
8
  import * as operations from "./creations-operations";
9
9
  import * as stateOperations from "./creations-state-operations";
@@ -12,37 +12,40 @@ import * as stateOperations from "./creations-state-operations";
12
12
  * Handles write operations for creations
13
13
  */
14
14
  export class CreationsWriter {
15
- constructor(private readonly pathResolver: FirestorePathResolver) {}
15
+ constructor(
16
+ private readonly getUserCollection: GetUserCollection,
17
+ private readonly getDocRef: GetDocRef,
18
+ ) {}
16
19
 
17
20
  async create(userId: string, creation: Creation): Promise<void> {
18
- return operations.createCreation(this.pathResolver, userId, creation);
21
+ return operations.createCreation(this.getUserCollection, this.getDocRef, userId, creation);
19
22
  }
20
23
 
21
24
  async update(userId: string, id: string, updates: Partial<Creation>): Promise<boolean> {
22
- return operations.updateCreation(this.pathResolver, userId, id, updates);
25
+ return operations.updateCreation(this.getDocRef, userId, id, updates);
23
26
  }
24
27
 
25
28
  async delete(userId: string, creationId: string): Promise<boolean> {
26
- return operations.deleteCreation(this.pathResolver, userId, creationId);
29
+ return operations.deleteCreation(this.getDocRef, userId, creationId);
27
30
  }
28
31
 
29
32
  async hardDelete(userId: string, creationId: string): Promise<boolean> {
30
- return operations.hardDeleteCreation(this.pathResolver, userId, creationId);
33
+ return operations.hardDeleteCreation(this.getDocRef, userId, creationId);
31
34
  }
32
35
 
33
36
  async restore(userId: string, creationId: string): Promise<boolean> {
34
- return operations.restoreCreation(this.pathResolver, userId, creationId);
37
+ return operations.restoreCreation(this.getDocRef, userId, creationId);
35
38
  }
36
39
 
37
40
  async updateShared(userId: string, creationId: string, isShared: boolean): Promise<boolean> {
38
- return stateOperations.updateCreationShared(this.pathResolver, userId, creationId, isShared);
41
+ return stateOperations.updateCreationShared(this.getDocRef, userId, creationId, isShared);
39
42
  }
40
43
 
41
44
  async updateFavorite(userId: string, creationId: string, isFavorite: boolean): Promise<boolean> {
42
- return stateOperations.updateCreationFavorite(this.pathResolver, userId, creationId, isFavorite);
45
+ return stateOperations.updateCreationFavorite(this.getDocRef, userId, creationId, isFavorite);
43
46
  }
44
47
 
45
48
  async rate(userId: string, creationId: string, rating: number, description?: string): Promise<boolean> {
46
- return stateOperations.rateCreation(this.pathResolver, userId, creationId, rating, description);
49
+ return stateOperations.rateCreation(this.getDocRef, userId, creationId, rating, description);
47
50
  }
48
51
  }
@@ -3,18 +3,19 @@
3
3
  */
4
4
 
5
5
  import { setDoc } from "firebase/firestore";
6
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
6
+ import type { GetUserCollection, GetDocRef } from "./CreationsFetcher";
7
7
  import type { Creation, CreationDocument } from "../../domain/entities/Creation";
8
8
 
9
9
  /**
10
10
  * Creates a new creation document
11
11
  */
12
12
  export async function createCreation(
13
- pathResolver: FirestorePathResolver,
13
+ _getUserCollection: GetUserCollection,
14
+ getDocRef: GetDocRef,
14
15
  userId: string,
15
16
  creation: Creation
16
17
  ): Promise<void> {
17
- const docRef = pathResolver.getDocRef(userId, creation.id);
18
+ const docRef = getDocRef(userId, creation.id);
18
19
  if (!docRef) throw new Error("Firestore not initialized");
19
20
 
20
21
  const data: CreationDocument = {
@@ -1,122 +1,86 @@
1
1
  /**
2
2
  * Creation Delete Operations
3
+ * Single Responsibility: Delete/restore operations with centralized error handling
3
4
  */
4
5
 
5
6
  import { updateDoc, deleteDoc } from "firebase/firestore";
6
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
7
-
8
- declare const __DEV__: boolean;
7
+ import type { GetDocRef } from "./CreationsFetcher";
8
+ import { logOperationError, logOperationSuccess, logInvalidRef } from "./creation-error-handler.util";
9
9
 
10
10
  /**
11
- * Soft deletes a creation
11
+ * Soft deletes a creation by setting deletedAt timestamp
12
12
  */
13
13
  export async function deleteCreation(
14
- pathResolver: FirestorePathResolver,
14
+ getDocRef: GetDocRef,
15
15
  userId: string,
16
16
  creationId: string
17
17
  ): Promise<boolean> {
18
- const docRef = pathResolver.getDocRef(userId, creationId);
18
+ const docRef = getDocRef(userId, creationId);
19
+ const context = { userId, creationId };
20
+
19
21
  if (!docRef) {
20
- if (__DEV__) {
21
- console.error("[CreationDelete] Cannot delete: Invalid document reference", {
22
- userId,
23
- creationId,
24
- });
25
- }
22
+ logInvalidRef("Delete", context);
26
23
  return false;
27
24
  }
28
25
 
29
26
  try {
30
27
  await updateDoc(docRef, { deletedAt: new Date() });
31
- if (__DEV__) {
32
- console.log("[CreationDelete] Soft deleted successfully", { userId, creationId });
33
- }
28
+ logOperationSuccess("Delete", context);
34
29
  return true;
35
30
  } catch (error) {
36
- if (__DEV__) {
37
- console.error("[CreationDelete] Soft delete failed", {
38
- userId,
39
- creationId,
40
- error: error instanceof Error ? error.message : String(error),
41
- code: (error as { code?: string })?.code,
42
- });
43
- }
31
+ logOperationError("Delete", context, error);
44
32
  return false;
45
33
  }
46
34
  }
47
35
 
48
36
  /**
49
- * Hard deletes a creation
37
+ * Permanently deletes a creation from Firestore
50
38
  */
51
39
  export async function hardDeleteCreation(
52
- pathResolver: FirestorePathResolver,
40
+ getDocRef: GetDocRef,
53
41
  userId: string,
54
42
  creationId: string
55
43
  ): Promise<boolean> {
56
- const docRef = pathResolver.getDocRef(userId, creationId);
44
+ const docRef = getDocRef(userId, creationId);
45
+ const context = { userId, creationId };
46
+
57
47
  if (!docRef) {
58
- if (__DEV__) {
59
- console.error("[CreationDelete] Cannot hard delete: Invalid document reference", {
60
- userId,
61
- creationId,
62
- });
63
- }
48
+ logInvalidRef("HardDelete", context);
64
49
  return false;
65
50
  }
66
51
 
67
52
  try {
68
53
  await deleteDoc(docRef);
69
- if (__DEV__) {
70
- console.log("[CreationDelete] Hard deleted successfully", { userId, creationId });
71
- }
54
+ logOperationSuccess("HardDelete", context);
72
55
  return true;
73
56
  } catch (error) {
74
- if (__DEV__) {
75
- console.error("[CreationDelete] Hard delete failed", {
76
- userId,
77
- creationId,
78
- error: error instanceof Error ? error.message : String(error),
79
- code: (error as { code?: string })?.code,
80
- });
81
- }
57
+ logOperationError("HardDelete", context, error);
82
58
  return false;
83
59
  }
84
60
  }
85
61
 
86
62
  /**
87
- * Restores a soft-deleted creation
63
+ * Restores a soft-deleted creation by clearing deletedAt
88
64
  */
89
65
  export async function restoreCreation(
90
- pathResolver: FirestorePathResolver,
66
+ getDocRef: GetDocRef,
91
67
  userId: string,
92
68
  creationId: string
93
69
  ): Promise<boolean> {
94
- const docRef = pathResolver.getDocRef(userId, creationId);
70
+ const docRef = getDocRef(userId, creationId);
71
+ const context = { userId, creationId };
72
+
95
73
  if (!docRef) {
96
- if (__DEV__) {
97
- console.error("[CreationDelete] Cannot restore: Invalid document reference", {
98
- userId,
99
- creationId,
100
- });
101
- }
74
+ logInvalidRef("Restore", context);
102
75
  return false;
103
76
  }
104
77
 
105
78
  try {
106
79
  await updateDoc(docRef, { deletedAt: null });
107
- if (__DEV__) {
108
- console.log("[CreationDelete] Restored successfully", { userId, creationId });
109
- }
80
+ logOperationSuccess("Restore", context);
110
81
  return true;
111
82
  } catch (error) {
112
- if (__DEV__) {
113
- console.error("[CreationDelete] Restore failed", {
114
- userId,
115
- creationId,
116
- error: error instanceof Error ? error.message : String(error),
117
- code: (error as { code?: string })?.code,
118
- });
119
- }
83
+ logOperationError("Restore", context, error);
120
84
  return false;
121
85
  }
122
86
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Creation Error Handler Utility
3
+ * Single Responsibility: Centralized error logging
4
+ */
5
+
6
+ declare const __DEV__: boolean;
7
+
8
+ export function logOperationError(
9
+ operation: string,
10
+ context: { userId: string; creationId: string },
11
+ error: unknown
12
+ ): void {
13
+ if (!__DEV__) return;
14
+
15
+ console.error(`[Creation${operation}] Failed:`, {
16
+ ...context,
17
+ error: error instanceof Error ? error.message : String(error),
18
+ code: (error as { code?: string })?.code,
19
+ });
20
+ }
21
+
22
+ export function logOperationSuccess(
23
+ operation: string,
24
+ context: { userId: string; creationId: string }
25
+ ): void {
26
+ if (!__DEV__) return;
27
+ console.log(`[Creation${operation}] Success:`, context);
28
+ }
29
+
30
+ export function logInvalidRef(
31
+ operation: string,
32
+ context: { userId: string; creationId: string }
33
+ ): void {
34
+ if (!__DEV__) return;
35
+ console.error(`[Creation${operation}] Invalid document reference:`, context);
36
+ }
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { updateDoc } from "firebase/firestore";
6
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
6
+ import type { GetDocRef } from "./CreationsFetcher";
7
7
  import type { Creation } from "../../domain/entities/Creation";
8
8
  import { CREATION_FIELDS, type CreationFieldName } from "../../domain/constants";
9
9
 
@@ -34,12 +34,12 @@ export const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
34
34
  * Updates a creation document
35
35
  */
36
36
  export async function updateCreation(
37
- pathResolver: FirestorePathResolver,
37
+ getDocRef: GetDocRef,
38
38
  userId: string,
39
39
  id: string,
40
40
  updates: Partial<Creation>
41
41
  ): Promise<boolean> {
42
- const docRef = pathResolver.getDocRef(userId, id);
42
+ const docRef = getDocRef(userId, id);
43
43
 
44
44
  if (!docRef) {
45
45
  throw new Error(
@@ -4,19 +4,19 @@
4
4
  */
5
5
 
6
6
  import { updateDoc } from "firebase/firestore";
7
- import { type FirestorePathResolver } from "@umituz/react-native-firebase";
7
+ import type { GetDocRef } from "./CreationsFetcher";
8
8
  import { submitFeedback } from "@umituz/react-native-subscription";
9
9
 
10
10
  /**
11
11
  * Updates the shared status of a creation
12
12
  */
13
13
  export async function updateCreationShared(
14
- pathResolver: FirestorePathResolver,
14
+ getDocRef: GetDocRef,
15
15
  userId: string,
16
16
  creationId: string,
17
17
  isShared: boolean
18
18
  ): Promise<boolean> {
19
- const docRef = pathResolver.getDocRef(userId, creationId);
19
+ const docRef = getDocRef(userId, creationId);
20
20
  if (!docRef) return false;
21
21
 
22
22
  try {
@@ -31,12 +31,12 @@ export async function updateCreationShared(
31
31
  * Updates the favorite status of a creation
32
32
  */
33
33
  export async function updateCreationFavorite(
34
- pathResolver: FirestorePathResolver,
34
+ getDocRef: GetDocRef,
35
35
  userId: string,
36
36
  creationId: string,
37
37
  isFavorite: boolean
38
38
  ): Promise<boolean> {
39
- const docRef = pathResolver.getDocRef(userId, creationId);
39
+ const docRef = getDocRef(userId, creationId);
40
40
  if (!docRef) return false;
41
41
 
42
42
  try {
@@ -51,13 +51,13 @@ export async function updateCreationFavorite(
51
51
  * Rates a creation and optionally submits feedback
52
52
  */
53
53
  export async function rateCreation(
54
- pathResolver: FirestorePathResolver,
54
+ getDocRef: GetDocRef,
55
55
  userId: string,
56
56
  creationId: string,
57
57
  rating: number,
58
58
  description?: string
59
59
  ): Promise<boolean> {
60
- const docRef = pathResolver.getDocRef(userId, creationId);
60
+ const docRef = getDocRef(userId, creationId);
61
61
  if (!docRef) return false;
62
62
 
63
63
  try {
@@ -49,57 +49,56 @@ export function useProcessingJobsPoller(
49
49
  [creations],
50
50
  );
51
51
 
52
- const pollJob = useCallback(
53
- async (creation: Creation) => {
54
- if (!userId || !creation.requestId || !creation.model) return;
55
- if (pollingRef.current.has(creation.id)) return;
56
-
57
- const provider = providerRegistry.getActiveProvider();
58
- if (!provider || !provider.isInitialized()) return;
59
-
60
- pollingRef.current.add(creation.id);
61
-
62
- try {
63
- if (typeof __DEV__ !== "undefined" && __DEV__) {
64
- console.log("[ProcessingJobsPoller] Checking status:", creation.id);
65
- }
66
-
67
- const status = await provider.getJobStatus(creation.model, creation.requestId);
68
-
69
- if (typeof __DEV__ !== "undefined" && __DEV__) {
70
- console.log("[ProcessingJobsPoller] Status:", creation.id, status.status);
71
- }
72
-
73
- if (status.status === QUEUE_STATUS.COMPLETED) {
74
- const result = await provider.getJobResult<FalResult>(creation.model, creation.requestId);
75
- const urls = extractResultUrl(result);
76
- if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
77
-
78
- const uri = urls.videoUrl || urls.imageUrl || "";
79
- await repository.update(userId, creation.id, {
80
- status: CREATION_STATUS.COMPLETED,
81
- uri,
82
- output: urls,
83
- });
84
- } else if (status.status === QUEUE_STATUS.FAILED) {
85
- if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Failed:", creation.id);
86
-
87
- await repository.update(userId, creation.id, {
88
- status: CREATION_STATUS.FAILED,
89
- metadata: { error: "Generation failed" },
90
- });
91
- }
92
- // If still IN_PROGRESS or IN_QUEUE, we'll check again next interval
93
- } catch (error) {
94
- if (typeof __DEV__ !== "undefined" && __DEV__) {
95
- console.error("[ProcessingJobsPoller] Poll error:", creation.id, error);
96
- }
97
- } finally {
98
- pollingRef.current.delete(creation.id);
52
+ // Use ref for stable function reference to prevent effect re-runs
53
+ const pollJobRef = useRef<(creation: Creation) => Promise<void>>();
54
+
55
+ pollJobRef.current = async (creation: Creation) => {
56
+ if (!userId || !creation.requestId || !creation.model) return;
57
+ if (pollingRef.current.has(creation.id)) return;
58
+
59
+ const provider = providerRegistry.getActiveProvider();
60
+ if (!provider || !provider.isInitialized()) return;
61
+
62
+ pollingRef.current.add(creation.id);
63
+
64
+ try {
65
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
66
+ console.log("[ProcessingJobsPoller] Checking status:", creation.id);
99
67
  }
100
- },
101
- [userId, repository],
102
- );
68
+
69
+ const status = await provider.getJobStatus(creation.model, creation.requestId);
70
+
71
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
72
+ console.log("[ProcessingJobsPoller] Status:", creation.id, status.status);
73
+ }
74
+
75
+ if (status.status === QUEUE_STATUS.COMPLETED) {
76
+ const result = await provider.getJobResult<FalResult>(creation.model, creation.requestId);
77
+ const urls = extractResultUrl(result);
78
+ if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
79
+
80
+ const uri = urls.videoUrl || urls.imageUrl || "";
81
+ await repository.update(userId, creation.id, {
82
+ status: CREATION_STATUS.COMPLETED,
83
+ uri,
84
+ output: urls,
85
+ });
86
+ } else if (status.status === QUEUE_STATUS.FAILED) {
87
+ if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Failed:", creation.id);
88
+
89
+ await repository.update(userId, creation.id, {
90
+ status: CREATION_STATUS.FAILED,
91
+ metadata: { error: "Generation failed" },
92
+ });
93
+ }
94
+ } catch (error) {
95
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
96
+ console.error("[ProcessingJobsPoller] Poll error:", creation.id, error);
97
+ }
98
+ } finally {
99
+ pollingRef.current.delete(creation.id);
100
+ }
101
+ };
103
102
 
104
103
  useEffect(() => {
105
104
  if (!enabled || !userId || processingJobs.length === 0) {
@@ -111,11 +110,11 @@ export function useProcessingJobsPoller(
111
110
  }
112
111
 
113
112
  // Initial poll
114
- processingJobs.forEach((job) => void pollJob(job));
113
+ processingJobs.forEach((job) => pollJobRef.current?.(job));
115
114
 
116
115
  // Set up interval polling
117
116
  intervalRef.current = setInterval(() => {
118
- processingJobs.forEach((job) => void pollJob(job));
117
+ processingJobs.forEach((job) => pollJobRef.current?.(job));
119
118
  }, DEFAULT_POLL_INTERVAL_MS);
120
119
 
121
120
  return () => {
@@ -128,7 +127,7 @@ export function useProcessingJobsPoller(
128
127
  intervalRef.current = null;
129
128
  }
130
129
  };
131
- }, [enabled, userId, processingJobs, pollJob]);
130
+ }, [enabled, userId, processingJobs]);
132
131
 
133
132
  return {
134
133
  processingCount: processingJobs.length,