@yetter/client 0.0.12 → 0.0.14

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/dist/client.js CHANGED
@@ -1,37 +1,127 @@
1
1
  var _a;
2
2
  import { YetterImageClient } from "./api.js";
3
- import { EventSourcePolyfill } from 'event-source-polyfill';
4
- export class yetter {
5
- static configure(options) {
6
- if (options.apiKey) {
7
- if (options.is_bearer) {
8
- _a.apiKey = 'Bearer ' + options.apiKey;
9
- }
10
- else {
11
- _a.apiKey = 'Key ' + options.apiKey;
3
+ import { EventSourcePolyfill } from "event-source-polyfill";
4
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
5
+ const DEFAULT_UPLOAD_TIMEOUT_MS = 5 * 60 * 1000;
6
+ const STREAM_HEARTBEAT_TIMEOUT_MS = 120000;
7
+ const DEFAULT_STREAM_SSE_MAX_CONSECUTIVE_ERRORS = 3;
8
+ const DEFAULT_ENDPOINT = "https://api.yetter.ai";
9
+ function sleep(ms) {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
12
+ function getEnvApiKey() {
13
+ return process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "";
14
+ }
15
+ export class YetterClient {
16
+ constructor(options = {}) {
17
+ this.queue = {
18
+ submit: async (model, options) => {
19
+ const client = this.createApiClient();
20
+ return client.generate({
21
+ model,
22
+ ...options.input,
23
+ });
24
+ },
25
+ status: async (model, options) => {
26
+ const client = this.createApiClient();
27
+ const endpoint = client.getApiEndpoint();
28
+ const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
29
+ const statusData = await client.getStatus({ url: statusUrl });
30
+ return {
31
+ data: statusData,
32
+ requestId: options.requestId,
33
+ };
34
+ },
35
+ result: async (model, options) => {
36
+ const client = this.createApiClient();
37
+ const endpoint = client.getApiEndpoint();
38
+ const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
39
+ const responseData = await client.getResponse({ url: responseUrl });
40
+ return {
41
+ data: responseData,
42
+ requestId: options.requestId,
43
+ };
44
+ },
45
+ };
46
+ this.apiKey = "";
47
+ this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
48
+ const envKey = getEnvApiKey();
49
+ if (envKey) {
50
+ this.apiKey = `Key ${envKey}`;
51
+ }
52
+ if (typeof options.apiKey === "string") {
53
+ this.setApiKey(options.apiKey, options.is_bearer);
54
+ }
55
+ }
56
+ setApiKey(rawApiKey, isBearer) {
57
+ const normalizedApiKey = rawApiKey.trim();
58
+ if (!normalizedApiKey) {
59
+ this.apiKey = "";
60
+ return;
61
+ }
62
+ this.apiKey = isBearer ? `Bearer ${normalizedApiKey}` : `Key ${normalizedApiKey}`;
63
+ }
64
+ assertApiKeyConfigured() {
65
+ if (!this.apiKey) {
66
+ throw new Error("API key is not configured. Call configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
67
+ }
68
+ }
69
+ createApiClient() {
70
+ this.assertApiKeyConfigured();
71
+ return new YetterImageClient({
72
+ apiKey: this.apiKey,
73
+ endpoint: this.endpoint,
74
+ });
75
+ }
76
+ getUploadTimeoutMs(options) {
77
+ if (typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0) {
78
+ return options.timeout;
79
+ }
80
+ return DEFAULT_UPLOAD_TIMEOUT_MS;
81
+ }
82
+ async putWithTimeout(url, body, headers, timeoutMs) {
83
+ const controller = new AbortController();
84
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
85
+ try {
86
+ return await fetch(url, {
87
+ method: "PUT",
88
+ headers,
89
+ body,
90
+ signal: controller.signal,
91
+ });
92
+ }
93
+ catch (error) {
94
+ if ((error === null || error === void 0 ? void 0 : error.name) === "AbortError") {
95
+ throw new Error(`Upload request timed out after ${timeoutMs}ms`);
12
96
  }
97
+ throw error;
98
+ }
99
+ finally {
100
+ clearTimeout(timeout);
101
+ }
102
+ }
103
+ configure(options) {
104
+ if (typeof options.apiKey === "string") {
105
+ this.setApiKey(options.apiKey, options.is_bearer);
13
106
  }
14
107
  if (options.endpoint) {
15
- _a.endpoint = options.endpoint;
108
+ this.endpoint = options.endpoint;
16
109
  }
17
110
  }
18
- static async subscribe(model, options) {
111
+ async subscribe(model, options) {
19
112
  var _b;
20
- if (!_a.apiKey) {
21
- throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
22
- }
23
- const client = new YetterImageClient({
24
- apiKey: _a.apiKey,
25
- endpoint: _a.endpoint
26
- });
113
+ const client = this.createApiClient();
27
114
  const generateResponse = await client.generate({
28
- model: model,
115
+ model,
29
116
  ...options.input,
30
117
  });
31
118
  let status = generateResponse.status;
32
119
  let lastStatusResponse;
33
120
  const startTime = Date.now();
34
- const timeoutMilliseconds = 30 * 60 * 1000; // 30 minutes
121
+ const timeoutMilliseconds = 30 * 60 * 1000;
122
+ const pollIntervalMs = typeof options.pollIntervalMs === "number" && Number.isFinite(options.pollIntervalMs) && options.pollIntervalMs > 0
123
+ ? options.pollIntervalMs
124
+ : DEFAULT_POLL_INTERVAL_MS;
35
125
  while (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
36
126
  if (Date.now() - startTime > timeoutMilliseconds) {
37
127
  console.warn(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}. Attempting to cancel.`);
@@ -53,6 +143,9 @@ export class yetter {
53
143
  if (options.onQueueUpdate) {
54
144
  options.onQueueUpdate(lastStatusResponse);
55
145
  }
146
+ if (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
147
+ await sleep(pollIntervalMs);
148
+ }
56
149
  }
57
150
  catch (error) {
58
151
  console.error("Error during status polling:", error);
@@ -60,35 +153,36 @@ export class yetter {
60
153
  }
61
154
  }
62
155
  if (status === "ERROR") {
63
- const errorMessage = ((_b = lastStatusResponse === null || lastStatusResponse === void 0 ? void 0 : lastStatusResponse.logs) === null || _b === void 0 ? void 0 : _b.map(log => log.message).join("\n")) || "Image generation failed.";
156
+ const errorMessage = ((_b = lastStatusResponse === null || lastStatusResponse === void 0 ? void 0 : lastStatusResponse.logs) === null || _b === void 0 ? void 0 : _b.map((log) => log.message).join("\n")) || "Image generation failed.";
64
157
  throw new Error(errorMessage);
65
158
  }
66
- else if (status === "CANCELLED") {
159
+ if (status === "CANCELLED") {
67
160
  throw new Error("Image generation was cancelled by user.");
68
161
  }
69
- const finalResponse = await client.getResponse({
162
+ return client.getResponse({
70
163
  url: generateResponse.response_url,
71
164
  });
72
- return finalResponse;
73
165
  }
74
- static async stream(model, options) {
75
- if (!_a.apiKey) {
76
- throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
77
- }
78
- const client = new YetterImageClient({
79
- apiKey: _a.apiKey,
80
- endpoint: _a.endpoint
81
- });
166
+ async stream(model, options) {
167
+ const client = this.createApiClient();
82
168
  const initialApiResponse = await client.generate({
83
- model: model,
169
+ model,
84
170
  ...options.input,
85
171
  });
86
172
  const requestId = initialApiResponse.request_id;
87
173
  const responseUrl = initialApiResponse.response_url;
174
+ const statusUrl = initialApiResponse.status_url;
88
175
  const cancelUrl = initialApiResponse.cancel_url;
89
176
  const sseStreamUrl = `${client.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
177
+ const streamPollIntervalMs = typeof options.pollIntervalMs === "number" && Number.isFinite(options.pollIntervalMs) && options.pollIntervalMs > 0
178
+ ? options.pollIntervalMs
179
+ : DEFAULT_POLL_INTERVAL_MS;
180
+ const maxConsecutiveSseErrors = typeof options.sseMaxConsecutiveErrors === "number" &&
181
+ Number.isFinite(options.sseMaxConsecutiveErrors) &&
182
+ options.sseMaxConsecutiveErrors > 0
183
+ ? Math.floor(options.sseMaxConsecutiveErrors)
184
+ : DEFAULT_STREAM_SSE_MAX_CONSECUTIVE_ERRORS;
90
185
  let eventSource;
91
- // Setup the promise for the done() method
92
186
  let resolveDonePromise;
93
187
  let rejectDonePromise;
94
188
  const donePromise = new Promise((resolve, reject) => {
@@ -96,8 +190,6 @@ export class yetter {
96
190
  rejectDonePromise = reject;
97
191
  });
98
192
  const controller = {
99
- // This will be used by the async iterator to pull events
100
- // It needs a way to buffer events or signal availability
101
193
  events: [],
102
194
  resolvers: [],
103
195
  isClosed: false,
@@ -124,7 +216,6 @@ export class yetter {
124
216
  else {
125
217
  this.events.push(event);
126
218
  }
127
- // Check for terminal events to resolve/reject the donePromise
128
219
  this.currentStatus = event.status;
129
220
  },
130
221
  error(err) {
@@ -159,7 +250,7 @@ export class yetter {
159
250
  if (this.isClosed)
160
251
  return;
161
252
  this.isClosed = true;
162
- this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
253
+ this.resolvers.forEach((resolve) => resolve({ value: undefined, done: true }));
163
254
  this.resolvers = [];
164
255
  eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
165
256
  },
@@ -170,73 +261,158 @@ export class yetter {
170
261
  if (this.isClosed) {
171
262
  return Promise.resolve({ value: undefined, done: true });
172
263
  }
173
- return new Promise(resolve => this.resolvers.push(resolve));
174
- }
175
- };
176
- eventSource = new EventSourcePolyfill(sseStreamUrl, {
177
- headers: { 'Authorization': `${_a.apiKey}` },
178
- });
179
- eventSource.onopen = (event) => {
180
- console.log("SSE Connection Opened:", event);
264
+ return new Promise((resolve) => this.resolvers.push(resolve));
265
+ },
181
266
  };
182
- eventSource.addEventListener('data', (event) => {
183
- // console.log("SSE 'data' event received, raw data:", event.data);
184
- try {
185
- const statusData = JSON.parse(event.data);
186
- controller.push(statusData);
187
- }
188
- catch (e) {
189
- console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
190
- controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
267
+ let hasStartedFallbackPolling = false;
268
+ const terminalStatuses = new Set(["COMPLETED", "ERROR", "CANCELLED"]);
269
+ const startPollingFallback = async (reason) => {
270
+ if (controller.isClosed || hasStartedFallbackPolling) {
271
+ return;
191
272
  }
192
- });
193
- // when 'done' event is received, currentStatus can only be COMPLETED or CANCELLED
194
- // TODO: remove currentStatus and branch two cases(COMPLETED and CANCELLED) by response status by responseUrl
195
- // TODO: Determine whether raise error or not when response status is CANCELLED
196
- // => current code raise error when response status is CANCELLED because resolveDonePromise only get completed response
197
- // => this mean that user expect only completed response from .done() method
198
- eventSource.addEventListener('done', async (event) => {
199
- // console.log("SSE 'done' event received, raw data:", event.data);
273
+ hasStartedFallbackPolling = true;
200
274
  try {
201
- if (controller.currentStatus === "COMPLETED") {
202
- // Close SSE immediately to avoid any late 'error' events during await
203
- try {
204
- eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
275
+ console.warn(`Falling back to status polling for ${requestId}: ${reason}`);
276
+ while (!controller.isClosed) {
277
+ const latestStatus = await client.getStatus({ url: statusUrl });
278
+ controller.push(latestStatus);
279
+ if (terminalStatuses.has(latestStatus.status)) {
280
+ if (latestStatus.status === "COMPLETED") {
281
+ const response = await client.getResponse({ url: responseUrl });
282
+ controller.done(response);
283
+ }
284
+ else if (latestStatus.status === "CANCELLED") {
285
+ controller.cancel();
286
+ }
287
+ else {
288
+ controller.error(new Error(`Stream reported ERROR for ${requestId}`));
289
+ }
290
+ return;
205
291
  }
206
- catch { }
207
- const response = await client.getResponse({ url: responseUrl });
208
- controller.done(response);
209
- }
210
- else if (controller.currentStatus === "CANCELLED") {
211
- controller.cancel();
292
+ await sleep(streamPollIntervalMs);
212
293
  }
213
294
  }
214
- catch (e) {
215
- console.error("Error parsing SSE 'done' event:", e, "Raw data:", event.data);
216
- controller.error(new Error(`Error parsing SSE 'done' event: ${e.message}`));
217
- }
218
- });
219
- eventSource.addEventListener('error', (event) => {
220
- // console.log("SSE 'done' event received, raw data:", event.data);
221
- console.log("SSE 'error' event received, raw data:", event.data);
222
- controller.error(new Error("Stream reported ERROR for ${requestId}"));
223
- });
224
- eventSource.onerror = (err) => {
225
- var _b;
226
- const message = ((_b = err === null || err === void 0 ? void 0 : err.error) === null || _b === void 0 ? void 0 : _b.message) || (err === null || err === void 0 ? void 0 : err.message) || '';
227
- const isIdleTimeout = typeof message === 'string' && message.includes('No activity within') && message.includes('Reconnecting');
228
- if (isIdleTimeout) {
229
- console.warn("SSE idle timeout; letting EventSource auto-reconnect.", err);
230
- return;
295
+ catch (error) {
296
+ controller.error(error instanceof Error
297
+ ? error
298
+ : new Error(`Fallback polling failed for ${requestId}: ${String((error === null || error === void 0 ? void 0 : error.message) || error)}`));
231
299
  }
232
- console.warn("SSE Connection Error (onerror) - will allow auto-reconnect:", err);
233
300
  };
234
- // Handle if API immediately returns a terminal status in initialApiResponse (e.g. already completed/failed)
235
301
  if (initialApiResponse.status === "COMPLETED") {
236
302
  controller.push(initialApiResponse);
303
+ const finalResponse = await client.getResponse({ url: responseUrl });
304
+ controller.done(finalResponse);
237
305
  }
238
306
  else if (initialApiResponse.status === "ERROR") {
239
307
  controller.push(initialApiResponse);
308
+ controller.error(new Error(`Stream reported ERROR for ${requestId}`));
309
+ }
310
+ else if (initialApiResponse.status === "CANCELLED") {
311
+ controller.push(initialApiResponse);
312
+ controller.cancel();
313
+ }
314
+ else {
315
+ if (options.disableSse) {
316
+ void startPollingFallback("SSE disabled by stream options.");
317
+ }
318
+ else {
319
+ let consecutiveSseTransportErrors = 0;
320
+ try {
321
+ eventSource = new EventSourcePolyfill(sseStreamUrl, {
322
+ headers: { Authorization: this.apiKey },
323
+ heartbeatTimeout: STREAM_HEARTBEAT_TIMEOUT_MS,
324
+ });
325
+ }
326
+ catch (error) {
327
+ console.warn("Failed to initialize SSE transport; switching to polling.", error);
328
+ void startPollingFallback(`SSE initialization failed: ${String((error === null || error === void 0 ? void 0 : error.message) || error)}`);
329
+ }
330
+ if (eventSource) {
331
+ eventSource.onopen = (event) => {
332
+ console.log("SSE Connection Opened:", event);
333
+ };
334
+ eventSource.addEventListener("data", (event) => {
335
+ try {
336
+ consecutiveSseTransportErrors = 0;
337
+ const statusData = JSON.parse(event.data);
338
+ controller.push(statusData);
339
+ }
340
+ catch (e) {
341
+ console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
342
+ controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
343
+ }
344
+ });
345
+ eventSource.addEventListener("done", async (event) => {
346
+ try {
347
+ try {
348
+ eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
349
+ }
350
+ catch { }
351
+ if (controller.currentStatus === "COMPLETED") {
352
+ const response = await client.getResponse({ url: responseUrl });
353
+ controller.done(response);
354
+ return;
355
+ }
356
+ if (controller.currentStatus === "CANCELLED") {
357
+ controller.cancel();
358
+ return;
359
+ }
360
+ if (controller.currentStatus === "ERROR") {
361
+ controller.error(new Error(`Stream reported ERROR for ${requestId}`));
362
+ return;
363
+ }
364
+ const latestStatus = await client.getStatus({ url: statusUrl });
365
+ controller.push(latestStatus);
366
+ if (latestStatus.status === "COMPLETED") {
367
+ const response = await client.getResponse({ url: responseUrl });
368
+ controller.done(response);
369
+ }
370
+ else if (latestStatus.status === "CANCELLED") {
371
+ controller.cancel();
372
+ }
373
+ else if (latestStatus.status === "ERROR") {
374
+ controller.error(new Error(`Stream reported ERROR for ${requestId}`));
375
+ }
376
+ else {
377
+ void startPollingFallback("SSE stream ended without terminal status.");
378
+ }
379
+ }
380
+ catch (e) {
381
+ console.error("Error parsing SSE 'done' event:", e, "Raw data:", event.data);
382
+ controller.error(new Error(`Error parsing SSE 'done' event: ${e.message}`));
383
+ }
384
+ });
385
+ eventSource.addEventListener("error", (event) => {
386
+ const rawData = event === null || event === void 0 ? void 0 : event.data;
387
+ if (!rawData) {
388
+ console.warn("SSE transport error event; waiting for reconnect.");
389
+ return;
390
+ }
391
+ console.log("SSE application 'error' event received, raw data:", rawData);
392
+ controller.error(new Error(`Stream reported ERROR for ${requestId}: ${rawData}`));
393
+ });
394
+ eventSource.onerror = (err) => {
395
+ var _b;
396
+ const message = ((_b = err === null || err === void 0 ? void 0 : err.error) === null || _b === void 0 ? void 0 : _b.message) || (err === null || err === void 0 ? void 0 : err.message) || "";
397
+ const isIdleTimeout = typeof message === "string" &&
398
+ message.includes("No activity within") &&
399
+ message.includes("Reconnecting");
400
+ if (isIdleTimeout) {
401
+ console.warn("SSE idle timeout; letting EventSource auto-reconnect.", err);
402
+ return;
403
+ }
404
+ consecutiveSseTransportErrors += 1;
405
+ console.warn("SSE connection issue (onerror); waiting for auto-reconnect.", err);
406
+ if (consecutiveSseTransportErrors >= maxConsecutiveSseErrors) {
407
+ try {
408
+ eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
409
+ }
410
+ catch { }
411
+ void startPollingFallback(`SSE transport failed ${consecutiveSseTransportErrors} times.`);
412
+ }
413
+ };
414
+ }
415
+ }
240
416
  }
241
417
  return {
242
418
  async *[Symbol.asyncIterator]() {
@@ -260,56 +436,19 @@ export class yetter {
260
436
  getRequestId: () => requestId,
261
437
  };
262
438
  }
263
- /**
264
- * Upload a file from the filesystem (Node.js) or File/Blob object (browser)
265
- *
266
- * @param fileOrPath File path (Node.js) or File/Blob object (browser)
267
- * @param options Upload configuration options
268
- * @returns Promise resolving to upload result with public URL
269
- *
270
- * @example
271
- * ```typescript
272
- * // Node.js
273
- * const result = await yetter.uploadFile("/path/to/image.jpg", {
274
- * onProgress: (pct) => console.log(`Upload: ${pct}%`)
275
- * });
276
- *
277
- * // Browser
278
- * const fileInput = document.querySelector('input[type="file"]');
279
- * const file = fileInput.files[0];
280
- * const result = await yetter.uploadFile(file, {
281
- * onProgress: (pct) => updateProgressBar(pct)
282
- * });
283
- * ```
284
- */
285
- static async uploadFile(fileOrPath, options = {}) {
286
- if (!_a.apiKey) {
287
- throw new Error("API key is not configured. Call yetter.configure()");
288
- }
289
- if (typeof fileOrPath === 'string') {
290
- // Node.js path
291
- return _a._uploadFromPath(fileOrPath, options);
292
- }
293
- else {
294
- // Browser File/Blob
295
- return _a._uploadFromBlob(fileOrPath, options);
439
+ async uploadFile(fileOrPath, options = {}) {
440
+ this.assertApiKeyConfigured();
441
+ if (typeof fileOrPath === "string") {
442
+ return this._uploadFromPath(fileOrPath, options);
296
443
  }
444
+ return this._uploadFromBlob(fileOrPath, options);
297
445
  }
298
- /**
299
- * Upload a file from browser (File or Blob object)
300
- * This is an alias for uploadFile for better clarity in browser contexts
301
- */
302
- static async uploadBlob(file, options = {}) {
303
- return _a.uploadFile(file, options);
446
+ async uploadBlob(file, options = {}) {
447
+ return this.uploadFile(file, options);
304
448
  }
305
- /**
306
- * Upload file from filesystem path (Node.js only)
307
- */
308
- static async _uploadFromPath(filePath, options) {
309
- // Dynamic import for Node.js modules
310
- const fs = await import('fs');
311
- const mime = await import('mime-types');
312
- // Validate file exists
449
+ async _uploadFromPath(filePath, options) {
450
+ const fs = await import("fs");
451
+ const mime = await import("mime-types");
313
452
  if (!fs.existsSync(filePath)) {
314
453
  throw new Error(`File not found: ${filePath}`);
315
454
  }
@@ -317,24 +456,19 @@ export class yetter {
317
456
  const fileSize = stats.size;
318
457
  const fileName = filePath.split(/[\\/]/).pop() || "upload";
319
458
  const mimeType = mime.default.lookup(filePath) || "application/octet-stream";
320
- const client = new YetterImageClient({
321
- apiKey: _a.apiKey,
322
- endpoint: _a.endpoint,
323
- });
324
- // Step 1: Request presigned URL(s)
459
+ const uploadTimeoutMs = this.getUploadTimeoutMs(options);
460
+ const client = this.createApiClient();
325
461
  const uploadUrlResponse = await client.getUploadUrl({
326
462
  file_name: fileName,
327
463
  content_type: mimeType,
328
464
  size: fileSize,
329
465
  });
330
- // Step 2: Upload file content
331
466
  if (uploadUrlResponse.mode === "single") {
332
- await _a._uploadFileSingle(filePath, uploadUrlResponse.put_url, mimeType, fileSize, options, fs);
467
+ await this._uploadFileSingle(filePath, uploadUrlResponse.put_url, mimeType, fileSize, options, fs, uploadTimeoutMs);
333
468
  }
334
469
  else {
335
- await _a._uploadFileMultipart(filePath, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, fs);
470
+ await this._uploadFileMultipart(filePath, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, fs, uploadTimeoutMs);
336
471
  }
337
- // Step 3: Notify completion
338
472
  const result = await client.uploadComplete({
339
473
  key: uploadUrlResponse.key,
340
474
  });
@@ -343,32 +477,23 @@ export class yetter {
343
477
  }
344
478
  return result;
345
479
  }
346
- /**
347
- * Upload file from Blob/File object (browser)
348
- */
349
- static async _uploadFromBlob(file, options) {
480
+ async _uploadFromBlob(file, options) {
350
481
  const fileSize = file.size;
351
- const fileName = options.filename ||
352
- (file instanceof File ? file.name : "blob-upload");
482
+ const fileName = options.filename || (file instanceof File ? file.name : "blob-upload");
353
483
  const mimeType = file.type || "application/octet-stream";
354
- const client = new YetterImageClient({
355
- apiKey: _a.apiKey,
356
- endpoint: _a.endpoint,
357
- });
358
- // Step 1: Request presigned URL(s)
484
+ const uploadTimeoutMs = this.getUploadTimeoutMs(options);
485
+ const client = this.createApiClient();
359
486
  const uploadUrlResponse = await client.getUploadUrl({
360
487
  file_name: fileName,
361
488
  content_type: mimeType,
362
489
  size: fileSize,
363
490
  });
364
- // Step 2: Upload file content
365
491
  if (uploadUrlResponse.mode === "single") {
366
- await _a._uploadBlobSingle(file, uploadUrlResponse.put_url, mimeType, fileSize, options);
492
+ await this._uploadBlobSingle(file, uploadUrlResponse.put_url, mimeType, fileSize, options, uploadTimeoutMs);
367
493
  }
368
494
  else {
369
- await _a._uploadBlobMultipart(file, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options);
495
+ await this._uploadBlobMultipart(file, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, uploadTimeoutMs);
370
496
  }
371
- // Step 3: Notify completion
372
497
  const result = await client.uploadComplete({
373
498
  key: uploadUrlResponse.key,
374
499
  });
@@ -377,19 +502,12 @@ export class yetter {
377
502
  }
378
503
  return result;
379
504
  }
380
- /**
381
- * Upload file using single PUT request (Node.js, private helper)
382
- */
383
- static async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs) {
505
+ async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs, timeoutMs) {
384
506
  const fileBuffer = fs.readFileSync(filePath);
385
- const response = await fetch(presignedUrl, {
386
- method: "PUT",
387
- headers: {
388
- "Content-Type": contentType,
389
- "Content-Length": String(totalSize),
390
- },
391
- body: fileBuffer,
392
- });
507
+ const response = await this.putWithTimeout(presignedUrl, fileBuffer, {
508
+ "Content-Type": contentType,
509
+ "Content-Length": String(totalSize),
510
+ }, timeoutMs);
393
511
  if (!response.ok) {
394
512
  const errorText = await response.text();
395
513
  throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
@@ -398,10 +516,7 @@ export class yetter {
398
516
  options.onProgress(90);
399
517
  }
400
518
  }
401
- /**
402
- * Upload file using multipart upload (Node.js, private helper)
403
- */
404
- static async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs) {
519
+ async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs, timeoutMs) {
405
520
  const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
406
521
  const fileHandle = fs.openSync(filePath, "r");
407
522
  try {
@@ -414,17 +529,12 @@ export class yetter {
414
529
  if (chunk.length === 0) {
415
530
  break;
416
531
  }
417
- const response = await fetch(part.url, {
418
- method: "PUT",
419
- headers: {
420
- "Content-Length": String(chunk.length),
421
- },
422
- body: chunk,
423
- });
532
+ const response = await this.putWithTimeout(part.url, chunk, {
533
+ "Content-Length": String(chunk.length),
534
+ }, timeoutMs);
424
535
  if (!response.ok) {
425
536
  const errorText = await response.text();
426
- throw new Error(`Multipart upload failed at part ${part.part_number} ` +
427
- `(${response.status}): ${errorText}`);
537
+ throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
428
538
  }
429
539
  if (options.onProgress) {
430
540
  const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
@@ -436,18 +546,10 @@ export class yetter {
436
546
  fs.closeSync(fileHandle);
437
547
  }
438
548
  }
439
- /**
440
- * Upload blob using single PUT request (browser, private helper)
441
- */
442
- static async _uploadBlobSingle(blob, presignedUrl, contentType, totalSize, options) {
443
- const response = await fetch(presignedUrl, {
444
- method: "PUT",
445
- headers: {
446
- "Content-Type": contentType,
447
- "Content-Length": String(totalSize),
448
- },
449
- body: blob,
450
- });
549
+ async _uploadBlobSingle(blob, presignedUrl, contentType, _totalSize, options, timeoutMs) {
550
+ const response = await this.putWithTimeout(presignedUrl, blob, {
551
+ "Content-Type": contentType,
552
+ }, timeoutMs);
451
553
  if (!response.ok) {
452
554
  const errorText = await response.text();
453
555
  throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
@@ -456,10 +558,7 @@ export class yetter {
456
558
  options.onProgress(90);
457
559
  }
458
560
  }
459
- /**
460
- * Upload blob using multipart upload (browser, private helper)
461
- */
462
- static async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options) {
561
+ async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options, timeoutMs) {
463
562
  const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
464
563
  for (let i = 0; i < sortedParts.length; i++) {
465
564
  const part = sortedParts[i];
@@ -469,17 +568,10 @@ export class yetter {
469
568
  if (chunk.size === 0) {
470
569
  break;
471
570
  }
472
- const response = await fetch(part.url, {
473
- method: "PUT",
474
- headers: {
475
- "Content-Length": String(chunk.size),
476
- },
477
- body: chunk,
478
- });
571
+ const response = await this.putWithTimeout(part.url, chunk, {}, timeoutMs);
479
572
  if (!response.ok) {
480
573
  const errorText = await response.text();
481
- throw new Error(`Multipart upload failed at part ${part.part_number} ` +
482
- `(${response.status}): ${errorText}`);
574
+ throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
483
575
  }
484
576
  if (options.onProgress) {
485
577
  const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
@@ -488,54 +580,33 @@ export class yetter {
488
580
  }
489
581
  }
490
582
  }
583
+ export class yetter {
584
+ static configure(options) {
585
+ this.client.configure(options);
586
+ }
587
+ static subscribe(model, options) {
588
+ return this.client.subscribe(model, options);
589
+ }
590
+ static stream(model, options) {
591
+ return this.client.stream(model, options);
592
+ }
593
+ static uploadFile(fileOrPath, options = {}) {
594
+ return this.client.uploadFile(fileOrPath, options);
595
+ }
596
+ static uploadBlob(file, options = {}) {
597
+ return this.client.uploadBlob(file, options);
598
+ }
599
+ }
491
600
  _a = yetter;
492
- yetter.apiKey = 'Key ' + (process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "");
493
- yetter.endpoint = "https://api.yetter.ai";
601
+ yetter.client = new YetterClient();
494
602
  yetter.queue = {
495
603
  submit: async (model, options) => {
496
- if (!_a.apiKey) {
497
- throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
498
- }
499
- const client = new YetterImageClient({
500
- apiKey: _a.apiKey,
501
- endpoint: _a.endpoint
502
- });
503
- const generateResponse = await client.generate({
504
- model: model,
505
- ...options.input,
506
- });
507
- return generateResponse;
604
+ return _a.client.queue.submit(model, options);
508
605
  },
509
606
  status: async (model, options) => {
510
- if (!_a.apiKey) {
511
- throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
512
- }
513
- const client = new YetterImageClient({
514
- apiKey: _a.apiKey,
515
- endpoint: _a.endpoint
516
- });
517
- const endpoint = client.getApiEndpoint();
518
- const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
519
- const statusData = await client.getStatus({ url: statusUrl });
520
- return {
521
- data: statusData,
522
- requestId: options.requestId,
523
- };
607
+ return _a.client.queue.status(model, options);
524
608
  },
525
609
  result: async (model, options) => {
526
- if (!_a.apiKey) {
527
- throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
528
- }
529
- const client = new YetterImageClient({
530
- apiKey: _a.apiKey,
531
- endpoint: _a.endpoint
532
- });
533
- const endpoint = client.getApiEndpoint();
534
- const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
535
- const responseData = await client.getResponse({ url: responseUrl });
536
- return {
537
- data: responseData,
538
- requestId: options.requestId,
539
- };
540
- }
610
+ return _a.client.queue.result(model, options);
611
+ },
541
612
  };