@yetter/client 0.0.12 → 0.0.13

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,126 @@
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_ENDPOINT = "https://api.yetter.ai";
8
+ function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+ function getEnvApiKey() {
12
+ return process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "";
13
+ }
14
+ export class YetterClient {
15
+ constructor(options = {}) {
16
+ this.queue = {
17
+ submit: async (model, options) => {
18
+ const client = this.createApiClient();
19
+ return client.generate({
20
+ model,
21
+ ...options.input,
22
+ });
23
+ },
24
+ status: async (model, options) => {
25
+ const client = this.createApiClient();
26
+ const endpoint = client.getApiEndpoint();
27
+ const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
28
+ const statusData = await client.getStatus({ url: statusUrl });
29
+ return {
30
+ data: statusData,
31
+ requestId: options.requestId,
32
+ };
33
+ },
34
+ result: async (model, options) => {
35
+ const client = this.createApiClient();
36
+ const endpoint = client.getApiEndpoint();
37
+ const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
38
+ const responseData = await client.getResponse({ url: responseUrl });
39
+ return {
40
+ data: responseData,
41
+ requestId: options.requestId,
42
+ };
43
+ },
44
+ };
45
+ this.apiKey = "";
46
+ this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
47
+ const envKey = getEnvApiKey();
48
+ if (envKey) {
49
+ this.apiKey = `Key ${envKey}`;
50
+ }
51
+ if (typeof options.apiKey === "string") {
52
+ this.setApiKey(options.apiKey, options.is_bearer);
53
+ }
54
+ }
55
+ setApiKey(rawApiKey, isBearer) {
56
+ const normalizedApiKey = rawApiKey.trim();
57
+ if (!normalizedApiKey) {
58
+ this.apiKey = "";
59
+ return;
60
+ }
61
+ this.apiKey = isBearer ? `Bearer ${normalizedApiKey}` : `Key ${normalizedApiKey}`;
62
+ }
63
+ assertApiKeyConfigured() {
64
+ if (!this.apiKey) {
65
+ throw new Error("API key is not configured. Call configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
66
+ }
67
+ }
68
+ createApiClient() {
69
+ this.assertApiKeyConfigured();
70
+ return new YetterImageClient({
71
+ apiKey: this.apiKey,
72
+ endpoint: this.endpoint,
73
+ });
74
+ }
75
+ getUploadTimeoutMs(options) {
76
+ if (typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0) {
77
+ return options.timeout;
78
+ }
79
+ return DEFAULT_UPLOAD_TIMEOUT_MS;
80
+ }
81
+ async putWithTimeout(url, body, headers, timeoutMs) {
82
+ const controller = new AbortController();
83
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
84
+ try {
85
+ return await fetch(url, {
86
+ method: "PUT",
87
+ headers,
88
+ body,
89
+ signal: controller.signal,
90
+ });
91
+ }
92
+ catch (error) {
93
+ if ((error === null || error === void 0 ? void 0 : error.name) === "AbortError") {
94
+ throw new Error(`Upload request timed out after ${timeoutMs}ms`);
12
95
  }
96
+ throw error;
97
+ }
98
+ finally {
99
+ clearTimeout(timeout);
100
+ }
101
+ }
102
+ configure(options) {
103
+ if (typeof options.apiKey === "string") {
104
+ this.setApiKey(options.apiKey, options.is_bearer);
13
105
  }
14
106
  if (options.endpoint) {
15
- _a.endpoint = options.endpoint;
107
+ this.endpoint = options.endpoint;
16
108
  }
17
109
  }
18
- static async subscribe(model, options) {
110
+ async subscribe(model, options) {
19
111
  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
- });
112
+ const client = this.createApiClient();
27
113
  const generateResponse = await client.generate({
28
- model: model,
114
+ model,
29
115
  ...options.input,
30
116
  });
31
117
  let status = generateResponse.status;
32
118
  let lastStatusResponse;
33
119
  const startTime = Date.now();
34
- const timeoutMilliseconds = 30 * 60 * 1000; // 30 minutes
120
+ const timeoutMilliseconds = 30 * 60 * 1000;
121
+ const pollIntervalMs = typeof options.pollIntervalMs === "number" && Number.isFinite(options.pollIntervalMs) && options.pollIntervalMs > 0
122
+ ? options.pollIntervalMs
123
+ : DEFAULT_POLL_INTERVAL_MS;
35
124
  while (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
36
125
  if (Date.now() - startTime > timeoutMilliseconds) {
37
126
  console.warn(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}. Attempting to cancel.`);
@@ -53,6 +142,9 @@ export class yetter {
53
142
  if (options.onQueueUpdate) {
54
143
  options.onQueueUpdate(lastStatusResponse);
55
144
  }
145
+ if (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
146
+ await sleep(pollIntervalMs);
147
+ }
56
148
  }
57
149
  catch (error) {
58
150
  console.error("Error during status polling:", error);
@@ -60,35 +152,28 @@ export class yetter {
60
152
  }
61
153
  }
62
154
  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.";
155
+ 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
156
  throw new Error(errorMessage);
65
157
  }
66
- else if (status === "CANCELLED") {
158
+ if (status === "CANCELLED") {
67
159
  throw new Error("Image generation was cancelled by user.");
68
160
  }
69
- const finalResponse = await client.getResponse({
161
+ return client.getResponse({
70
162
  url: generateResponse.response_url,
71
163
  });
72
- return finalResponse;
73
164
  }
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
- });
165
+ async stream(model, options) {
166
+ const client = this.createApiClient();
82
167
  const initialApiResponse = await client.generate({
83
- model: model,
168
+ model,
84
169
  ...options.input,
85
170
  });
86
171
  const requestId = initialApiResponse.request_id;
87
172
  const responseUrl = initialApiResponse.response_url;
173
+ const statusUrl = initialApiResponse.status_url;
88
174
  const cancelUrl = initialApiResponse.cancel_url;
89
175
  const sseStreamUrl = `${client.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
90
176
  let eventSource;
91
- // Setup the promise for the done() method
92
177
  let resolveDonePromise;
93
178
  let rejectDonePromise;
94
179
  const donePromise = new Promise((resolve, reject) => {
@@ -96,8 +181,6 @@ export class yetter {
96
181
  rejectDonePromise = reject;
97
182
  });
98
183
  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
184
  events: [],
102
185
  resolvers: [],
103
186
  isClosed: false,
@@ -124,7 +207,6 @@ export class yetter {
124
207
  else {
125
208
  this.events.push(event);
126
209
  }
127
- // Check for terminal events to resolve/reject the donePromise
128
210
  this.currentStatus = event.status;
129
211
  },
130
212
  error(err) {
@@ -159,7 +241,7 @@ export class yetter {
159
241
  if (this.isClosed)
160
242
  return;
161
243
  this.isClosed = true;
162
- this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
244
+ this.resolvers.forEach((resolve) => resolve({ value: undefined, done: true }));
163
245
  this.resolvers = [];
164
246
  eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
165
247
  },
@@ -170,73 +252,98 @@ export class yetter {
170
252
  if (this.isClosed) {
171
253
  return Promise.resolve({ value: undefined, done: true });
172
254
  }
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);
255
+ return new Promise((resolve) => this.resolvers.push(resolve));
256
+ },
181
257
  };
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}`));
191
- }
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);
200
- try {
201
- if (controller.currentStatus === "COMPLETED") {
202
- // Close SSE immediately to avoid any late 'error' events during await
258
+ if (initialApiResponse.status === "COMPLETED") {
259
+ controller.push(initialApiResponse);
260
+ const finalResponse = await client.getResponse({ url: responseUrl });
261
+ controller.done(finalResponse);
262
+ }
263
+ else if (initialApiResponse.status === "ERROR") {
264
+ controller.push(initialApiResponse);
265
+ controller.error(new Error(`Stream reported ERROR for ${requestId}`));
266
+ }
267
+ else if (initialApiResponse.status === "CANCELLED") {
268
+ controller.push(initialApiResponse);
269
+ controller.cancel();
270
+ }
271
+ else {
272
+ eventSource = new EventSourcePolyfill(sseStreamUrl, {
273
+ headers: { Authorization: this.apiKey },
274
+ heartbeatTimeout: STREAM_HEARTBEAT_TIMEOUT_MS,
275
+ });
276
+ eventSource.onopen = (event) => {
277
+ console.log("SSE Connection Opened:", event);
278
+ };
279
+ eventSource.addEventListener("data", (event) => {
280
+ try {
281
+ const statusData = JSON.parse(event.data);
282
+ controller.push(statusData);
283
+ }
284
+ catch (e) {
285
+ console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
286
+ controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
287
+ }
288
+ });
289
+ eventSource.addEventListener("done", async (event) => {
290
+ try {
203
291
  try {
204
292
  eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
205
293
  }
206
294
  catch { }
207
- const response = await client.getResponse({ url: responseUrl });
208
- controller.done(response);
295
+ if (controller.currentStatus === "COMPLETED") {
296
+ const response = await client.getResponse({ url: responseUrl });
297
+ controller.done(response);
298
+ return;
299
+ }
300
+ if (controller.currentStatus === "CANCELLED") {
301
+ controller.cancel();
302
+ return;
303
+ }
304
+ if (controller.currentStatus === "ERROR") {
305
+ controller.error(new Error(`Stream reported ERROR for ${requestId}`));
306
+ return;
307
+ }
308
+ const latestStatus = await client.getStatus({ url: statusUrl });
309
+ controller.push(latestStatus);
310
+ if (latestStatus.status === "COMPLETED") {
311
+ const response = await client.getResponse({ url: responseUrl });
312
+ controller.done(response);
313
+ }
314
+ else if (latestStatus.status === "CANCELLED") {
315
+ controller.cancel();
316
+ }
317
+ else {
318
+ controller.error(new Error(`Stream ended without terminal status for ${requestId}`));
319
+ }
209
320
  }
210
- else if (controller.currentStatus === "CANCELLED") {
211
- controller.cancel();
321
+ catch (e) {
322
+ console.error("Error parsing SSE 'done' event:", e, "Raw data:", event.data);
323
+ controller.error(new Error(`Error parsing SSE 'done' event: ${e.message}`));
212
324
  }
213
- }
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;
231
- }
232
- console.warn("SSE Connection Error (onerror) - will allow auto-reconnect:", err);
233
- };
234
- // Handle if API immediately returns a terminal status in initialApiResponse (e.g. already completed/failed)
235
- if (initialApiResponse.status === "COMPLETED") {
236
- controller.push(initialApiResponse);
237
- }
238
- else if (initialApiResponse.status === "ERROR") {
239
- controller.push(initialApiResponse);
325
+ });
326
+ eventSource.addEventListener("error", (event) => {
327
+ const rawData = event === null || event === void 0 ? void 0 : event.data;
328
+ if (!rawData) {
329
+ console.warn("SSE transport error event; waiting for reconnect.");
330
+ return;
331
+ }
332
+ console.log("SSE application 'error' event received, raw data:", rawData);
333
+ controller.error(new Error(`Stream reported ERROR for ${requestId}: ${rawData}`));
334
+ });
335
+ eventSource.onerror = (err) => {
336
+ var _b;
337
+ 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) || "";
338
+ const isIdleTimeout = typeof message === "string" &&
339
+ message.includes("No activity within") &&
340
+ message.includes("Reconnecting");
341
+ if (isIdleTimeout) {
342
+ console.warn("SSE idle timeout; letting EventSource auto-reconnect.", err);
343
+ return;
344
+ }
345
+ console.warn("SSE connection issue (onerror); waiting for auto-reconnect.", err);
346
+ };
240
347
  }
241
348
  return {
242
349
  async *[Symbol.asyncIterator]() {
@@ -260,56 +367,19 @@ export class yetter {
260
367
  getRequestId: () => requestId,
261
368
  };
262
369
  }
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);
370
+ async uploadFile(fileOrPath, options = {}) {
371
+ this.assertApiKeyConfigured();
372
+ if (typeof fileOrPath === "string") {
373
+ return this._uploadFromPath(fileOrPath, options);
296
374
  }
375
+ return this._uploadFromBlob(fileOrPath, options);
297
376
  }
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);
377
+ async uploadBlob(file, options = {}) {
378
+ return this.uploadFile(file, options);
304
379
  }
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
380
+ async _uploadFromPath(filePath, options) {
381
+ const fs = await import("fs");
382
+ const mime = await import("mime-types");
313
383
  if (!fs.existsSync(filePath)) {
314
384
  throw new Error(`File not found: ${filePath}`);
315
385
  }
@@ -317,24 +387,19 @@ export class yetter {
317
387
  const fileSize = stats.size;
318
388
  const fileName = filePath.split(/[\\/]/).pop() || "upload";
319
389
  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)
390
+ const uploadTimeoutMs = this.getUploadTimeoutMs(options);
391
+ const client = this.createApiClient();
325
392
  const uploadUrlResponse = await client.getUploadUrl({
326
393
  file_name: fileName,
327
394
  content_type: mimeType,
328
395
  size: fileSize,
329
396
  });
330
- // Step 2: Upload file content
331
397
  if (uploadUrlResponse.mode === "single") {
332
- await _a._uploadFileSingle(filePath, uploadUrlResponse.put_url, mimeType, fileSize, options, fs);
398
+ await this._uploadFileSingle(filePath, uploadUrlResponse.put_url, mimeType, fileSize, options, fs, uploadTimeoutMs);
333
399
  }
334
400
  else {
335
- await _a._uploadFileMultipart(filePath, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, fs);
401
+ await this._uploadFileMultipart(filePath, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, fs, uploadTimeoutMs);
336
402
  }
337
- // Step 3: Notify completion
338
403
  const result = await client.uploadComplete({
339
404
  key: uploadUrlResponse.key,
340
405
  });
@@ -343,32 +408,23 @@ export class yetter {
343
408
  }
344
409
  return result;
345
410
  }
346
- /**
347
- * Upload file from Blob/File object (browser)
348
- */
349
- static async _uploadFromBlob(file, options) {
411
+ async _uploadFromBlob(file, options) {
350
412
  const fileSize = file.size;
351
- const fileName = options.filename ||
352
- (file instanceof File ? file.name : "blob-upload");
413
+ const fileName = options.filename || (file instanceof File ? file.name : "blob-upload");
353
414
  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)
415
+ const uploadTimeoutMs = this.getUploadTimeoutMs(options);
416
+ const client = this.createApiClient();
359
417
  const uploadUrlResponse = await client.getUploadUrl({
360
418
  file_name: fileName,
361
419
  content_type: mimeType,
362
420
  size: fileSize,
363
421
  });
364
- // Step 2: Upload file content
365
422
  if (uploadUrlResponse.mode === "single") {
366
- await _a._uploadBlobSingle(file, uploadUrlResponse.put_url, mimeType, fileSize, options);
423
+ await this._uploadBlobSingle(file, uploadUrlResponse.put_url, mimeType, fileSize, options, uploadTimeoutMs);
367
424
  }
368
425
  else {
369
- await _a._uploadBlobMultipart(file, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options);
426
+ await this._uploadBlobMultipart(file, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, uploadTimeoutMs);
370
427
  }
371
- // Step 3: Notify completion
372
428
  const result = await client.uploadComplete({
373
429
  key: uploadUrlResponse.key,
374
430
  });
@@ -377,19 +433,12 @@ export class yetter {
377
433
  }
378
434
  return result;
379
435
  }
380
- /**
381
- * Upload file using single PUT request (Node.js, private helper)
382
- */
383
- static async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs) {
436
+ async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs, timeoutMs) {
384
437
  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
- });
438
+ const response = await this.putWithTimeout(presignedUrl, fileBuffer, {
439
+ "Content-Type": contentType,
440
+ "Content-Length": String(totalSize),
441
+ }, timeoutMs);
393
442
  if (!response.ok) {
394
443
  const errorText = await response.text();
395
444
  throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
@@ -398,10 +447,7 @@ export class yetter {
398
447
  options.onProgress(90);
399
448
  }
400
449
  }
401
- /**
402
- * Upload file using multipart upload (Node.js, private helper)
403
- */
404
- static async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs) {
450
+ async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs, timeoutMs) {
405
451
  const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
406
452
  const fileHandle = fs.openSync(filePath, "r");
407
453
  try {
@@ -414,17 +460,12 @@ export class yetter {
414
460
  if (chunk.length === 0) {
415
461
  break;
416
462
  }
417
- const response = await fetch(part.url, {
418
- method: "PUT",
419
- headers: {
420
- "Content-Length": String(chunk.length),
421
- },
422
- body: chunk,
423
- });
463
+ const response = await this.putWithTimeout(part.url, chunk, {
464
+ "Content-Length": String(chunk.length),
465
+ }, timeoutMs);
424
466
  if (!response.ok) {
425
467
  const errorText = await response.text();
426
- throw new Error(`Multipart upload failed at part ${part.part_number} ` +
427
- `(${response.status}): ${errorText}`);
468
+ throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
428
469
  }
429
470
  if (options.onProgress) {
430
471
  const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
@@ -436,18 +477,10 @@ export class yetter {
436
477
  fs.closeSync(fileHandle);
437
478
  }
438
479
  }
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
- });
480
+ async _uploadBlobSingle(blob, presignedUrl, contentType, _totalSize, options, timeoutMs) {
481
+ const response = await this.putWithTimeout(presignedUrl, blob, {
482
+ "Content-Type": contentType,
483
+ }, timeoutMs);
451
484
  if (!response.ok) {
452
485
  const errorText = await response.text();
453
486
  throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
@@ -456,10 +489,7 @@ export class yetter {
456
489
  options.onProgress(90);
457
490
  }
458
491
  }
459
- /**
460
- * Upload blob using multipart upload (browser, private helper)
461
- */
462
- static async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options) {
492
+ async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options, timeoutMs) {
463
493
  const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
464
494
  for (let i = 0; i < sortedParts.length; i++) {
465
495
  const part = sortedParts[i];
@@ -469,17 +499,10 @@ export class yetter {
469
499
  if (chunk.size === 0) {
470
500
  break;
471
501
  }
472
- const response = await fetch(part.url, {
473
- method: "PUT",
474
- headers: {
475
- "Content-Length": String(chunk.size),
476
- },
477
- body: chunk,
478
- });
502
+ const response = await this.putWithTimeout(part.url, chunk, {}, timeoutMs);
479
503
  if (!response.ok) {
480
504
  const errorText = await response.text();
481
- throw new Error(`Multipart upload failed at part ${part.part_number} ` +
482
- `(${response.status}): ${errorText}`);
505
+ throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
483
506
  }
484
507
  if (options.onProgress) {
485
508
  const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
@@ -488,54 +511,33 @@ export class yetter {
488
511
  }
489
512
  }
490
513
  }
514
+ export class yetter {
515
+ static configure(options) {
516
+ this.client.configure(options);
517
+ }
518
+ static subscribe(model, options) {
519
+ return this.client.subscribe(model, options);
520
+ }
521
+ static stream(model, options) {
522
+ return this.client.stream(model, options);
523
+ }
524
+ static uploadFile(fileOrPath, options = {}) {
525
+ return this.client.uploadFile(fileOrPath, options);
526
+ }
527
+ static uploadBlob(file, options = {}) {
528
+ return this.client.uploadBlob(file, options);
529
+ }
530
+ }
491
531
  _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";
532
+ yetter.client = new YetterClient();
494
533
  yetter.queue = {
495
534
  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;
535
+ return _a.client.queue.submit(model, options);
508
536
  },
509
537
  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
- };
538
+ return _a.client.queue.status(model, options);
524
539
  },
525
540
  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
- }
541
+ return _a.client.queue.result(model, options);
542
+ },
541
543
  };