@umituz/react-native-ai-generation-content 1.82.9 → 1.83.1

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.82.9",
3
+ "version": "1.83.1",
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",
@@ -32,18 +32,20 @@ export const CREATION_FIELDS = {
32
32
 
33
33
  // Timestamps
34
34
  CREATED_AT: "createdAt" as const,
35
+ STARTED_AT: "startedAt" as const,
35
36
  UPDATED_AT: "updatedAt" as const,
36
37
  DELETED_AT: "deletedAt" as const,
37
38
  RATED_AT: "ratedAt" as const,
39
+ COMPLETED_AT: "completedAt" as const,
40
+
41
+ // Duration (ms elapsed from startedAt to completedAt)
42
+ DURATION_MS: "durationMs" as const,
38
43
 
39
44
  // User interactions
40
45
  IS_FAVORITE: "isFavorite" as const,
41
46
  IS_SHARED: "isShared" as const,
42
47
  RATING: "rating" as const,
43
48
 
44
- // Completion timestamp
45
- COMPLETED_AT: "completedAt" as const,
46
-
47
49
  // AI provider metadata
48
50
  REQUEST_ID: "requestId" as const,
49
51
  MODEL: "model" as const,
@@ -73,6 +75,8 @@ export const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
73
75
  CREATION_FIELDS.MODEL,
74
76
  CREATION_FIELDS.PROMPT,
75
77
  CREATION_FIELDS.COMPLETED_AT,
78
+ CREATION_FIELDS.STARTED_AT,
79
+ CREATION_FIELDS.DURATION_MS,
76
80
  ] as const;
77
81
 
78
82
  /**
@@ -39,11 +39,17 @@ export interface Creation {
39
39
  readonly requestId?: string;
40
40
  readonly model?: string;
41
41
  // Timestamps
42
- readonly completedAt?: Date;
42
+ readonly startedAt?: Date; // When generation was submitted to the queue
43
+ readonly completedAt?: Date; // When generation finished (success or failure)
44
+ readonly durationMs?: number; // Elapsed ms from startedAt to completedAt
43
45
  // Soft delete - if set, the creation is considered deleted
44
46
  readonly deletedAt?: Date;
45
47
  }
46
48
 
49
+ interface FirebaseTimestamp {
50
+ toDate: () => Date;
51
+ }
52
+
47
53
  export interface CreationDocument {
48
54
  readonly uri?: string;
49
55
  readonly prompt?: string;
@@ -61,30 +67,30 @@ export interface CreationDocument {
61
67
  readonly rating?: number;
62
68
  readonly ratedAt?: FirebaseTimestamp | Date | null;
63
69
  readonly createdAt: FirebaseTimestamp | Date;
70
+ readonly startedAt?: FirebaseTimestamp | Date | null;
64
71
  readonly completedAt?: FirebaseTimestamp | Date | null;
65
72
  readonly deletedAt?: FirebaseTimestamp | Date | null;
66
73
  readonly updatedAt?: FirebaseTimestamp | Date | null;
74
+ readonly durationMs?: number;
67
75
  // Background job tracking
68
76
  readonly requestId?: string;
69
77
  readonly model?: string;
70
78
  }
71
79
 
72
- interface FirebaseTimestamp {
73
- toDate: () => Date;
80
+ function toDate(value: FirebaseTimestamp | Date | null | undefined): Date | undefined {
81
+ if (!value) return undefined;
82
+ if (value instanceof Date) return value;
83
+ if (typeof value === "object" && "toDate" in value && typeof (value as FirebaseTimestamp).toDate === "function") {
84
+ return (value as FirebaseTimestamp).toDate();
85
+ }
86
+ return undefined;
74
87
  }
75
88
 
76
89
  export function mapDocumentToCreation(
77
90
  id: string,
78
91
  data: CreationDocument,
79
92
  ): Creation {
80
- let creationDate: Date;
81
- if (data.createdAt instanceof Date) {
82
- creationDate = data.createdAt;
83
- } else if (data.createdAt && typeof data.createdAt === "object" && "toDate" in data.createdAt && typeof data.createdAt.toDate === "function") {
84
- creationDate = data.createdAt.toDate();
85
- } else {
86
- creationDate = new Date();
87
- }
93
+ const creationDate = toDate(data.createdAt) ?? new Date();
88
94
 
89
95
  // Get URI from output or direct fields
90
96
  const uri = data.output?.imageUrl ||
@@ -94,27 +100,6 @@ export function mapDocumentToCreation(
94
100
  data.uri ||
95
101
  "";
96
102
 
97
- let ratedAtDate: Date | undefined;
98
- if (data.ratedAt instanceof Date) {
99
- ratedAtDate = data.ratedAt;
100
- } else if (data.ratedAt && typeof data.ratedAt === "object" && "toDate" in data.ratedAt && typeof data.ratedAt.toDate === "function") {
101
- ratedAtDate = data.ratedAt.toDate();
102
- }
103
-
104
- let deletedAtDate: Date | undefined;
105
- if (data.deletedAt instanceof Date) {
106
- deletedAtDate = data.deletedAt;
107
- } else if (data.deletedAt && typeof data.deletedAt === "object" && "toDate" in data.deletedAt && typeof data.deletedAt.toDate === "function") {
108
- deletedAtDate = data.deletedAt.toDate();
109
- }
110
-
111
- let completedAtDate: Date | undefined;
112
- if (data.completedAt instanceof Date) {
113
- completedAtDate = data.completedAt;
114
- } else if (data.completedAt && typeof data.completedAt === "object" && "toDate" in data.completedAt && typeof data.completedAt.toDate === "function") {
115
- completedAtDate = data.completedAt.toDate();
116
- }
117
-
118
103
  return {
119
104
  id,
120
105
  uri,
@@ -126,12 +111,14 @@ export function mapDocumentToCreation(
126
111
  isShared: data.isShared ?? false,
127
112
  isFavorite: data.isFavorite ?? false,
128
113
  rating: data.rating,
129
- ratedAt: ratedAtDate,
114
+ ratedAt: toDate(data.ratedAt),
130
115
  status: data.status as CreationStatus | undefined,
131
116
  output: data.output ?? undefined,
132
117
  requestId: data.requestId,
133
118
  model: data.model,
134
- completedAt: completedAtDate,
135
- deletedAt: deletedAtDate,
119
+ startedAt: toDate(data.startedAt),
120
+ completedAt: toDate(data.completedAt),
121
+ durationMs: data.durationMs,
122
+ deletedAt: toDate(data.deletedAt),
136
123
  };
137
124
  }
@@ -27,6 +27,7 @@ export async function createCreation(
27
27
  ...(creation.prompt !== undefined && { prompt: creation.prompt }),
28
28
  ...(creation.requestId !== undefined && { requestId: creation.requestId }),
29
29
  ...(creation.model !== undefined && { model: creation.model }),
30
+ ...(creation.startedAt !== undefined && { startedAt: creation.startedAt }),
30
31
  };
31
32
 
32
33
  try {
@@ -5,27 +5,11 @@
5
5
  import { updateDoc } from "firebase/firestore";
6
6
  import type { IPathResolver } from "@umituz/react-native-firebase";
7
7
  import type { Creation } from "../../domain/entities/Creation";
8
- import { CREATION_FIELDS, type CreationFieldName } from "../../domain/constants";
8
+ import { UPDATABLE_FIELDS } from "../../domain/constants";
9
9
 
10
10
  declare const __DEV__: boolean;
11
11
 
12
- export const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
13
- CREATION_FIELDS.URI,
14
- CREATION_FIELDS.STATUS,
15
- CREATION_FIELDS.OUTPUT,
16
- CREATION_FIELDS.IMAGE_URL,
17
- CREATION_FIELDS.VIDEO_URL,
18
- CREATION_FIELDS.METADATA,
19
- CREATION_FIELDS.IS_SHARED,
20
- CREATION_FIELDS.IS_FAVORITE,
21
- CREATION_FIELDS.RATING,
22
- CREATION_FIELDS.RATED_AT,
23
- CREATION_FIELDS.DELETED_AT,
24
- CREATION_FIELDS.REQUEST_ID,
25
- CREATION_FIELDS.MODEL,
26
- CREATION_FIELDS.PROMPT,
27
- "type" as CreationFieldName,
28
- ] as const;
12
+ export { UPDATABLE_FIELDS };
29
13
 
30
14
  export async function updateCreation(
31
15
  pathResolver: IPathResolver,
@@ -26,4 +26,6 @@ export interface CompletedCreationData {
26
26
  readonly imageUrl?: string;
27
27
  readonly videoUrl?: string;
28
28
  readonly thumbnailUrl?: string;
29
+ /** Unix timestamp (ms) when generation was submitted; used to compute durationMs */
30
+ readonly generationStartedAt?: number;
29
31
  }
@@ -16,9 +16,11 @@ export async function saveAsProcessing(
16
16
  repository: ICreationsRepository,
17
17
  userId: string,
18
18
  data: ProcessingCreationData
19
- ): Promise<string> {
19
+ ): Promise<{ creationId: string; startedAt: Date }> {
20
20
  const creationId = `${data.scenarioId}_${Date.now()}`;
21
21
 
22
+ const startedAt = new Date();
23
+
22
24
  await repository.create(userId, {
23
25
  id: creationId,
24
26
  uri: "",
@@ -26,6 +28,7 @@ export async function saveAsProcessing(
26
28
  prompt: data.prompt,
27
29
  status: "processing" as const,
28
30
  createdAt: new Date(),
31
+ startedAt,
29
32
  isShared: false,
30
33
  isFavorite: false,
31
34
  requestId: data.requestId,
@@ -39,7 +42,6 @@ export async function saveAsProcessing(
39
42
  ...(data.aspectRatio && { aspectRatio: data.aspectRatio }),
40
43
  ...(data.provider && { provider: data.provider }),
41
44
  ...(data.outputType && { outputType: data.outputType }),
42
- startedAt: new Date().toISOString(),
43
45
  },
44
46
  });
45
47
 
@@ -51,5 +53,5 @@ export async function saveAsProcessing(
51
53
  });
52
54
  }
53
55
 
54
- return creationId;
56
+ return { creationId, startedAt };
55
57
  }
@@ -22,11 +22,18 @@ export async function updateToCompleted(
22
22
  if (data.videoUrl) output.videoUrl = data.videoUrl;
23
23
  if (data.thumbnailUrl) output.thumbnailUrl = data.thumbnailUrl;
24
24
 
25
+ const completedAt = new Date();
26
+ const durationMs =
27
+ data.generationStartedAt !== undefined
28
+ ? completedAt.getTime() - data.generationStartedAt
29
+ : undefined;
30
+
25
31
  await repository.update(userId, creationId, {
26
32
  uri: data.uri,
27
33
  status: "completed" as const,
28
34
  output,
29
- completedAt: new Date(),
35
+ completedAt,
36
+ ...(durationMs !== undefined && { durationMs }),
30
37
  } as Partial<import("../../../../creations/domain/entities/Creation").Creation>);
31
38
 
32
39
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -6,7 +6,10 @@ import { useEffect, useRef, useCallback, useState } from "react";
6
6
 
7
7
  declare const __DEV__: boolean;
8
8
  import { pollQueueStatus } from "./videoQueuePoller";
9
- import { DEFAULT_POLL_INTERVAL_MS } from "../../../../../infrastructure/constants/polling.constants";
9
+ import {
10
+ DEFAULT_POLL_INTERVAL_MS,
11
+ DEFAULT_MAX_POLL_TIME_MS,
12
+ } from "../../../../../infrastructure/constants/polling.constants";
10
13
  import type { GenerationUrls } from "./generation-result.utils";
11
14
  import type {
12
15
  UseVideoQueueGenerationProps,
@@ -23,6 +26,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
23
26
  const isGeneratingRef = useRef(false);
24
27
  const isPollingRef = useRef(false);
25
28
  const consecutiveErrorsRef = useRef(0);
29
+ const pollStartTimeRef = useRef<number | null>(null);
26
30
  const [isGenerating, setIsGenerating] = useState(false);
27
31
 
28
32
  const clearPolling = useCallback(() => {
@@ -38,6 +42,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
38
42
  isGeneratingRef.current = false;
39
43
  isPollingRef.current = false;
40
44
  consecutiveErrorsRef.current = 0;
45
+ pollStartTimeRef.current = null;
41
46
  creationIdRef.current = null;
42
47
  requestIdRef.current = null;
43
48
  modelRef.current = null;
@@ -53,12 +58,12 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
53
58
  isGeneratingRef.current = false;
54
59
  isPollingRef.current = false;
55
60
  consecutiveErrorsRef.current = 0;
61
+ pollStartTimeRef.current = null;
56
62
  setIsGenerating(false);
57
63
  }, [clearPolling]);
58
64
 
59
65
  const handleComplete = useCallback(
60
66
  async (urls: GenerationUrls) => {
61
- // Stop polling immediately on completion
62
67
  clearPolling();
63
68
 
64
69
  const creationId = creationIdRef.current;
@@ -74,7 +79,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
74
79
  });
75
80
  }
76
81
 
77
- // Validate non-empty URI
78
82
  if (!creationId || !userId || !uri || uri.trim() === "") {
79
83
  if (typeof __DEV__ !== "undefined" && __DEV__) {
80
84
  console.error("[VideoQueue] ❌ Invalid completion data:", { creationId, userId, uri });
@@ -92,6 +96,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
92
96
  imageUrl: urls.imageUrl,
93
97
  videoUrl: urls.videoUrl,
94
98
  thumbnailUrl: urls.thumbnailUrl,
99
+ generationStartedAt: pollStartTimeRef.current ?? undefined,
95
100
  });
96
101
  if (typeof __DEV__ !== "undefined" && __DEV__) {
97
102
  console.log("[VideoQueue] ✅ Updated completion status in Firestore");
@@ -106,8 +111,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
106
111
 
107
112
  resetRefs();
108
113
 
109
- // Still call onSuccess even if persistence failed - the generation itself succeeded
110
- // The video/image URL is valid, user should still see the result
111
114
  if (typeof __DEV__ !== "undefined" && __DEV__) {
112
115
  console.log("[VideoQueue] 🎯 Calling onSuccess callback now...", { persistenceSucceeded });
113
116
  }
@@ -139,11 +142,30 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
139
142
  [userId, persistence, onError, resetRefs, clearPolling],
140
143
  );
141
144
 
145
+ // Use a ref to hold the latest handleComplete/handleError to avoid stale closures
146
+ // in the setInterval callback
147
+ const handleCompleteRef = useRef(handleComplete);
148
+ const handleErrorRef = useRef(handleError);
149
+ useEffect(() => { handleCompleteRef.current = handleComplete; }, [handleComplete]);
150
+ useEffect(() => { handleErrorRef.current = handleError; }, [handleError]);
151
+
142
152
  const pollStatus = useCallback(async () => {
143
153
  const requestId = requestIdRef.current;
144
154
  const model = modelRef.current;
145
155
  if (!requestId || !model) return;
146
156
 
157
+ // Check max poll time
158
+ if (pollStartTimeRef.current !== null) {
159
+ const elapsed = Date.now() - pollStartTimeRef.current;
160
+ if (elapsed >= DEFAULT_MAX_POLL_TIME_MS) {
161
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
162
+ console.warn("[VideoQueue] ⏰ Max poll time exceeded, aborting");
163
+ }
164
+ await handleErrorRef.current("Generation timed out. Please try again.");
165
+ return;
166
+ }
167
+ }
168
+
147
169
  try {
148
170
  await pollQueueStatus({
149
171
  requestId,
@@ -151,15 +173,19 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
151
173
  isPollingRef,
152
174
  pollingRef,
153
175
  consecutiveErrorsRef,
154
- onComplete: handleComplete,
155
- onError: handleError,
176
+ onComplete: handleCompleteRef.current,
177
+ onError: handleErrorRef.current,
156
178
  });
157
179
  } catch (error) {
158
180
  if (typeof __DEV__ !== "undefined" && __DEV__) {
159
181
  console.error("[VideoQueue] Unexpected poll error:", error);
160
182
  }
161
183
  }
162
- }, [handleComplete, handleError]);
184
+ }, []);
185
+
186
+ // Keep a stable ref to pollStatus for the setInterval closure
187
+ const pollStatusRef = useRef(pollStatus);
188
+ useEffect(() => { pollStatusRef.current = pollStatus; }, [pollStatus]);
163
189
 
164
190
  const startGeneration = useCallback(
165
191
  async (input: unknown, prompt: string) => {
@@ -175,13 +201,12 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
175
201
  let creationId: string | null = null;
176
202
  if (userId && prompt) {
177
203
  try {
178
- // Extract generation parameters from input
179
204
  const inputData = input as Record<string, unknown>;
180
205
  const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
181
206
  const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
182
207
  const aspectRatio = typeof inputData?.aspectRatio === "string" ? inputData.aspectRatio : undefined;
183
208
 
184
- creationId = await persistence.saveAsProcessing(userId, {
209
+ const result = await persistence.saveAsProcessing(userId, {
185
210
  scenarioId: scenario.id,
186
211
  scenarioTitle: scenario.title || scenario.id,
187
212
  prompt,
@@ -192,13 +217,14 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
192
217
  provider: "fal",
193
218
  outputType: scenario.outputType,
194
219
  });
220
+ creationId = result.creationId;
195
221
  creationIdRef.current = creationId;
222
+ // Record the actual DB-level start time for accurate durationMs
223
+ pollStartTimeRef.current = result.startedAt.getTime();
196
224
  } catch (error) {
197
225
  if (typeof __DEV__ !== "undefined" && __DEV__) {
198
226
  console.error("[VideoQueue] Failed to save processing creation:", error);
199
227
  }
200
- // Continue without creation tracking - generation can still proceed
201
- // The video will be generated but won't appear in gallery history
202
228
  }
203
229
  }
204
230
 
@@ -206,7 +232,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
206
232
  try {
207
233
  queueResult = await strategy.submitToQueue(input);
208
234
  } catch (error) {
209
- // Queue submission threw - reset state and report error
210
235
  if (creationId && userId) {
211
236
  try {
212
237
  await persistence.updateToFailed(userId, creationId, error instanceof Error ? error.message : "Queue submission failed");
@@ -243,10 +268,14 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
243
268
  }
244
269
  }
245
270
 
246
- pollingRef.current = setInterval(() => void pollStatus(), DEFAULT_POLL_INTERVAL_MS);
247
- void pollStatus();
271
+ // Start polling: use DB-level startedAt if available, otherwise fallback to now
272
+ if (pollStartTimeRef.current === null) {
273
+ pollStartTimeRef.current = Date.now();
274
+ }
275
+ pollingRef.current = setInterval(() => void pollStatusRef.current(), DEFAULT_POLL_INTERVAL_MS);
276
+ void pollStatusRef.current();
248
277
  },
249
- [userId, scenario, persistence, strategy, creditCost, pollStatus, onError],
278
+ [userId, scenario, persistence, strategy, creditCost, onError],
250
279
  );
251
280
 
252
281
  return { isGenerating, startGeneration };
@@ -1,12 +1,10 @@
1
1
  import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
2
2
  import { extractResultUrl, type GenerationUrls, type GenerationResult } from "./generation-result.utils";
3
3
  import { QUEUE_STATUS } from "../../../../../domain/constants/queue-status.constants";
4
+ import { DEFAULT_MAX_CONSECUTIVE_ERRORS } from "../../../../../infrastructure/constants/polling.constants";
4
5
 
5
6
  declare const __DEV__: boolean;
6
7
 
7
- /** Max consecutive transient errors before aborting */
8
- const MAX_CONSECUTIVE_ERRORS = 5;
9
-
10
8
  /**
11
9
  * Extract meaningful error message from various error formats.
12
10
  * Fal AI client throws ValidationError with empty .message but details in .body/.detail
@@ -14,12 +12,8 @@ const MAX_CONSECUTIVE_ERRORS = 5;
14
12
  function extractErrorMessage(err: unknown): string {
15
13
  if (!err) return "Generation failed";
16
14
 
17
- // Standard Error with message
18
- if (err instanceof Error && err.message && err.message.length > 0) {
19
- return err.message;
20
- }
21
-
22
- // Fal AI ValidationError - has .body.detail array
15
+ // Fal AI ValidationError - has .body.detail array (check before instanceof Error
16
+ // because ValidationError may extend Error with empty .message)
23
17
  const errObj = err as Record<string, unknown>;
24
18
  if (errObj.body && typeof errObj.body === "object") {
25
19
  const body = errObj.body as Record<string, unknown>;
@@ -35,6 +29,11 @@ function extractErrorMessage(err: unknown): string {
35
29
  if (first?.msg) return first.msg;
36
30
  }
37
31
 
32
+ // Standard Error with message
33
+ if (err instanceof Error && err.message && err.message.length > 0) {
34
+ return err.message;
35
+ }
36
+
38
37
  // Fallback to string conversion
39
38
  const str = String(err);
40
39
  return str.length > 0 && str !== "[object Object]" ? str : "Generation failed";
@@ -96,15 +95,21 @@ export const pollQueueStatus = async (params: PollParams): Promise<void> => {
96
95
  await onError(errorMessage);
97
96
  }
98
97
  } else {
99
- await onError("Generation failed");
98
+ // Try to extract error from FAL job logs (error-level log takes priority)
99
+ const logs = status.logs ?? [];
100
+ const errorLog = logs.findLast?.((l) => l.level === "error") ?? logs[logs.length - 1];
101
+ const failMessage =
102
+ errorLog?.message && errorLog.message !== "[object Object]"
103
+ ? errorLog.message
104
+ : "Generation failed";
105
+ await onError(failMessage);
100
106
  }
101
107
  }
102
108
  } catch (err) {
103
109
  consecutiveErrorsRef.current += 1;
104
- const errorMessage = err instanceof Error ? err.message : "Generation failed";
110
+ const errorMessage = extractErrorMessage(err);
105
111
 
106
- if (consecutiveErrorsRef.current >= MAX_CONSECUTIVE_ERRORS) {
107
- // Too many consecutive errors - abort
112
+ if (consecutiveErrorsRef.current >= DEFAULT_MAX_CONSECUTIVE_ERRORS) {
108
113
  if (pollingRef.current) {
109
114
  clearInterval(pollingRef.current);
110
115
  pollingRef.current = null;
@@ -112,10 +117,9 @@ export const pollQueueStatus = async (params: PollParams): Promise<void> => {
112
117
  if (__DEV__) console.error("[VideoQueueGeneration] Max consecutive errors reached, aborting:", errorMessage);
113
118
  await onError(errorMessage);
114
119
  } else {
115
- // Transient error - continue polling
116
120
  if (__DEV__) {
117
121
  console.warn(
118
- `[VideoQueueGeneration] Transient poll error (${consecutiveErrorsRef.current}/${MAX_CONSECUTIVE_ERRORS}):`,
122
+ `[VideoQueueGeneration] Transient poll error (${consecutiveErrorsRef.current}/${DEFAULT_MAX_CONSECUTIVE_ERRORS}):`,
119
123
  errorMessage,
120
124
  );
121
125
  }