@umituz/react-native-ai-generation-content 1.0.2 → 1.0.6

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.0.2",
3
+ "version": "1.0.6",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -53,26 +53,50 @@ export { DEFAULT_POLLING_CONFIG, DEFAULT_PROGRESS_STAGES } from "./domain/entiti
53
53
  export {
54
54
  providerRegistry,
55
55
  generationOrchestrator,
56
+ pollJob,
57
+ createJobPoller,
56
58
  } from "./infrastructure/services";
57
59
 
58
- export type { OrchestratorConfig } from "./infrastructure/services";
60
+ export type {
61
+ OrchestratorConfig,
62
+ PollJobOptions,
63
+ PollJobResult,
64
+ } from "./infrastructure/services";
59
65
 
60
66
  // =============================================================================
61
67
  // INFRASTRUCTURE LAYER - Utils
62
68
  // =============================================================================
63
69
 
64
70
  export {
71
+ // Error classification
65
72
  classifyError,
66
73
  isTransientError,
67
74
  isPermanentError,
75
+ // Polling
68
76
  calculatePollingInterval,
69
77
  createPollingDelay,
78
+ // Progress
70
79
  getProgressForStatus,
71
80
  interpolateProgress,
72
81
  createProgressTracker,
82
+ // Status checking
83
+ checkStatusForErrors,
84
+ isJobComplete,
85
+ isJobProcessing,
86
+ isJobFailed,
87
+ // Result validation
88
+ validateResult,
89
+ extractOutputUrl,
90
+ extractOutputUrls,
73
91
  } from "./infrastructure/utils";
74
92
 
75
- export type { IntervalOptions, ProgressOptions } from "./infrastructure/utils";
93
+ export type {
94
+ IntervalOptions,
95
+ ProgressOptions,
96
+ StatusCheckResult,
97
+ ResultValidation,
98
+ ValidateResultOptions,
99
+ } from "./infrastructure/utils";
76
100
 
77
101
  // =============================================================================
78
102
  // PRESENTATION LAYER - Hooks
@@ -5,3 +5,5 @@
5
5
  export { providerRegistry } from "./provider-registry.service";
6
6
  export { generationOrchestrator } from "./generation-orchestrator.service";
7
7
  export type { OrchestratorConfig } from "./generation-orchestrator.service";
8
+ export { pollJob, createJobPoller } from "./job-poller.service";
9
+ export type { PollJobOptions, PollJobResult } from "./job-poller.service";
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Job Poller Service
3
+ * Provider-agnostic job polling with exponential backoff
4
+ */
5
+
6
+ import type { IAIProvider, JobStatus } from "../../domain/interfaces";
7
+ import type { PollingConfig } from "../../domain/entities";
8
+ import { DEFAULT_POLLING_CONFIG } from "../../domain/entities";
9
+ import { calculatePollingInterval } from "../utils/polling-interval.util";
10
+ import { checkStatusForErrors, isJobComplete } from "../utils/status-checker.util";
11
+ import { validateResult } from "../utils/result-validator.util";
12
+ import { isTransientError } from "../utils/error-classifier.util";
13
+
14
+ declare const __DEV__: boolean;
15
+
16
+ export interface PollJobOptions {
17
+ provider: IAIProvider;
18
+ model: string;
19
+ requestId: string;
20
+ config?: Partial<PollingConfig>;
21
+ onProgress?: (progress: number) => void;
22
+ onStatusChange?: (status: JobStatus) => void;
23
+ signal?: AbortSignal;
24
+ }
25
+
26
+ export interface PollJobResult<T = unknown> {
27
+ success: boolean;
28
+ data?: T;
29
+ error?: Error;
30
+ attempts: number;
31
+ elapsedMs: number;
32
+ }
33
+
34
+ const MAX_CONSECUTIVE_TRANSIENT_ERRORS = 5;
35
+
36
+ /**
37
+ * Poll job until completion with exponential backoff
38
+ */
39
+ export async function pollJob<T = unknown>(
40
+ options: PollJobOptions,
41
+ ): Promise<PollJobResult<T>> {
42
+ const {
43
+ provider,
44
+ model,
45
+ requestId,
46
+ config,
47
+ onProgress,
48
+ onStatusChange,
49
+ signal,
50
+ } = options;
51
+
52
+ const pollingConfig = { ...DEFAULT_POLLING_CONFIG, ...config };
53
+ const { maxAttempts } = pollingConfig;
54
+
55
+ const startTime = Date.now();
56
+ let consecutiveTransientErrors = 0;
57
+ let lastProgress = 0;
58
+
59
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
60
+ // Check for abort
61
+ if (signal?.aborted) {
62
+ return {
63
+ success: false,
64
+ error: new Error("Polling aborted"),
65
+ attempts: attempt + 1,
66
+ elapsedMs: Date.now() - startTime,
67
+ };
68
+ }
69
+
70
+ // Wait for polling interval (skip first attempt)
71
+ if (attempt > 0) {
72
+ const interval = calculatePollingInterval({ attempt, config: pollingConfig });
73
+ await new Promise((resolve) => setTimeout(resolve, interval));
74
+ }
75
+
76
+ try {
77
+ // Get job status
78
+ const status = await provider.getJobStatus(model, requestId);
79
+ onStatusChange?.(status);
80
+
81
+ // Check for errors in status
82
+ const statusCheck = checkStatusForErrors(status);
83
+
84
+ if (statusCheck.shouldStop && statusCheck.hasError) {
85
+ return {
86
+ success: false,
87
+ error: new Error(statusCheck.errorMessage || "Job failed"),
88
+ attempts: attempt + 1,
89
+ elapsedMs: Date.now() - startTime,
90
+ };
91
+ }
92
+
93
+ // Reset transient error counter on success
94
+ consecutiveTransientErrors = 0;
95
+
96
+ // Update progress
97
+ const progress = calculateProgressFromStatus(status, attempt, maxAttempts);
98
+ if (progress > lastProgress) {
99
+ lastProgress = progress;
100
+ onProgress?.(progress);
101
+ }
102
+
103
+ // Check if complete
104
+ if (isJobComplete(status)) {
105
+ onProgress?.(90);
106
+
107
+ // Fetch result
108
+ const result = await provider.getJobResult<T>(model, requestId);
109
+
110
+ // Validate result
111
+ const validation = validateResult(result);
112
+ if (!validation.isValid) {
113
+ return {
114
+ success: false,
115
+ error: new Error(validation.errorMessage || "Invalid result"),
116
+ attempts: attempt + 1,
117
+ elapsedMs: Date.now() - startTime,
118
+ };
119
+ }
120
+
121
+ onProgress?.(100);
122
+
123
+ return {
124
+ success: true,
125
+ data: result,
126
+ attempts: attempt + 1,
127
+ elapsedMs: Date.now() - startTime,
128
+ };
129
+ }
130
+ } catch (error) {
131
+ if (isTransientError(error)) {
132
+ consecutiveTransientErrors++;
133
+
134
+ // Too many consecutive transient errors
135
+ if (consecutiveTransientErrors >= MAX_CONSECUTIVE_TRANSIENT_ERRORS) {
136
+ return {
137
+ success: false,
138
+ error: error instanceof Error ? error : new Error(String(error)),
139
+ attempts: attempt + 1,
140
+ elapsedMs: Date.now() - startTime,
141
+ };
142
+ }
143
+
144
+ // Continue retrying
145
+ if (attempt < maxAttempts - 1) {
146
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
147
+ // eslint-disable-next-line no-console
148
+ console.log(
149
+ `[JobPoller] Transient error, retrying (${attempt + 1}/${maxAttempts})`,
150
+ );
151
+ }
152
+ continue;
153
+ }
154
+ }
155
+
156
+ // Permanent error or max retries reached
157
+ return {
158
+ success: false,
159
+ error: error instanceof Error ? error : new Error(String(error)),
160
+ attempts: attempt + 1,
161
+ elapsedMs: Date.now() - startTime,
162
+ };
163
+ }
164
+ }
165
+
166
+ // Max attempts reached
167
+ return {
168
+ success: false,
169
+ error: new Error(`Polling timeout after ${maxAttempts} attempts`),
170
+ attempts: maxAttempts,
171
+ elapsedMs: Date.now() - startTime,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Calculate progress percentage from job status
177
+ */
178
+ function calculateProgressFromStatus(
179
+ status: JobStatus,
180
+ attempt: number,
181
+ maxAttempts: number,
182
+ ): number {
183
+ const statusString = String(status.status).toUpperCase();
184
+
185
+ switch (statusString) {
186
+ case "IN_QUEUE":
187
+ return 30 + Math.min(attempt * 2, 10);
188
+ case "IN_PROGRESS":
189
+ return 50 + Math.min(attempt * 3, 30);
190
+ case "COMPLETED":
191
+ return 90;
192
+ case "FAILED":
193
+ return 0;
194
+ default:
195
+ return 20 + Math.min((attempt / maxAttempts) * 30, 30);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Create a job poller with preset configuration
201
+ */
202
+ export function createJobPoller(defaultConfig?: Partial<PollingConfig>) {
203
+ return {
204
+ poll<T = unknown>(options: Omit<PollJobOptions, "config">) {
205
+ return pollJob<T>({ ...options, config: defaultConfig });
206
+ },
207
+ };
208
+ }
@@ -5,3 +5,5 @@
5
5
  export * from "./error-classifier.util";
6
6
  export * from "./polling-interval.util";
7
7
  export * from "./progress-calculator.util";
8
+ export * from "./status-checker.util";
9
+ export * from "./result-validator.util";
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Result Validator Utility
3
+ * Validates AI generation job results
4
+ */
5
+
6
+ export interface ResultValidation {
7
+ isValid: boolean;
8
+ hasError: boolean;
9
+ errorMessage?: string;
10
+ hasOutput: boolean;
11
+ }
12
+
13
+ export interface ValidateResultOptions {
14
+ /** Custom output field names to check */
15
+ outputFields?: string[];
16
+ /** Whether empty results are allowed */
17
+ allowEmpty?: boolean;
18
+ }
19
+
20
+ const DEFAULT_OUTPUT_FIELDS = [
21
+ "data",
22
+ "output",
23
+ "image",
24
+ "image_url",
25
+ "images",
26
+ "video",
27
+ "video_url",
28
+ "url",
29
+ "result",
30
+ "text",
31
+ "content",
32
+ ];
33
+
34
+ /**
35
+ * Validate job result and detect errors
36
+ * Checks for error fields even if job status was COMPLETED
37
+ */
38
+ export function validateResult(
39
+ result: unknown,
40
+ options?: ValidateResultOptions,
41
+ ): ResultValidation {
42
+ const { outputFields = DEFAULT_OUTPUT_FIELDS, allowEmpty = false } =
43
+ options ?? {};
44
+
45
+ // Handle null/undefined
46
+ if (result === null || result === undefined) {
47
+ return {
48
+ isValid: allowEmpty,
49
+ hasError: !allowEmpty,
50
+ errorMessage: allowEmpty ? undefined : "Result is empty",
51
+ hasOutput: false,
52
+ };
53
+ }
54
+
55
+ // Handle non-object results
56
+ if (typeof result !== "object") {
57
+ return {
58
+ isValid: true,
59
+ hasError: false,
60
+ hasOutput: true,
61
+ };
62
+ }
63
+
64
+ const resultObj = result as Record<string, unknown>;
65
+
66
+ // Check for error fields
67
+ const errorValue = resultObj.error || resultObj.detail;
68
+ const errorString = errorValue ? String(errorValue).toLowerCase() : "";
69
+
70
+ const hasInternalServerError =
71
+ errorString.includes("internal server error") ||
72
+ errorString.includes("500") ||
73
+ errorString === "internal server error";
74
+
75
+ // Check for empty object
76
+ const isEmpty = Object.keys(resultObj).length === 0;
77
+
78
+ // Check for output in expected fields
79
+ const hasOutput = outputFields.some((field) => {
80
+ const value = resultObj[field];
81
+ if (!value) return false;
82
+
83
+ // Handle nested output structures
84
+ if (typeof value === "object" && value !== null) {
85
+ const nested = value as Record<string, unknown>;
86
+ return !!(
87
+ nested.url ||
88
+ nested.image_url ||
89
+ nested.video_url ||
90
+ Object.keys(nested).length > 0
91
+ );
92
+ }
93
+
94
+ return true;
95
+ });
96
+
97
+ // Determine if result has error
98
+ const hasError =
99
+ hasInternalServerError || (isEmpty && !hasOutput && !allowEmpty);
100
+
101
+ return {
102
+ isValid: !hasError && (hasOutput || allowEmpty),
103
+ hasError,
104
+ errorMessage: hasError && errorValue ? String(errorValue) : undefined,
105
+ hasOutput,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Extract output URL from result
111
+ * Supports various AI provider response formats
112
+ */
113
+ export function extractOutputUrl(
114
+ result: unknown,
115
+ urlFields?: string[],
116
+ ): string | undefined {
117
+ if (!result || typeof result !== "object") {
118
+ return undefined;
119
+ }
120
+
121
+ const fields = urlFields ?? [
122
+ "url",
123
+ "image_url",
124
+ "video_url",
125
+ "output_url",
126
+ "result_url",
127
+ ];
128
+
129
+ const resultObj = result as Record<string, unknown>;
130
+
131
+ // Check top-level fields
132
+ for (const field of fields) {
133
+ const value = resultObj[field];
134
+ if (typeof value === "string" && value.length > 0) {
135
+ return value;
136
+ }
137
+ }
138
+
139
+ // Check nested data/output objects
140
+ const nested =
141
+ (resultObj.data as Record<string, unknown>) ||
142
+ (resultObj.output as Record<string, unknown>) ||
143
+ (resultObj.result as Record<string, unknown>);
144
+
145
+ if (nested && typeof nested === "object") {
146
+ for (const field of fields) {
147
+ const value = nested[field];
148
+ if (typeof value === "string" && value.length > 0) {
149
+ return value;
150
+ }
151
+ }
152
+
153
+ // Check for nested image/video objects
154
+ const media =
155
+ (nested.image as Record<string, unknown>) ||
156
+ (nested.video as Record<string, unknown>);
157
+ if (media && typeof media === "object" && typeof media.url === "string") {
158
+ return media.url;
159
+ }
160
+ }
161
+
162
+ return undefined;
163
+ }
164
+
165
+ /**
166
+ * Extract multiple output URLs from result
167
+ */
168
+ export function extractOutputUrls(
169
+ result: unknown,
170
+ urlFields?: string[],
171
+ ): string[] {
172
+ if (!result || typeof result !== "object") {
173
+ return [];
174
+ }
175
+
176
+ const urls: string[] = [];
177
+ const resultObj = result as Record<string, unknown>;
178
+
179
+ // Check for arrays
180
+ const arrayFields = ["images", "videos", "outputs", "results", "urls"];
181
+ for (const field of arrayFields) {
182
+ const arr = resultObj[field];
183
+ if (Array.isArray(arr)) {
184
+ for (const item of arr) {
185
+ const url = extractOutputUrl(item, urlFields);
186
+ if (url) {
187
+ urls.push(url);
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ // Check nested data/output
194
+ const nested = resultObj.data || resultObj.output;
195
+ if (nested && typeof nested === "object") {
196
+ for (const field of arrayFields) {
197
+ const arr = (nested as Record<string, unknown>)[field];
198
+ if (Array.isArray(arr)) {
199
+ for (const item of arr) {
200
+ const url = extractOutputUrl(item, urlFields);
201
+ if (url) {
202
+ urls.push(url);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ // If no array found, try single URL
210
+ if (urls.length === 0) {
211
+ const singleUrl = extractOutputUrl(result, urlFields);
212
+ if (singleUrl) {
213
+ urls.push(singleUrl);
214
+ }
215
+ }
216
+
217
+ return urls;
218
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Status Checker Utility
3
+ * Checks job status responses for errors
4
+ */
5
+
6
+ import type { JobStatus, AILogEntry } from "../../domain/interfaces";
7
+
8
+ export interface StatusCheckResult {
9
+ status: string;
10
+ hasError: boolean;
11
+ errorMessage?: string;
12
+ shouldStop: boolean;
13
+ }
14
+
15
+ /**
16
+ * Check job status response for errors
17
+ * Detects errors even if status is not explicitly FAILED
18
+ */
19
+ export function checkStatusForErrors(
20
+ status: JobStatus | Record<string, unknown>,
21
+ ): StatusCheckResult {
22
+ const statusString = String(
23
+ (status as Record<string, unknown>)?.status || "",
24
+ ).toUpperCase();
25
+
26
+ // Check for error in status response fields
27
+ const statusError =
28
+ (status as Record<string, unknown>)?.error ||
29
+ (status as Record<string, unknown>)?.detail ||
30
+ (status as Record<string, unknown>)?.message;
31
+ const hasStatusError = !!statusError;
32
+
33
+ // Check logs array for ERROR/FATAL level logs
34
+ const logs = Array.isArray((status as JobStatus)?.logs)
35
+ ? (status as JobStatus).logs
36
+ : [];
37
+ const errorLogs = (logs as AILogEntry[]).filter((log) => {
38
+ const level = String(log?.level || "").toUpperCase();
39
+ return level === "ERROR" || level === "FATAL";
40
+ });
41
+ const hasErrorLog = errorLogs.length > 0;
42
+
43
+ // Extract error message from logs
44
+ const errorLogMessage =
45
+ errorLogs.length > 0
46
+ ? (errorLogs[0] as AILogEntry & { text?: string; content?: string })
47
+ ?.message ||
48
+ (errorLogs[0] as AILogEntry & { text?: string })?.text ||
49
+ (errorLogs[0] as AILogEntry & { content?: string })?.content
50
+ : undefined;
51
+
52
+ // Combine error messages
53
+ const errorMessage = statusError || errorLogMessage;
54
+
55
+ // Determine if we should stop immediately
56
+ const shouldStop =
57
+ statusString === "FAILED" || hasStatusError || hasErrorLog;
58
+
59
+ return {
60
+ status: statusString,
61
+ hasError: hasStatusError || hasErrorLog,
62
+ errorMessage: errorMessage ? String(errorMessage) : undefined,
63
+ shouldStop,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Check if status indicates job is complete
69
+ */
70
+ export function isJobComplete(status: JobStatus | string): boolean {
71
+ const statusString =
72
+ typeof status === "string"
73
+ ? status.toUpperCase()
74
+ : String(status?.status || "").toUpperCase();
75
+
76
+ return statusString === "COMPLETED";
77
+ }
78
+
79
+ /**
80
+ * Check if status indicates job is still processing
81
+ */
82
+ export function isJobProcessing(status: JobStatus | string): boolean {
83
+ const statusString =
84
+ typeof status === "string"
85
+ ? status.toUpperCase()
86
+ : String(status?.status || "").toUpperCase();
87
+
88
+ return statusString === "IN_QUEUE" || statusString === "IN_PROGRESS";
89
+ }
90
+
91
+ /**
92
+ * Check if status indicates job has failed
93
+ */
94
+ export function isJobFailed(status: JobStatus | string): boolean {
95
+ const statusString =
96
+ typeof status === "string"
97
+ ? status.toUpperCase()
98
+ : String(status?.status || "").toUpperCase();
99
+
100
+ return statusString === "FAILED";
101
+ }