vargai 0.4.0-alpha67 → 0.4.0-alpha69

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
@@ -61,6 +61,7 @@
61
61
  "fluent-ffmpeg": "^2.1.3",
62
62
  "groq-sdk": "^0.36.0",
63
63
  "ink": "^6.5.1",
64
+ "p-limit": "^6.2.0",
64
65
  "p-map": "^7.0.4",
65
66
  "react": "^19.2.0",
66
67
  "react-dom": "^19.2.0",
@@ -70,7 +71,7 @@
70
71
  "zod": "^4.2.1"
71
72
  },
72
73
  "sideEffects": false,
73
- "version": "0.4.0-alpha67",
74
+ "version": "0.4.0-alpha69",
74
75
  "exports": {
75
76
  ".": "./src/index.ts",
76
77
  "./ai": "./src/ai-sdk/index.ts",
@@ -101,8 +101,12 @@ export {
101
101
  } from "./providers/together";
102
102
  export {
103
103
  falStorage,
104
+ limitedRetryUpload,
104
105
  type R2StorageOptions,
106
+ type RetryOptions,
105
107
  r2Storage,
108
+ r2UploadLimiter,
109
+ retryR2Upload,
106
110
  type StorageProvider,
107
111
  } from "./storage";
108
112
  export type {
@@ -1,3 +1,9 @@
1
1
  export { falStorage } from "./fal";
2
2
  export { type R2StorageOptions, r2Storage } from "./r2";
3
+ export {
4
+ limitedRetryUpload,
5
+ type RetryOptions,
6
+ r2UploadLimiter,
7
+ retryR2Upload,
8
+ } from "./retry";
3
9
  export type { StorageProvider } from "./types";
@@ -1,4 +1,5 @@
1
1
  import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
2
+ import { limitedRetryUpload } from "./retry";
2
3
  import type { StorageProvider } from "./types";
3
4
 
4
5
  export interface R2StorageOptions {
@@ -41,13 +42,15 @@ export function r2Storage(options?: R2StorageOptions): StorageProvider {
41
42
 
42
43
  return {
43
44
  async upload(data: Uint8Array, key: string, mediaType: string) {
44
- await client.send(
45
- new PutObjectCommand({
46
- Bucket: bucket,
47
- Key: key,
48
- Body: data,
49
- ContentType: mediaType,
50
- }),
45
+ await limitedRetryUpload(() =>
46
+ client.send(
47
+ new PutObjectCommand({
48
+ Bucket: bucket,
49
+ Key: key,
50
+ Body: data,
51
+ ContentType: mediaType,
52
+ }),
53
+ ),
51
54
  );
52
55
  return getPublicUrl(key);
53
56
  },
@@ -0,0 +1,372 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import { limitedRetryUpload, r2UploadLimiter, retryR2Upload } from "./retry";
3
+
4
+ // Use minimal delays for tests
5
+ const fastOpts = { maxRetries: 3, baseDelay: 1 };
6
+
7
+ // ─── Error factories ─────────────────────────────────────────────────────────
8
+
9
+ /** Simulates an AWS SDK v3 ServiceException with $metadata */
10
+ function awsError(
11
+ name: string,
12
+ message: string,
13
+ httpStatusCode?: number,
14
+ ): Error & { $metadata?: { httpStatusCode: number } } {
15
+ const err = new Error(message) as Error & {
16
+ $metadata?: { httpStatusCode: number };
17
+ };
18
+ err.name = name;
19
+ if (httpStatusCode !== undefined) {
20
+ err.$metadata = { httpStatusCode };
21
+ }
22
+ return err;
23
+ }
24
+
25
+ /** Error with a `code` property (Node.js-style) */
26
+ function codeError(
27
+ code: string,
28
+ message = "connection failed",
29
+ ): Error & { code: string } {
30
+ const err = new Error(message) as Error & { code: string };
31
+ err.code = code;
32
+ return err;
33
+ }
34
+
35
+ // ─── isRetryable (tested indirectly through retryR2Upload) ───────────────────
36
+
37
+ describe("retryR2Upload", () => {
38
+ describe("retries on retryable errors", () => {
39
+ test("retries on error.message containing 'concurrent request rate'", async () => {
40
+ let calls = 0;
41
+ const result = await retryR2Upload(async () => {
42
+ calls++;
43
+ if (calls === 1)
44
+ throw new Error(
45
+ "Reduce your concurrent request rate for the same object.",
46
+ );
47
+ return "ok";
48
+ }, fastOpts);
49
+
50
+ expect(result).toBe("ok");
51
+ expect(calls).toBe(2);
52
+ });
53
+
54
+ test("retries on error.name = SlowDown (AWS SDK v3 pattern)", async () => {
55
+ let calls = 0;
56
+ const result = await retryR2Upload(async () => {
57
+ calls++;
58
+ if (calls === 1)
59
+ throw awsError("SlowDown", "Please reduce request rate.");
60
+ return "ok";
61
+ }, fastOpts);
62
+
63
+ expect(result).toBe("ok");
64
+ expect(calls).toBe(2);
65
+ });
66
+
67
+ test("retries on error.name = ServiceUnavailable", async () => {
68
+ let calls = 0;
69
+ const result = await retryR2Upload(async () => {
70
+ calls++;
71
+ if (calls === 1)
72
+ throw awsError(
73
+ "ServiceUnavailable",
74
+ "Service is temporarily unavailable",
75
+ );
76
+ return "ok";
77
+ }, fastOpts);
78
+
79
+ expect(result).toBe("ok");
80
+ expect(calls).toBe(2);
81
+ });
82
+
83
+ test("retries on $metadata.httpStatusCode = 429", async () => {
84
+ let calls = 0;
85
+ const result = await retryR2Upload(async () => {
86
+ calls++;
87
+ if (calls === 1) throw awsError("UnknownError", "rate limited", 429);
88
+ return "ok";
89
+ }, fastOpts);
90
+
91
+ expect(result).toBe("ok");
92
+ expect(calls).toBe(2);
93
+ });
94
+
95
+ test("retries on $metadata.httpStatusCode = 500", async () => {
96
+ let calls = 0;
97
+ const result = await retryR2Upload(async () => {
98
+ calls++;
99
+ if (calls === 1) throw awsError("InternalServerError", "oops", 500);
100
+ return "ok";
101
+ }, fastOpts);
102
+
103
+ expect(result).toBe("ok");
104
+ expect(calls).toBe(2);
105
+ });
106
+
107
+ test("retries on $metadata.httpStatusCode = 503", async () => {
108
+ let calls = 0;
109
+ const result = await retryR2Upload(async () => {
110
+ calls++;
111
+ if (calls === 1) throw awsError("Unavailable", "try again", 503);
112
+ return "ok";
113
+ }, fastOpts);
114
+
115
+ expect(result).toBe("ok");
116
+ expect(calls).toBe(2);
117
+ });
118
+
119
+ test("retries on error.code = ECONNRESET", async () => {
120
+ let calls = 0;
121
+ const result = await retryR2Upload(async () => {
122
+ calls++;
123
+ if (calls === 1) throw codeError("ECONNRESET");
124
+ return "ok";
125
+ }, fastOpts);
126
+
127
+ expect(result).toBe("ok");
128
+ expect(calls).toBe(2);
129
+ });
130
+
131
+ test("retries on error.code = ETIMEDOUT", async () => {
132
+ let calls = 0;
133
+ const result = await retryR2Upload(async () => {
134
+ calls++;
135
+ if (calls === 1) throw codeError("ETIMEDOUT");
136
+ return "ok";
137
+ }, fastOpts);
138
+
139
+ expect(result).toBe("ok");
140
+ expect(calls).toBe(2);
141
+ });
142
+
143
+ test("retries on 'socket hang up' in message", async () => {
144
+ let calls = 0;
145
+ const result = await retryR2Upload(async () => {
146
+ calls++;
147
+ if (calls === 1) throw new Error("socket hang up");
148
+ return "ok";
149
+ }, fastOpts);
150
+
151
+ expect(result).toBe("ok");
152
+ expect(calls).toBe(2);
153
+ });
154
+ });
155
+
156
+ describe("does NOT retry on non-retryable errors", () => {
157
+ test("throws immediately on AccessDenied", async () => {
158
+ let calls = 0;
159
+ await expect(
160
+ retryR2Upload(async () => {
161
+ calls++;
162
+ throw awsError("AccessDenied", "Access Denied", 403);
163
+ }, fastOpts),
164
+ ).rejects.toThrow("Access Denied");
165
+
166
+ expect(calls).toBe(1);
167
+ });
168
+
169
+ test("throws immediately on NoSuchBucket", async () => {
170
+ let calls = 0;
171
+ await expect(
172
+ retryR2Upload(async () => {
173
+ calls++;
174
+ throw awsError(
175
+ "NoSuchBucket",
176
+ "The specified bucket does not exist",
177
+ 404,
178
+ );
179
+ }, fastOpts),
180
+ ).rejects.toThrow("The specified bucket does not exist");
181
+
182
+ expect(calls).toBe(1);
183
+ });
184
+
185
+ test("throws immediately on generic Error", async () => {
186
+ let calls = 0;
187
+ await expect(
188
+ retryR2Upload(async () => {
189
+ calls++;
190
+ throw new Error("something completely unrelated");
191
+ }, fastOpts),
192
+ ).rejects.toThrow("something completely unrelated");
193
+
194
+ expect(calls).toBe(1);
195
+ });
196
+
197
+ test("does not retry primitive/null errors", async () => {
198
+ let calls = 0;
199
+ await expect(
200
+ retryR2Upload(async () => {
201
+ calls++;
202
+ throw null;
203
+ }, fastOpts),
204
+ ).rejects.toBeNull();
205
+
206
+ expect(calls).toBe(1);
207
+ });
208
+ });
209
+
210
+ describe("retry limits and exhaustion", () => {
211
+ test("retries up to maxRetries times then throws", async () => {
212
+ let calls = 0;
213
+ await expect(
214
+ retryR2Upload(
215
+ async () => {
216
+ calls++;
217
+ throw new Error("SlowDown");
218
+ },
219
+ { maxRetries: 2, baseDelay: 1 },
220
+ ),
221
+ ).rejects.toThrow("SlowDown");
222
+
223
+ // 1 initial + 2 retries = 3 total calls
224
+ expect(calls).toBe(3);
225
+ });
226
+
227
+ test("maxRetries: 0 means no retries (single attempt)", async () => {
228
+ let calls = 0;
229
+ await expect(
230
+ retryR2Upload(
231
+ async () => {
232
+ calls++;
233
+ throw new Error("SlowDown");
234
+ },
235
+ { maxRetries: 0, baseDelay: 1 },
236
+ ),
237
+ ).rejects.toThrow("SlowDown");
238
+
239
+ expect(calls).toBe(1);
240
+ });
241
+
242
+ test("succeeds on the last retry attempt", async () => {
243
+ let calls = 0;
244
+ const result = await retryR2Upload(
245
+ async () => {
246
+ calls++;
247
+ if (calls <= 3) throw new Error("SlowDown");
248
+ return "finally";
249
+ },
250
+ { maxRetries: 3, baseDelay: 1 },
251
+ );
252
+
253
+ expect(result).toBe("finally");
254
+ expect(calls).toBe(4); // 1 initial + 3 retries
255
+ });
256
+ });
257
+
258
+ describe("returns value on success", () => {
259
+ test("returns value on first attempt", async () => {
260
+ const result = await retryR2Upload(async () => "immediate", fastOpts);
261
+ expect(result).toBe("immediate");
262
+ });
263
+
264
+ test("returns complex objects", async () => {
265
+ const obj = { url: "https://s3.varg.ai/test.mp4", key: "test.mp4" };
266
+ const result = await retryR2Upload(async () => obj, fastOpts);
267
+ expect(result).toEqual(obj);
268
+ });
269
+ });
270
+
271
+ describe("input validation", () => {
272
+ test("rejects negative maxRetries", async () => {
273
+ await expect(
274
+ retryR2Upload(async () => "ok", { maxRetries: -1, baseDelay: 1 }),
275
+ ).rejects.toThrow("maxRetries");
276
+ });
277
+
278
+ test("rejects NaN maxRetries", async () => {
279
+ await expect(
280
+ retryR2Upload(async () => "ok", { maxRetries: NaN, baseDelay: 1 }),
281
+ ).rejects.toThrow("maxRetries");
282
+ });
283
+
284
+ test("rejects float maxRetries", async () => {
285
+ await expect(
286
+ retryR2Upload(async () => "ok", { maxRetries: 2.5, baseDelay: 1 }),
287
+ ).rejects.toThrow("maxRetries");
288
+ });
289
+
290
+ test("rejects negative baseDelay", async () => {
291
+ await expect(
292
+ retryR2Upload(async () => "ok", { maxRetries: 1, baseDelay: -100 }),
293
+ ).rejects.toThrow("baseDelay");
294
+ });
295
+
296
+ test("rejects Infinity baseDelay", async () => {
297
+ await expect(
298
+ retryR2Upload(async () => "ok", {
299
+ maxRetries: 1,
300
+ baseDelay: Number.POSITIVE_INFINITY,
301
+ }),
302
+ ).rejects.toThrow("baseDelay");
303
+ });
304
+
305
+ test("accepts zero baseDelay", async () => {
306
+ const result = await retryR2Upload(async () => "ok", {
307
+ maxRetries: 0,
308
+ baseDelay: 0,
309
+ });
310
+ expect(result).toBe("ok");
311
+ });
312
+ });
313
+ });
314
+
315
+ // ─── limitedRetryUpload ──────────────────────────────────────────────────────
316
+
317
+ describe("limitedRetryUpload", () => {
318
+ test("runs upload through concurrency limiter and retry", async () => {
319
+ const result = await limitedRetryUpload(async () => "uploaded");
320
+ expect(result).toBe("uploaded");
321
+ });
322
+
323
+ test("retries within the limiter on retryable errors", async () => {
324
+ let calls = 0;
325
+ const result = await limitedRetryUpload(
326
+ async () => {
327
+ calls++;
328
+ if (calls === 1) throw new Error("SlowDown");
329
+ return "ok";
330
+ },
331
+ { maxRetries: 2, baseDelay: 1 },
332
+ );
333
+
334
+ expect(result).toBe("ok");
335
+ expect(calls).toBe(2);
336
+ });
337
+
338
+ test("respects concurrency limit", async () => {
339
+ let concurrent = 0;
340
+ let maxConcurrent = 0;
341
+
342
+ const tasks = Array.from({ length: 20 }, () =>
343
+ limitedRetryUpload(
344
+ async () => {
345
+ concurrent++;
346
+ maxConcurrent = Math.max(maxConcurrent, concurrent);
347
+ await new Promise((r) => setTimeout(r, 10));
348
+ concurrent--;
349
+ return "done";
350
+ },
351
+ { maxRetries: 0, baseDelay: 0 },
352
+ ),
353
+ );
354
+
355
+ await Promise.all(tasks);
356
+
357
+ // r2UploadLimiter is set to 10
358
+ expect(maxConcurrent).toBeLessThanOrEqual(10);
359
+ expect(maxConcurrent).toBeGreaterThan(1); // should actually parallelize
360
+ });
361
+ });
362
+
363
+ // ─── r2UploadLimiter ─────────────────────────────────────────────────────────
364
+
365
+ describe("r2UploadLimiter", () => {
366
+ test("is a p-limit instance with concurrency 10", () => {
367
+ // p-limit exposes activeCount and pendingCount
368
+ expect(typeof r2UploadLimiter).toBe("function");
369
+ expect(r2UploadLimiter.activeCount).toBe(0);
370
+ expect(r2UploadLimiter.pendingCount).toBe(0);
371
+ });
372
+ });
@@ -0,0 +1,111 @@
1
+ import pLimit from "p-limit";
2
+
3
+ /**
4
+ * Global concurrency limiter for R2/S3 uploads.
5
+ * Caps concurrent uploads to avoid Cloudflare R2 rate limits
6
+ * ("Reduce your concurrent request rate for the same object").
7
+ */
8
+ export const r2UploadLimiter = pLimit(10);
9
+
10
+ /** Errors that indicate R2/S3 rate limiting or transient failures worth retrying */
11
+ const RETRYABLE_PATTERNS = [
12
+ "concurrent request rate",
13
+ "SlowDown",
14
+ "ServiceUnavailable",
15
+ "TooManyRequests",
16
+ "RequestTimeout",
17
+ "InternalError",
18
+ "ECONNRESET",
19
+ "ETIMEDOUT",
20
+ "socket hang up",
21
+ ];
22
+
23
+ /** HTTP status codes that indicate a retryable error */
24
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 503]);
25
+
26
+ function isRetryable(error: unknown): boolean {
27
+ if (typeof error !== "object" || error === null) {
28
+ return false;
29
+ }
30
+
31
+ const err = error as {
32
+ message?: string;
33
+ name?: string;
34
+ code?: string;
35
+ $metadata?: { httpStatusCode?: number };
36
+ };
37
+
38
+ // Check AWS SDK v3 $metadata.httpStatusCode
39
+ const statusCode = err.$metadata?.httpStatusCode;
40
+ if (statusCode !== undefined && RETRYABLE_STATUS_CODES.has(statusCode)) {
41
+ return true;
42
+ }
43
+
44
+ // Check error.name, error.code, and error.message against known patterns
45
+ const haystack = [err.message, err.name, err.code].filter(Boolean).join(" ");
46
+ return RETRYABLE_PATTERNS.some((pattern) => haystack.includes(pattern));
47
+ }
48
+
49
+ export interface RetryOptions {
50
+ /** Maximum number of retries after the initial attempt (default: 5) */
51
+ maxRetries?: number;
52
+ /** Initial backoff delay in ms (default: 500) */
53
+ baseDelay?: number;
54
+ }
55
+
56
+ /**
57
+ * Retry an R2/S3 upload with exponential backoff + jitter.
58
+ * Only retries on known transient/rate-limit errors.
59
+ *
60
+ * Backoff schedule (default): 500ms, 1s, 2s, 4s, 8s (+ random jitter up to 30%)
61
+ */
62
+ export async function retryR2Upload<T>(
63
+ fn: () => Promise<T>,
64
+ opts?: RetryOptions,
65
+ ): Promise<T> {
66
+ const maxRetries = opts?.maxRetries ?? 5;
67
+ const baseDelay = opts?.baseDelay ?? 500;
68
+
69
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
70
+ throw new Error("retry option `maxRetries` must be a non-negative integer");
71
+ }
72
+ if (!Number.isFinite(baseDelay) || baseDelay < 0) {
73
+ throw new Error("retry option `baseDelay` must be a non-negative number");
74
+ }
75
+
76
+ let lastError: unknown;
77
+
78
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
79
+ try {
80
+ return await fn();
81
+ } catch (error) {
82
+ lastError = error;
83
+
84
+ if (attempt >= maxRetries || !isRetryable(error)) {
85
+ throw error;
86
+ }
87
+
88
+ const delay = baseDelay * 2 ** attempt;
89
+ const jitter = delay * 0.3 * Math.random();
90
+ const totalDelay = Math.round(delay + jitter);
91
+
92
+ console.warn(
93
+ `[r2-upload] attempt ${attempt + 1}/${maxRetries + 1} failed, retrying in ${totalDelay}ms: ${error instanceof Error ? error.message : String(error)}`,
94
+ );
95
+
96
+ await new Promise((resolve) => setTimeout(resolve, totalDelay));
97
+ }
98
+ }
99
+
100
+ throw lastError;
101
+ }
102
+
103
+ /**
104
+ * Convenience: run an R2 upload through both the concurrency limiter and retry logic.
105
+ */
106
+ export function limitedRetryUpload<T>(
107
+ fn: () => Promise<T>,
108
+ opts?: RetryOptions,
109
+ ): Promise<T> {
110
+ return r2UploadLimiter(() => retryR2Upload(fn, opts));
111
+ }
@@ -8,6 +8,7 @@ import {
8
8
  S3Client,
9
9
  } from "@aws-sdk/client-s3";
10
10
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
11
+ import { limitedRetryUpload } from "../ai-sdk/storage/retry";
11
12
  import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types";
12
13
  import { BaseProvider } from "./base";
13
14
 
@@ -73,12 +74,14 @@ export class StorageProvider extends BaseProvider {
73
74
  const file = Bun.file(filePath);
74
75
  const buffer = await file.arrayBuffer();
75
76
 
76
- await this.client.send(
77
- new PutObjectCommand({
78
- Bucket: this.bucket,
79
- Key: objectKey,
80
- Body: new Uint8Array(buffer),
81
- }),
77
+ await limitedRetryUpload(() =>
78
+ this.client.send(
79
+ new PutObjectCommand({
80
+ Bucket: this.bucket,
81
+ Key: objectKey,
82
+ Body: new Uint8Array(buffer),
83
+ }),
84
+ ),
82
85
  );
83
86
 
84
87
  return this.getPublicUrl(objectKey);
@@ -93,12 +96,14 @@ export class StorageProvider extends BaseProvider {
93
96
  const response = await fetch(url);
94
97
  const buffer = await response.arrayBuffer();
95
98
 
96
- await this.client.send(
97
- new PutObjectCommand({
98
- Bucket: this.bucket,
99
- Key: objectKey,
100
- Body: new Uint8Array(buffer),
101
- }),
99
+ await limitedRetryUpload(() =>
100
+ this.client.send(
101
+ new PutObjectCommand({
102
+ Bucket: this.bucket,
103
+ Key: objectKey,
104
+ Body: new Uint8Array(buffer),
105
+ }),
106
+ ),
102
107
  );
103
108
 
104
109
  return this.getPublicUrl(objectKey);
@@ -114,13 +119,15 @@ export class StorageProvider extends BaseProvider {
114
119
  ): Promise<string> {
115
120
  console.log(`[storage] uploading buffer to ${objectKey}`);
116
121
 
117
- await this.client.send(
118
- new PutObjectCommand({
119
- Bucket: this.bucket,
120
- Key: objectKey,
121
- Body: buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer),
122
- ContentType: contentType,
123
- }),
122
+ await limitedRetryUpload(() =>
123
+ this.client.send(
124
+ new PutObjectCommand({
125
+ Bucket: this.bucket,
126
+ Key: objectKey,
127
+ Body: buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer),
128
+ ContentType: contentType,
129
+ }),
130
+ ),
124
131
  );
125
132
 
126
133
  return this.getPublicUrl(objectKey);
@@ -22,7 +22,20 @@ async function resolveImageInput(
22
22
  return new Uint8Array(await response.arrayBuffer());
23
23
  }
24
24
  const file = await renderImage(input, ctx);
25
- return file.arrayBuffer();
25
+ const data = await file.arrayBuffer();
26
+ // Debug: ensure we always return a proper Uint8Array
27
+ if (!(data instanceof Uint8Array)) {
28
+ console.error(
29
+ `[resolveImageInput] file.arrayBuffer() returned ${typeof data} (constructor: ${data?.constructor?.name}), converting...`,
30
+ );
31
+ if (data instanceof ArrayBuffer) {
32
+ return new Uint8Array(data);
33
+ }
34
+ throw new Error(
35
+ `resolveImageInput: unexpected data type ${typeof data} from file.arrayBuffer()`,
36
+ );
37
+ }
38
+ return data;
26
39
  }
27
40
 
28
41
  async function resolvePrompt(
@@ -35,6 +48,15 @@ async function resolvePrompt(
35
48
  const resolvedImages = await Promise.all(
36
49
  prompt.images.map((img) => resolveImageInput(img, ctx)),
37
50
  );
51
+ // Debug: verify all images are Uint8Array before passing to generateImage
52
+ for (let i = 0; i < resolvedImages.length; i++) {
53
+ const img = resolvedImages[i];
54
+ if (!(img instanceof Uint8Array)) {
55
+ console.error(
56
+ `[resolvePrompt] images[${i}] is ${typeof img} (constructor: ${(img as any)?.constructor?.name}), not Uint8Array`,
57
+ );
58
+ }
59
+ }
38
60
  return { text: prompt.text, images: resolvedImages };
39
61
  }
40
62
 
@@ -290,9 +290,7 @@ export async function renderRoot(
290
290
  }
291
291
 
292
292
  const concurrency =
293
- options.concurrency === undefined
294
- ? Number.POSITIVE_INFINITY
295
- : options.concurrency;
293
+ options.concurrency === undefined ? 3 : options.concurrency;
296
294
 
297
295
  if (
298
296
  concurrency !== Number.POSITIVE_INFINITY &&
@@ -285,7 +285,7 @@ export interface RenderOptions {
285
285
  defaults?: DefaultModels;
286
286
  backend?: FFmpegBackend;
287
287
  storage?: StorageProvider;
288
- /** Max concurrent clip renders. Defaults to unlimited. */
288
+ /** Max concurrent clip renders. Defaults to 3. */
289
289
  concurrency?: number;
290
290
  }
291
291