@yetter/client 0.0.11 → 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 +121 -17
- package/dist/api.d.ts +16 -1
- package/dist/api.js +130 -40
- package/dist/client.d.ts +30 -3
- package/dist/client.js +412 -156
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +63 -0
- package/package.json +9 -2
- package/examples/stream.ts +0 -54
- package/examples/submit.ts +0 -80
- package/examples/subscribe.ts +0 -41
- package/src/api.ts +0 -105
- package/src/client.ts +0 -338
- package/src/index.ts +0 -3
- package/src/types.ts +0 -116
- package/tsconfig.json +0 -13
package/dist/client.js
CHANGED
|
@@ -1,38 +1,127 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { YetterImageClient } from "./api.js";
|
|
3
|
-
import { EventSourcePolyfill } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
107
|
+
this.endpoint = options.endpoint;
|
|
16
108
|
}
|
|
17
109
|
}
|
|
18
|
-
|
|
110
|
+
async subscribe(model, options) {
|
|
19
111
|
var _b;
|
|
20
|
-
|
|
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
|
|
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;
|
|
35
|
-
|
|
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;
|
|
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.`);
|
|
38
127
|
try {
|
|
@@ -53,40 +142,38 @@ 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);
|
|
59
151
|
throw error;
|
|
60
152
|
}
|
|
61
153
|
}
|
|
62
|
-
if (status === "
|
|
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.";
|
|
154
|
+
if (status === "ERROR") {
|
|
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
|
-
|
|
158
|
+
if (status === "CANCELLED") {
|
|
159
|
+
throw new Error("Image generation was cancelled by user.");
|
|
160
|
+
}
|
|
161
|
+
return client.getResponse({
|
|
67
162
|
url: generateResponse.response_url,
|
|
68
163
|
});
|
|
69
|
-
return finalResponse;
|
|
70
164
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
74
|
-
}
|
|
75
|
-
const client = new YetterImageClient({
|
|
76
|
-
apiKey: _a.apiKey,
|
|
77
|
-
endpoint: _a.endpoint
|
|
78
|
-
});
|
|
165
|
+
async stream(model, options) {
|
|
166
|
+
const client = this.createApiClient();
|
|
79
167
|
const initialApiResponse = await client.generate({
|
|
80
|
-
model
|
|
168
|
+
model,
|
|
81
169
|
...options.input,
|
|
82
170
|
});
|
|
83
171
|
const requestId = initialApiResponse.request_id;
|
|
84
172
|
const responseUrl = initialApiResponse.response_url;
|
|
173
|
+
const statusUrl = initialApiResponse.status_url;
|
|
85
174
|
const cancelUrl = initialApiResponse.cancel_url;
|
|
86
175
|
const sseStreamUrl = `${client.getApiEndpoint()}/${model}/requests/${requestId}/status/stream`;
|
|
87
176
|
let eventSource;
|
|
88
|
-
let streamEnded = false;
|
|
89
|
-
// Setup the promise for the done() method
|
|
90
177
|
let resolveDonePromise;
|
|
91
178
|
let rejectDonePromise;
|
|
92
179
|
const donePromise = new Promise((resolve, reject) => {
|
|
@@ -94,13 +181,24 @@ export class yetter {
|
|
|
94
181
|
rejectDonePromise = reject;
|
|
95
182
|
});
|
|
96
183
|
const controller = {
|
|
97
|
-
// This will be used by the async iterator to pull events
|
|
98
|
-
// It needs a way to buffer events or signal availability
|
|
99
184
|
events: [],
|
|
100
185
|
resolvers: [],
|
|
101
186
|
isClosed: false,
|
|
187
|
+
isSettled: false,
|
|
188
|
+
currentStatus: "",
|
|
189
|
+
callResolver(value) {
|
|
190
|
+
if (this.isSettled)
|
|
191
|
+
return;
|
|
192
|
+
this.isSettled = true;
|
|
193
|
+
resolveDonePromise(value);
|
|
194
|
+
},
|
|
195
|
+
callRejecter(reason) {
|
|
196
|
+
if (this.isSettled)
|
|
197
|
+
return;
|
|
198
|
+
this.isSettled = true;
|
|
199
|
+
rejectDonePromise(reason);
|
|
200
|
+
},
|
|
102
201
|
push(event) {
|
|
103
|
-
var _b;
|
|
104
202
|
if (this.isClosed)
|
|
105
203
|
return;
|
|
106
204
|
if (this.resolvers.length > 0) {
|
|
@@ -109,54 +207,43 @@ export class yetter {
|
|
|
109
207
|
else {
|
|
110
208
|
this.events.push(event);
|
|
111
209
|
}
|
|
112
|
-
|
|
113
|
-
if (event.status === "COMPLETED") {
|
|
114
|
-
streamEnded = true;
|
|
115
|
-
client.getResponse({ url: responseUrl })
|
|
116
|
-
.then(resolveDonePromise)
|
|
117
|
-
.catch(rejectDonePromise)
|
|
118
|
-
.finally(() => this.close());
|
|
119
|
-
}
|
|
120
|
-
else if (event.status === "FAILED") {
|
|
121
|
-
streamEnded = true;
|
|
122
|
-
rejectDonePromise(new Error(((_b = event.logs) === null || _b === void 0 ? void 0 : _b.map(l => l.message).join('\n')) || `Stream reported FAILED for ${requestId}`));
|
|
123
|
-
this.close();
|
|
124
|
-
}
|
|
210
|
+
this.currentStatus = event.status;
|
|
125
211
|
},
|
|
126
212
|
error(err) {
|
|
127
213
|
if (this.isClosed)
|
|
128
214
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// This error is likely a secondary effect of the connection closing.
|
|
132
|
-
if (!streamEnded) {
|
|
133
|
-
rejectDonePromise(err); // Only reject if no terminal event was processed
|
|
215
|
+
if (this.resolvers.length > 0) {
|
|
216
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
134
217
|
}
|
|
135
|
-
|
|
136
|
-
|
|
218
|
+
this._close();
|
|
219
|
+
const errorToReport = err instanceof Error ? err : new Error("Stream closed prematurely or unexpectedly.");
|
|
220
|
+
this.callRejecter(errorToReport);
|
|
221
|
+
},
|
|
222
|
+
done(value) {
|
|
223
|
+
if (this.isClosed)
|
|
224
|
+
return;
|
|
225
|
+
if (this.resolvers.length > 0) {
|
|
226
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
137
227
|
}
|
|
138
|
-
|
|
228
|
+
this._close();
|
|
229
|
+
this.callResolver(value);
|
|
230
|
+
},
|
|
231
|
+
cancel() {
|
|
232
|
+
if (this.isClosed)
|
|
233
|
+
return;
|
|
139
234
|
if (this.resolvers.length > 0) {
|
|
140
|
-
this.resolvers.shift()({ value: undefined, done: true });
|
|
235
|
+
this.resolvers.shift()({ value: undefined, done: true });
|
|
141
236
|
}
|
|
142
|
-
this.
|
|
237
|
+
this._close();
|
|
238
|
+
this.callRejecter(new Error("Stream was cancelled by user."));
|
|
143
239
|
},
|
|
144
|
-
|
|
240
|
+
_close() {
|
|
145
241
|
if (this.isClosed)
|
|
146
242
|
return;
|
|
147
243
|
this.isClosed = true;
|
|
148
|
-
this.resolvers.forEach(resolve => resolve({ value: undefined, done: true }));
|
|
244
|
+
this.resolvers.forEach((resolve) => resolve({ value: undefined, done: true }));
|
|
149
245
|
this.resolvers = [];
|
|
150
246
|
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
151
|
-
// If donePromise is still pending, reject it as stream closed prematurely
|
|
152
|
-
// Use a timeout to allow any final event processing for COMPLETED/FAILED to settle it first
|
|
153
|
-
setTimeout(() => {
|
|
154
|
-
donePromise.catch(() => { }).finally(() => {
|
|
155
|
-
if (!streamEnded) { // If not explicitly completed or failed
|
|
156
|
-
rejectDonePromise(new Error("Stream closed prematurely or unexpectedly."));
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}, 100);
|
|
160
247
|
},
|
|
161
248
|
next() {
|
|
162
249
|
if (this.events.length > 0) {
|
|
@@ -165,46 +252,98 @@ export class yetter {
|
|
|
165
252
|
if (this.isClosed) {
|
|
166
253
|
return Promise.resolve({ value: undefined, done: true });
|
|
167
254
|
}
|
|
168
|
-
return new Promise(resolve => this.resolvers.push(resolve));
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
172
|
-
headers: { 'Authorization': `${_a.apiKey}` },
|
|
173
|
-
});
|
|
174
|
-
eventSource.onopen = (event) => {
|
|
175
|
-
console.log("SSE Connection Opened:", event);
|
|
176
|
-
};
|
|
177
|
-
eventSource.addEventListener('data', (event) => {
|
|
178
|
-
// console.log("SSE 'data' event received, raw data:", event.data);
|
|
179
|
-
try {
|
|
180
|
-
const statusData = JSON.parse(event.data);
|
|
181
|
-
controller.push(statusData);
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
console.error("Error parsing SSE 'data' event:", e, "Raw data:", event.data);
|
|
185
|
-
controller.error(new Error(`Error parsing SSE 'data' event: ${e.message}`));
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
eventSource.addEventListener('done', (event) => {
|
|
189
|
-
// console.log("SSE 'done' event received, raw data:", event.data);
|
|
190
|
-
controller.close();
|
|
191
|
-
});
|
|
192
|
-
eventSource.onerror = (err) => {
|
|
193
|
-
var _b;
|
|
194
|
-
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) || '';
|
|
195
|
-
const isIdleTimeout = typeof message === 'string' && message.includes('No activity within') && message.includes('Reconnecting');
|
|
196
|
-
if (isIdleTimeout) {
|
|
197
|
-
console.warn("SSE idle timeout; letting EventSource auto-reconnect.", err);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
console.warn("SSE Connection Error (onerror) - will allow auto-reconnect:", err);
|
|
255
|
+
return new Promise((resolve) => this.resolvers.push(resolve));
|
|
256
|
+
},
|
|
201
257
|
};
|
|
202
|
-
// Handle if API immediately returns a terminal status in initialApiResponse (e.g. already completed/failed)
|
|
203
258
|
if (initialApiResponse.status === "COMPLETED") {
|
|
204
259
|
controller.push(initialApiResponse);
|
|
260
|
+
const finalResponse = await client.getResponse({ url: responseUrl });
|
|
261
|
+
controller.done(finalResponse);
|
|
205
262
|
}
|
|
206
|
-
else if (initialApiResponse.status === "
|
|
263
|
+
else if (initialApiResponse.status === "ERROR") {
|
|
207
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 {
|
|
291
|
+
try {
|
|
292
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
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
|
+
}
|
|
320
|
+
}
|
|
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}`));
|
|
324
|
+
}
|
|
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
|
+
};
|
|
208
347
|
}
|
|
209
348
|
return {
|
|
210
349
|
async *[Symbol.asyncIterator]() {
|
|
@@ -217,7 +356,6 @@ export class yetter {
|
|
|
217
356
|
},
|
|
218
357
|
done: () => donePromise,
|
|
219
358
|
cancel: async () => {
|
|
220
|
-
controller.close();
|
|
221
359
|
try {
|
|
222
360
|
await client.cancel({ url: cancelUrl });
|
|
223
361
|
console.log(`Stream for ${requestId} - underlying request cancelled.`);
|
|
@@ -225,63 +363,181 @@ export class yetter {
|
|
|
225
363
|
catch (e) {
|
|
226
364
|
console.error(`Error cancelling underlying request for stream ${requestId}:`, e.message);
|
|
227
365
|
}
|
|
228
|
-
// Ensure donePromise is settled if not already
|
|
229
|
-
if (!streamEnded) {
|
|
230
|
-
rejectDonePromise(new Error("Stream was cancelled by user."));
|
|
231
|
-
}
|
|
232
366
|
},
|
|
233
367
|
getRequestId: () => requestId,
|
|
234
368
|
};
|
|
235
369
|
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
yetter.queue = {
|
|
241
|
-
submit: async (model, options) => {
|
|
242
|
-
if (!_a.apiKey) {
|
|
243
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
370
|
+
async uploadFile(fileOrPath, options = {}) {
|
|
371
|
+
this.assertApiKeyConfigured();
|
|
372
|
+
if (typeof fileOrPath === "string") {
|
|
373
|
+
return this._uploadFromPath(fileOrPath, options);
|
|
244
374
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
375
|
+
return this._uploadFromBlob(fileOrPath, options);
|
|
376
|
+
}
|
|
377
|
+
async uploadBlob(file, options = {}) {
|
|
378
|
+
return this.uploadFile(file, options);
|
|
379
|
+
}
|
|
380
|
+
async _uploadFromPath(filePath, options) {
|
|
381
|
+
const fs = await import("fs");
|
|
382
|
+
const mime = await import("mime-types");
|
|
383
|
+
if (!fs.existsSync(filePath)) {
|
|
384
|
+
throw new Error(`File not found: ${filePath}`);
|
|
385
|
+
}
|
|
386
|
+
const stats = fs.statSync(filePath);
|
|
387
|
+
const fileSize = stats.size;
|
|
388
|
+
const fileName = filePath.split(/[\\/]/).pop() || "upload";
|
|
389
|
+
const mimeType = mime.default.lookup(filePath) || "application/octet-stream";
|
|
390
|
+
const uploadTimeoutMs = this.getUploadTimeoutMs(options);
|
|
391
|
+
const client = this.createApiClient();
|
|
392
|
+
const uploadUrlResponse = await client.getUploadUrl({
|
|
393
|
+
file_name: fileName,
|
|
394
|
+
content_type: mimeType,
|
|
395
|
+
size: fileSize,
|
|
248
396
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
397
|
+
if (uploadUrlResponse.mode === "single") {
|
|
398
|
+
await this._uploadFileSingle(filePath, uploadUrlResponse.put_url, mimeType, fileSize, options, fs, uploadTimeoutMs);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
await this._uploadFileMultipart(filePath, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, fs, uploadTimeoutMs);
|
|
402
|
+
}
|
|
403
|
+
const result = await client.uploadComplete({
|
|
404
|
+
key: uploadUrlResponse.key,
|
|
252
405
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
status: async (model, options) => {
|
|
256
|
-
if (!_a.apiKey) {
|
|
257
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
406
|
+
if (options.onProgress) {
|
|
407
|
+
options.onProgress(100);
|
|
258
408
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
async _uploadFromBlob(file, options) {
|
|
412
|
+
const fileSize = file.size;
|
|
413
|
+
const fileName = options.filename || (file instanceof File ? file.name : "blob-upload");
|
|
414
|
+
const mimeType = file.type || "application/octet-stream";
|
|
415
|
+
const uploadTimeoutMs = this.getUploadTimeoutMs(options);
|
|
416
|
+
const client = this.createApiClient();
|
|
417
|
+
const uploadUrlResponse = await client.getUploadUrl({
|
|
418
|
+
file_name: fileName,
|
|
419
|
+
content_type: mimeType,
|
|
420
|
+
size: fileSize,
|
|
262
421
|
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const statusData = await client.getStatus({ url: statusUrl });
|
|
266
|
-
return {
|
|
267
|
-
data: statusData,
|
|
268
|
-
requestId: options.requestId,
|
|
269
|
-
};
|
|
270
|
-
},
|
|
271
|
-
result: async (model, options) => {
|
|
272
|
-
if (!_a.apiKey) {
|
|
273
|
-
throw new Error("API key is not configured. Call yetter.configure({ apiKey: 'your_key' }) or set YTR_API_KEY.");
|
|
422
|
+
if (uploadUrlResponse.mode === "single") {
|
|
423
|
+
await this._uploadBlobSingle(file, uploadUrlResponse.put_url, mimeType, fileSize, options, uploadTimeoutMs);
|
|
274
424
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
425
|
+
else {
|
|
426
|
+
await this._uploadBlobMultipart(file, uploadUrlResponse.part_urls, uploadUrlResponse.part_size, fileSize, options, uploadTimeoutMs);
|
|
427
|
+
}
|
|
428
|
+
const result = await client.uploadComplete({
|
|
429
|
+
key: uploadUrlResponse.key,
|
|
278
430
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
431
|
+
if (options.onProgress) {
|
|
432
|
+
options.onProgress(100);
|
|
433
|
+
}
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
async _uploadFileSingle(filePath, presignedUrl, contentType, totalSize, options, fs, timeoutMs) {
|
|
437
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
438
|
+
const response = await this.putWithTimeout(presignedUrl, fileBuffer, {
|
|
439
|
+
"Content-Type": contentType,
|
|
440
|
+
"Content-Length": String(totalSize),
|
|
441
|
+
}, timeoutMs);
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
const errorText = await response.text();
|
|
444
|
+
throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
|
|
445
|
+
}
|
|
446
|
+
if (options.onProgress) {
|
|
447
|
+
options.onProgress(90);
|
|
448
|
+
}
|
|
286
449
|
}
|
|
450
|
+
async _uploadFileMultipart(filePath, partUrls, partSize, totalSize, options, fs, timeoutMs) {
|
|
451
|
+
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
452
|
+
const fileHandle = fs.openSync(filePath, "r");
|
|
453
|
+
try {
|
|
454
|
+
for (let i = 0; i < sortedParts.length; i++) {
|
|
455
|
+
const part = sortedParts[i];
|
|
456
|
+
const buffer = Buffer.alloc(partSize);
|
|
457
|
+
const offset = (part.part_number - 1) * partSize;
|
|
458
|
+
const bytesRead = fs.readSync(fileHandle, buffer, 0, partSize, offset);
|
|
459
|
+
const chunk = buffer.slice(0, bytesRead);
|
|
460
|
+
if (chunk.length === 0) {
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
const response = await this.putWithTimeout(part.url, chunk, {
|
|
464
|
+
"Content-Length": String(chunk.length),
|
|
465
|
+
}, timeoutMs);
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
const errorText = await response.text();
|
|
468
|
+
throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
|
|
469
|
+
}
|
|
470
|
+
if (options.onProgress) {
|
|
471
|
+
const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
|
|
472
|
+
options.onProgress(progress);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
fs.closeSync(fileHandle);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async _uploadBlobSingle(blob, presignedUrl, contentType, _totalSize, options, timeoutMs) {
|
|
481
|
+
const response = await this.putWithTimeout(presignedUrl, blob, {
|
|
482
|
+
"Content-Type": contentType,
|
|
483
|
+
}, timeoutMs);
|
|
484
|
+
if (!response.ok) {
|
|
485
|
+
const errorText = await response.text();
|
|
486
|
+
throw new Error(`Single-part upload failed (${response.status}): ${errorText}`);
|
|
487
|
+
}
|
|
488
|
+
if (options.onProgress) {
|
|
489
|
+
options.onProgress(90);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async _uploadBlobMultipart(blob, partUrls, partSize, totalSize, options, timeoutMs) {
|
|
493
|
+
const sortedParts = [...partUrls].sort((a, b) => a.part_number - b.part_number);
|
|
494
|
+
for (let i = 0; i < sortedParts.length; i++) {
|
|
495
|
+
const part = sortedParts[i];
|
|
496
|
+
const start = (part.part_number - 1) * partSize;
|
|
497
|
+
const end = Math.min(start + partSize, totalSize);
|
|
498
|
+
const chunk = blob.slice(start, end);
|
|
499
|
+
if (chunk.size === 0) {
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
const response = await this.putWithTimeout(part.url, chunk, {}, timeoutMs);
|
|
503
|
+
if (!response.ok) {
|
|
504
|
+
const errorText = await response.text();
|
|
505
|
+
throw new Error(`Multipart upload failed at part ${part.part_number} (${response.status}): ${errorText}`);
|
|
506
|
+
}
|
|
507
|
+
if (options.onProgress) {
|
|
508
|
+
const progress = Math.min(90, Math.floor(((i + 1) / sortedParts.length) * 90));
|
|
509
|
+
options.onProgress(progress);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
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
|
+
}
|
|
531
|
+
_a = yetter;
|
|
532
|
+
yetter.client = new YetterClient();
|
|
533
|
+
yetter.queue = {
|
|
534
|
+
submit: async (model, options) => {
|
|
535
|
+
return _a.client.queue.submit(model, options);
|
|
536
|
+
},
|
|
537
|
+
status: async (model, options) => {
|
|
538
|
+
return _a.client.queue.status(model, options);
|
|
539
|
+
},
|
|
540
|
+
result: async (model, options) => {
|
|
541
|
+
return _a.client.queue.result(model, options);
|
|
542
|
+
},
|
|
287
543
|
};
|