@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/README.md +52 -3
- package/dist/api.d.ts +3 -0
- package/dist/api.js +110 -60
- package/dist/client.d.ts +27 -52
- package/dist/client.js +264 -262
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +3 -0
- package/package.json +6 -1
- package/bowow2.jpeg +0 -0
- package/examples/stream.ts +0 -54
- package/examples/submit.ts +0 -80
- package/examples/subscribe.ts +0 -41
- package/examples/upload-and-generate.ts +0 -39
- package/src/api.ts +0 -159
- package/src/client.ts +0 -697
- package/src/index.ts +0 -12
- package/src/types.ts +0 -192
- package/tsconfig.json +0 -13
package/src/client.ts
DELETED
|
@@ -1,697 +0,0 @@
|
|
|
1
|
-
import { YetterImageClient } from "./api.js";
|
|
2
|
-
import { EventSourcePolyfill } from 'event-source-polyfill';
|
|
3
|
-
import {
|
|
4
|
-
ClientOptions,
|
|
5
|
-
GetStatusResponse,
|
|
6
|
-
GetResponseResponse,
|
|
7
|
-
SubscribeOptions,
|
|
8
|
-
GenerateResponse,
|
|
9
|
-
SubmitQueueOptions,
|
|
10
|
-
GetResultOptions,
|
|
11
|
-
GetResultResponse,
|
|
12
|
-
StatusOptions,
|
|
13
|
-
StatusResponse,
|
|
14
|
-
StreamOptions,
|
|
15
|
-
StreamEvent,
|
|
16
|
-
YetterStream,
|
|
17
|
-
UploadOptions,
|
|
18
|
-
UploadCompleteResponse,
|
|
19
|
-
} from "./types.js";
|
|
20
|
-
|
|
21
|
-
export class yetter {
|
|
22
|
-
private static apiKey: string = 'Key ' + (process.env.YTR_API_KEY || process.env.REACT_APP_YTR_API_KEY || "");
|
|
23
|
-
private static endpoint: string = "https://api.yetter.ai";
|
|
24
|
-
|
|
25
|
-
public static configure(options: ClientOptions) {
|
|
26
|
-
if (options.apiKey) {
|
|
27
|
-
if (options.is_bearer) {
|
|
28
|
-
yetter.apiKey = 'Bearer ' + options.apiKey;
|
|
29
|
-
} else {
|
|
30
|
-
yetter.apiKey = 'Key ' + options.apiKey;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (options.endpoint) {
|
|
34
|
-
yetter.endpoint = options.endpoint;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
public static async subscribe(
|
|
39
|
-
model: string,
|
|
40
|
-
options: SubscribeOptions
|
|
41
|
-
): Promise<GetResponseResponse> {
|
|
42
|
-
if (!yetter.apiKey) {
|
|
43
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
44
|
-
}
|
|
45
|
-
const client = new YetterImageClient({
|
|
46
|
-
apiKey: yetter.apiKey,
|
|
47
|
-
endpoint: yetter.endpoint
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const generateResponse = await client.generate({
|
|
51
|
-
model: model,
|
|
52
|
-
...options.input,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
let status = generateResponse.status;
|
|
56
|
-
let lastStatusResponse: GetStatusResponse | undefined;
|
|
57
|
-
|
|
58
|
-
const startTime = Date.now();
|
|
59
|
-
const timeoutMilliseconds = 30 * 60 * 1000; // 30 minutes
|
|
60
|
-
|
|
61
|
-
while (status !== "COMPLETED" && status !== "ERROR" && status !== "CANCELLED") {
|
|
62
|
-
if (Date.now() - startTime > timeoutMilliseconds) {
|
|
63
|
-
console.warn(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}. Attempting to cancel.`);
|
|
64
|
-
try {
|
|
65
|
-
await client.cancel({ url: generateResponse.cancel_url });
|
|
66
|
-
console.log(`Cancellation request sent for request ID: ${generateResponse.request_id}.`);
|
|
67
|
-
} catch (cancelError: any) {
|
|
68
|
-
console.error(`Failed to cancel request ID: ${generateResponse.request_id} after timeout:`, cancelError.message || cancelError);
|
|
69
|
-
}
|
|
70
|
-
throw new Error(`Subscription timed out after 30 minutes for request ID: ${generateResponse.request_id}.`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
lastStatusResponse = await client.getStatus({
|
|
75
|
-
url: generateResponse.status_url,
|
|
76
|
-
logs: options.logs,
|
|
77
|
-
});
|
|
78
|
-
status = lastStatusResponse.status;
|
|
79
|
-
|
|
80
|
-
if (options.onQueueUpdate) {
|
|
81
|
-
options.onQueueUpdate(lastStatusResponse);
|
|
82
|
-
}
|
|
83
|
-
} catch (error) {
|
|
84
|
-
console.error("Error during status polling:", error);
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (status === "ERROR") {
|
|
90
|
-
const errorMessage = lastStatusResponse?.logs?.map(log => log.message).join("\n") || "Image generation failed.";
|
|
91
|
-
throw new Error(errorMessage);
|
|
92
|
-
} else if (status === "CANCELLED") {
|
|
93
|
-
throw new Error("Image generation was cancelled by user.");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const finalResponse = await client.getResponse({
|
|
97
|
-
url: generateResponse.response_url,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
return finalResponse;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
public static queue = {
|
|
104
|
-
submit: async (
|
|
105
|
-
model: string,
|
|
106
|
-
options: SubmitQueueOptions
|
|
107
|
-
): Promise<GenerateResponse> => {
|
|
108
|
-
if (!yetter.apiKey) {
|
|
109
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
110
|
-
}
|
|
111
|
-
const client = new YetterImageClient({
|
|
112
|
-
apiKey: yetter.apiKey,
|
|
113
|
-
endpoint: yetter.endpoint
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const generateResponse = await client.generate({
|
|
117
|
-
model: model,
|
|
118
|
-
...options.input,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
return generateResponse;
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
status: async (
|
|
125
|
-
model: string,
|
|
126
|
-
options: StatusOptions
|
|
127
|
-
): Promise<StatusResponse> => {
|
|
128
|
-
if (!yetter.apiKey) {
|
|
129
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
130
|
-
}
|
|
131
|
-
const client = new YetterImageClient({
|
|
132
|
-
apiKey: yetter.apiKey,
|
|
133
|
-
endpoint: yetter.endpoint
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const endpoint = client.getApiEndpoint();
|
|
137
|
-
|
|
138
|
-
const statusUrl = `${endpoint}/${model}/requests/${options.requestId}/status`;
|
|
139
|
-
|
|
140
|
-
const statusData = await client.getStatus({ url: statusUrl });
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
data: statusData,
|
|
144
|
-
requestId: options.requestId,
|
|
145
|
-
};
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
result: async (
|
|
149
|
-
model: string,
|
|
150
|
-
options: GetResultOptions
|
|
151
|
-
): Promise<GetResultResponse> => {
|
|
152
|
-
if (!yetter.apiKey) {
|
|
153
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
154
|
-
}
|
|
155
|
-
const client = new YetterImageClient({
|
|
156
|
-
apiKey: yetter.apiKey,
|
|
157
|
-
endpoint: yetter.endpoint
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const endpoint = client.getApiEndpoint();
|
|
161
|
-
|
|
162
|
-
const responseUrl = `${endpoint}/${model}/requests/${options.requestId}`;
|
|
163
|
-
|
|
164
|
-
const responseData = await client.getResponse({ url: responseUrl });
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
data: responseData,
|
|
168
|
-
requestId: options.requestId,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
public static async stream(
|
|
174
|
-
model: string,
|
|
175
|
-
options: StreamOptions
|
|
176
|
-
): Promise<YetterStream> {
|
|
177
|
-
if (!yetter.apiKey) {
|
|
178
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
179
|
-
}
|
|
180
|
-
const client = new YetterImageClient({
|
|
181
|
-
apiKey: yetter.apiKey,
|
|
182
|
-
endpoint: yetter.endpoint
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const initialApiResponse = await client.generate({
|
|
186
|
-
model: model,
|
|
187
|
-
...options.input,
|
|
188
|
-
});
|
|
189
|
-
const requestId = initialApiResponse.request_id;
|
|
190
|
-
const responseUrl = initialApiResponse.response_url;
|
|
191
|
-
const cancelUrl = initialApiResponse.cancel_url;
|
|
192
|
-
const sseStreamUrl = `${client.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
|
|
193
|
-
|
|
194
|
-
let eventSource: EventSource;
|
|
195
|
-
|
|
196
|
-
// Setup the promise for the done() method
|
|
197
|
-
let resolveDonePromise: (value: GetResponseResponse) => void;
|
|
198
|
-
let rejectDonePromise: (reason?: any) => void;
|
|
199
|
-
const donePromise = new Promise<GetResponseResponse>((resolve, reject) => {
|
|
200
|
-
resolveDonePromise = resolve;
|
|
201
|
-
rejectDonePromise = reject;
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const controller = {
|
|
205
|
-
// This will be used by the async iterator to pull events
|
|
206
|
-
// It needs a way to buffer events or signal availability
|
|
207
|
-
events: [] as StreamEvent[],
|
|
208
|
-
resolvers: [] as Array<(value: IteratorResult<StreamEvent, any>) => void>,
|
|
209
|
-
isClosed: false,
|
|
210
|
-
isSettled: false,
|
|
211
|
-
currentStatus: "" as "COMPLETED" | "ERROR" | "CANCELLED" | "IN_PROGRESS" | "IN_QUEUE",
|
|
212
|
-
callResolver(value: GetResponseResponse) {
|
|
213
|
-
if (this.isSettled) return;
|
|
214
|
-
this.isSettled = true;
|
|
215
|
-
resolveDonePromise(value);
|
|
216
|
-
},
|
|
217
|
-
callRejecter(reason?: any) {
|
|
218
|
-
if (this.isSettled) return;
|
|
219
|
-
this.isSettled = true;
|
|
220
|
-
rejectDonePromise(reason);
|
|
221
|
-
},
|
|
222
|
-
push(event: StreamEvent) {
|
|
223
|
-
if (this.isClosed) return;
|
|
224
|
-
if (this.resolvers.length > 0) {
|
|
225
|
-
this.resolvers.shift()!({ value: event, done: false });
|
|
226
|
-
} else {
|
|
227
|
-
this.events.push(event);
|
|
228
|
-
}
|
|
229
|
-
// Check for terminal events to resolve/reject the donePromise
|
|
230
|
-
this.currentStatus = event.status as "COMPLETED" | "ERROR" | "CANCELLED" | "IN_PROGRESS" | "IN_QUEUE";
|
|
231
|
-
},
|
|
232
|
-
error(err: any) {
|
|
233
|
-
if (this.isClosed) return;
|
|
234
|
-
if (this.resolvers.length > 0) {
|
|
235
|
-
this.resolvers.shift()!({ value: undefined, done: true });
|
|
236
|
-
}
|
|
237
|
-
this._close();
|
|
238
|
-
const errorToReport = err instanceof Error ? err : new Error("Stream closed prematurely or unexpectedly.");
|
|
239
|
-
this.callRejecter(errorToReport);
|
|
240
|
-
},
|
|
241
|
-
done(value: GetResponseResponse) {
|
|
242
|
-
if (this.isClosed) return;
|
|
243
|
-
if (this.resolvers.length > 0) {
|
|
244
|
-
this.resolvers.shift()!({ value: undefined, done: true });
|
|
245
|
-
}
|
|
246
|
-
this._close();
|
|
247
|
-
this.callResolver(value);
|
|
248
|
-
},
|
|
249
|
-
cancel() {
|
|
250
|
-
if (this.isClosed) return;
|
|
251
|
-
if (this.resolvers.length > 0) {
|
|
252
|
-
this.resolvers.shift()!({ value: undefined, done: true });
|
|
253
|
-
}
|
|
254
|
-
this._close();
|
|
255
|
-
this.callRejecter(new Error("Stream was cancelled by user."));
|
|
256
|
-
},
|
|
257
|
-
_close() {
|
|
258
|
-
if (this.isClosed) return;
|
|
259
|
-
this.isClosed = true;
|
|
260
|
-
this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
|
|
261
|
-
this.resolvers = [];
|
|
262
|
-
eventSource?.close();
|
|
263
|
-
},
|
|
264
|
-
next(): Promise<IteratorResult<StreamEvent, any>> {
|
|
265
|
-
if (this.events.length > 0) {
|
|
266
|
-
return Promise.resolve({ value: this.events.shift()!, done: false });
|
|
267
|
-
}
|
|
268
|
-
if (this.isClosed) {
|
|
269
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
270
|
-
}
|
|
271
|
-
return new Promise(resolve => this.resolvers.push(resolve));
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
276
|
-
headers: { 'Authorization': `${yetter.apiKey}` },
|
|
277
|
-
} as any);
|
|
278
|
-
|
|
279
|
-
eventSource.onopen = (event: Event) => {
|
|
280
|
-
console.log("SSE Connection Opened:", event);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
eventSource.addEventListener('data', (event: MessageEvent) => {
|
|
284
|
-
// console.log("SSE 'data' event received, raw data:", event.data);
|
|
285
|
-
try {
|
|
286
|
-
const statusData: GetStatusResponse = JSON.parse(event.data as string);
|
|
287
|
-
controller.push(statusData as StreamEvent);
|
|
288
|
-
} catch (e: any) {
|
|
289
|
-
console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
|
|
290
|
-
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
// when 'done' event is received, currentStatus can only be COMPLETED or CANCELLED
|
|
294
|
-
// TODO: remove currentStatus and branch two cases(COMPLETED and CANCELLED) by response status by responseUrl
|
|
295
|
-
// TODO: Determine whether raise error or not when response status is CANCELLED
|
|
296
|
-
// => current code raise error when response status is CANCELLED because resolveDonePromise only get completed response
|
|
297
|
-
// => this mean that user expect only completed response from .done() method
|
|
298
|
-
eventSource.addEventListener('done', async (event: MessageEvent) => {
|
|
299
|
-
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
300
|
-
try {
|
|
301
|
-
if (controller.currentStatus === "COMPLETED") {
|
|
302
|
-
// Close SSE immediately to avoid any late 'error' events during await
|
|
303
|
-
try { eventSource?.close(); } catch {}
|
|
304
|
-
const response = await client.getResponse({ url: responseUrl });
|
|
305
|
-
controller.done(response);
|
|
306
|
-
} else if (controller.currentStatus === "CANCELLED") {
|
|
307
|
-
controller.cancel();
|
|
308
|
-
}
|
|
309
|
-
} catch (e: any) {
|
|
310
|
-
console.error("Error parsing SSE 'done' event:", e, "Raw data:", event.data);
|
|
311
|
-
controller.error(new Error(`Error parsing SSE 'done' event: ${e.message}`));
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
eventSource.addEventListener('error', (event: MessageEvent) => {
|
|
315
|
-
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
316
|
-
console.log("SSE 'error' event received, raw data:", event.data);
|
|
317
|
-
controller.error(new Error("Stream reported ERROR for ${requestId}"));
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
eventSource.onerror = (err: Event | MessageEvent) => {
|
|
321
|
-
const message = (err as any)?.error?.message || (err as any)?.message || '';
|
|
322
|
-
const isIdleTimeout = typeof message === 'string' && message.includes('No activity within') && message.includes('Reconnecting');
|
|
323
|
-
if (isIdleTimeout) {
|
|
324
|
-
console.warn("SSE idle timeout; letting EventSource auto-reconnect.", err);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
console.warn("SSE Connection Error (onerror) - will allow auto-reconnect:", err);
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// Handle if API immediately returns a terminal status in initialApiResponse (e.g. already completed/failed)
|
|
331
|
-
if (initialApiResponse.status === "COMPLETED"){
|
|
332
|
-
controller.push(initialApiResponse as any as StreamEvent);
|
|
333
|
-
} else if (initialApiResponse.status === "ERROR"){
|
|
334
|
-
controller.push(initialApiResponse as any as StreamEvent);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
async *[Symbol.asyncIterator]() {
|
|
339
|
-
while (!controller.isClosed || controller.events.length > 0) {
|
|
340
|
-
const event = await controller.next();
|
|
341
|
-
if (event.done) break;
|
|
342
|
-
yield event.value;
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
done: () => donePromise,
|
|
346
|
-
cancel: async () => {
|
|
347
|
-
try {
|
|
348
|
-
await client.cancel({ url: cancelUrl });
|
|
349
|
-
console.log(`Stream for ${requestId} - underlying request cancelled.`);
|
|
350
|
-
} catch (e: any) {
|
|
351
|
-
console.error(`Error cancelling underlying request for stream ${requestId}:`, e.message);
|
|
352
|
-
}
|
|
353
|
-
},
|
|
354
|
-
getRequestId: () => requestId,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Upload a file from the filesystem (Node.js) or File/Blob object (browser)
|
|
360
|
-
*
|
|
361
|
-
* @param fileOrPath File path (Node.js) or File/Blob object (browser)
|
|
362
|
-
* @param options Upload configuration options
|
|
363
|
-
* @returns Promise resolving to upload result with public URL
|
|
364
|
-
*
|
|
365
|
-
* @example
|
|
366
|
-
* ```typescript
|
|
367
|
-
* // Node.js
|
|
368
|
-
* const result = await yetter.uploadFile("/path/to/image.jpg", {
|
|
369
|
-
* onProgress: (pct) => console.log(`Upload: ${pct}%`)
|
|
370
|
-
* });
|
|
371
|
-
*
|
|
372
|
-
* // Browser
|
|
373
|
-
* const fileInput = document.querySelector('input[type="file"]');
|
|
374
|
-
* const file = fileInput.files[0];
|
|
375
|
-
* const result = await yetter.uploadFile(file, {
|
|
376
|
-
* onProgress: (pct) => updateProgressBar(pct)
|
|
377
|
-
* });
|
|
378
|
-
* ```
|
|
379
|
-
*/
|
|
380
|
-
public static async uploadFile(
|
|
381
|
-
fileOrPath: string | File | Blob,
|
|
382
|
-
options: UploadOptions = {}
|
|
383
|
-
): Promise<UploadCompleteResponse> {
|
|
384
|
-
if (!yetter.apiKey) {
|
|
385
|
-
throw new Error("API key is not configured. Call yetter.configure()");
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (typeof fileOrPath === 'string') {
|
|
389
|
-
// Node.js path
|
|
390
|
-
return yetter._uploadFromPath(fileOrPath, options);
|
|
391
|
-
} else {
|
|
392
|
-
// Browser File/Blob
|
|
393
|
-
return yetter._uploadFromBlob(fileOrPath, options);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Upload a file from browser (File or Blob object)
|
|
399
|
-
* This is an alias for uploadFile for better clarity in browser contexts
|
|
400
|
-
*/
|
|
401
|
-
public static async uploadBlob(
|
|
402
|
-
file: File | Blob,
|
|
403
|
-
options: UploadOptions = {}
|
|
404
|
-
): Promise<UploadCompleteResponse> {
|
|
405
|
-
return yetter.uploadFile(file, options);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Upload file from filesystem path (Node.js only)
|
|
410
|
-
*/
|
|
411
|
-
private static async _uploadFromPath(
|
|
412
|
-
filePath: string,
|
|
413
|
-
options: UploadOptions
|
|
414
|
-
): Promise<UploadCompleteResponse> {
|
|
415
|
-
// Dynamic import for Node.js modules
|
|
416
|
-
const fs = await import('fs');
|
|
417
|
-
const mime = await import('mime-types');
|
|
418
|
-
|
|
419
|
-
// Validate file exists
|
|
420
|
-
if (!fs.existsSync(filePath)) {
|
|
421
|
-
throw new Error(`File not found: ${filePath}`);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const stats = fs.statSync(filePath);
|
|
425
|
-
const fileSize = stats.size;
|
|
426
|
-
const fileName = filePath.split(/[\\/]/).pop() || "upload";
|
|
427
|
-
const mimeType = mime.default.lookup(filePath) || "application/octet-stream";
|
|
428
|
-
|
|
429
|
-
const client = new YetterImageClient({
|
|
430
|
-
apiKey: yetter.apiKey,
|
|
431
|
-
endpoint: yetter.endpoint,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// Step 1: Request presigned URL(s)
|
|
435
|
-
const uploadUrlResponse = await client.getUploadUrl({
|
|
436
|
-
file_name: fileName,
|
|
437
|
-
content_type: mimeType,
|
|
438
|
-
size: fileSize,
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
// Step 2: Upload file content
|
|
442
|
-
if (uploadUrlResponse.mode === "single") {
|
|
443
|
-
await yetter._uploadFileSingle(
|
|
444
|
-
filePath,
|
|
445
|
-
uploadUrlResponse.put_url!,
|
|
446
|
-
mimeType,
|
|
447
|
-
fileSize,
|
|
448
|
-
options,
|
|
449
|
-
fs
|
|
450
|
-
);
|
|
451
|
-
} else {
|
|
452
|
-
await yetter._uploadFileMultipart(
|
|
453
|
-
filePath,
|
|
454
|
-
uploadUrlResponse.part_urls!,
|
|
455
|
-
uploadUrlResponse.part_size!,
|
|
456
|
-
fileSize,
|
|
457
|
-
options,
|
|
458
|
-
fs
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Step 3: Notify completion
|
|
463
|
-
const result = await client.uploadComplete({
|
|
464
|
-
key: uploadUrlResponse.key,
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
if (options.onProgress) {
|
|
468
|
-
options.onProgress(100);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return result;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Upload file from Blob/File object (browser)
|
|
476
|
-
*/
|
|
477
|
-
private static async _uploadFromBlob(
|
|
478
|
-
file: File | Blob,
|
|
479
|
-
options: UploadOptions
|
|
480
|
-
): Promise<UploadCompleteResponse> {
|
|
481
|
-
const fileSize = file.size;
|
|
482
|
-
const fileName = options.filename ||
|
|
483
|
-
(file instanceof File ? file.name : "blob-upload");
|
|
484
|
-
const mimeType = file.type || "application/octet-stream";
|
|
485
|
-
|
|
486
|
-
const client = new YetterImageClient({
|
|
487
|
-
apiKey: yetter.apiKey,
|
|
488
|
-
endpoint: yetter.endpoint,
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
// Step 1: Request presigned URL(s)
|
|
492
|
-
const uploadUrlResponse = await client.getUploadUrl({
|
|
493
|
-
file_name: fileName,
|
|
494
|
-
content_type: mimeType,
|
|
495
|
-
size: fileSize,
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// Step 2: Upload file content
|
|
499
|
-
if (uploadUrlResponse.mode === "single") {
|
|
500
|
-
await yetter._uploadBlobSingle(
|
|
501
|
-
file,
|
|
502
|
-
uploadUrlResponse.put_url!,
|
|
503
|
-
mimeType,
|
|
504
|
-
fileSize,
|
|
505
|
-
options
|
|
506
|
-
);
|
|
507
|
-
} else {
|
|
508
|
-
await yetter._uploadBlobMultipart(
|
|
509
|
-
file,
|
|
510
|
-
uploadUrlResponse.part_urls!,
|
|
511
|
-
uploadUrlResponse.part_size!,
|
|
512
|
-
fileSize,
|
|
513
|
-
options
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Step 3: Notify completion
|
|
518
|
-
const result = await client.uploadComplete({
|
|
519
|
-
key: uploadUrlResponse.key,
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
if (options.onProgress) {
|
|
523
|
-
options.onProgress(100);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return result;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Upload file using single PUT request (Node.js, private helper)
|
|
531
|
-
*/
|
|
532
|
-
private static async _uploadFileSingle(
|
|
533
|
-
filePath: string,
|
|
534
|
-
presignedUrl: string,
|
|
535
|
-
contentType: string,
|
|
536
|
-
totalSize: number,
|
|
537
|
-
options: UploadOptions,
|
|
538
|
-
fs: any
|
|
539
|
-
): Promise<void> {
|
|
540
|
-
const fileBuffer = fs.readFileSync(filePath);
|
|
541
|
-
|
|
542
|
-
const response = await fetch(presignedUrl, {
|
|
543
|
-
method: "PUT",
|
|
544
|
-
headers: {
|
|
545
|
-
"Content-Type": contentType,
|
|
546
|
-
"Content-Length": String(totalSize),
|
|
547
|
-
},
|
|
548
|
-
body: fileBuffer,
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
if (!response.ok) {
|
|
552
|
-
const errorText = await response.text();
|
|
553
|
-
throw new Error(
|
|
554
|
-
`Single-part upload failed (${response.status}): ${errorText}`
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (options.onProgress) {
|
|
559
|
-
options.onProgress(90);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Upload file using multipart upload (Node.js, private helper)
|
|
565
|
-
*/
|
|
566
|
-
private static async _uploadFileMultipart(
|
|
567
|
-
filePath: string,
|
|
568
|
-
partUrls: Array<{ part_number: number; url: string }>,
|
|
569
|
-
partSize: number,
|
|
570
|
-
totalSize: number,
|
|
571
|
-
options: UploadOptions,
|
|
572
|
-
fs: any
|
|
573
|
-
): Promise<void> {
|
|
574
|
-
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
575
|
-
|
|
576
|
-
const fileHandle = fs.openSync(filePath, "r");
|
|
577
|
-
try {
|
|
578
|
-
for (let i = 0; i < sortedParts.length; i++) {
|
|
579
|
-
const part = sortedParts[i];
|
|
580
|
-
const buffer = Buffer.alloc(partSize);
|
|
581
|
-
const offset = (part.part_number - 1) * partSize;
|
|
582
|
-
|
|
583
|
-
const bytesRead = fs.readSync(fileHandle, buffer, 0, partSize, offset);
|
|
584
|
-
const chunk = buffer.slice(0, bytesRead);
|
|
585
|
-
|
|
586
|
-
if (chunk.length === 0) {
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const response = await fetch(part.url, {
|
|
591
|
-
method: "PUT",
|
|
592
|
-
headers: {
|
|
593
|
-
"Content-Length": String(chunk.length),
|
|
594
|
-
},
|
|
595
|
-
body: chunk,
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
if (!response.ok) {
|
|
599
|
-
const errorText = await response.text();
|
|
600
|
-
throw new Error(
|
|
601
|
-
`Multipart upload failed at part ${part.part_number} ` +
|
|
602
|
-
`(${response.status}): ${errorText}`
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (options.onProgress) {
|
|
607
|
-
const progress = Math.min(
|
|
608
|
-
90,
|
|
609
|
-
Math.floor(((i + 1) / sortedParts.length) * 90)
|
|
610
|
-
);
|
|
611
|
-
options.onProgress(progress);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
} finally {
|
|
615
|
-
fs.closeSync(fileHandle);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Upload blob using single PUT request (browser, private helper)
|
|
621
|
-
*/
|
|
622
|
-
private static async _uploadBlobSingle(
|
|
623
|
-
blob: Blob,
|
|
624
|
-
presignedUrl: string,
|
|
625
|
-
contentType: string,
|
|
626
|
-
totalSize: number,
|
|
627
|
-
options: UploadOptions
|
|
628
|
-
): Promise<void> {
|
|
629
|
-
const response = await fetch(presignedUrl, {
|
|
630
|
-
method: "PUT",
|
|
631
|
-
headers: {
|
|
632
|
-
"Content-Type": contentType,
|
|
633
|
-
"Content-Length": String(totalSize),
|
|
634
|
-
},
|
|
635
|
-
body: blob,
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
if (!response.ok) {
|
|
639
|
-
const errorText = await response.text();
|
|
640
|
-
throw new Error(
|
|
641
|
-
`Single-part upload failed (${response.status}): ${errorText}`
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (options.onProgress) {
|
|
646
|
-
options.onProgress(90);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Upload blob using multipart upload (browser, private helper)
|
|
652
|
-
*/
|
|
653
|
-
private static async _uploadBlobMultipart(
|
|
654
|
-
blob: Blob,
|
|
655
|
-
partUrls: Array<{ part_number: number; url: string }>,
|
|
656
|
-
partSize: number,
|
|
657
|
-
totalSize: number,
|
|
658
|
-
options: UploadOptions
|
|
659
|
-
): Promise<void> {
|
|
660
|
-
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
661
|
-
|
|
662
|
-
for (let i = 0; i < sortedParts.length; i++) {
|
|
663
|
-
const part = sortedParts[i];
|
|
664
|
-
const start = (part.part_number - 1) * partSize;
|
|
665
|
-
const end = Math.min(start + partSize, totalSize);
|
|
666
|
-
const chunk = blob.slice(start, end);
|
|
667
|
-
|
|
668
|
-
if (chunk.size === 0) {
|
|
669
|
-
break;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const response = await fetch(part.url, {
|
|
673
|
-
method: "PUT",
|
|
674
|
-
headers: {
|
|
675
|
-
"Content-Length": String(chunk.size),
|
|
676
|
-
},
|
|
677
|
-
body: chunk,
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
if (!response.ok) {
|
|
681
|
-
const errorText = await response.text();
|
|
682
|
-
throw new Error(
|
|
683
|
-
`Multipart upload failed at part ${part.part_number} ` +
|
|
684
|
-
`(${response.status}): ${errorText}`
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (options.onProgress) {
|
|
689
|
-
const progress = Math.min(
|
|
690
|
-
90,
|
|
691
|
-
Math.floor(((i + 1) / sortedParts.length) * 90)
|
|
692
|
-
);
|
|
693
|
-
options.onProgress(progress);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export { YetterImageClient } from "./api.js";
|
|
2
|
-
export * from "./types.js";
|
|
3
|
-
export { yetter } from "./client.js";
|
|
4
|
-
|
|
5
|
-
// Explicitly export upload-related types for better discoverability
|
|
6
|
-
export type {
|
|
7
|
-
UploadOptions,
|
|
8
|
-
GetUploadUrlRequest,
|
|
9
|
-
GetUploadUrlResponse,
|
|
10
|
-
UploadCompleteRequest,
|
|
11
|
-
UploadCompleteResponse,
|
|
12
|
-
} from "./types.js";
|