@yetter/client 0.0.13 → 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/README.md +7 -0
- package/dist/client.js +140 -71
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,11 +146,18 @@ This function submits an image generation request and returns an async iterable
|
|
|
146
146
|
**Features:**
|
|
147
147
|
|
|
148
148
|
* Initiates a request and establishes an SSE connection.
|
|
149
|
+
* Automatically falls back to status polling if SSE transport repeatedly fails.
|
|
149
150
|
* Provides an `AsyncIterator` (`Symbol.asyncIterator`) to loop through status events (`StreamEvent`).
|
|
150
151
|
* A `done()` method: Returns a Promise that resolves with the final `GetResponseResponse` **after the server emits `event: done`** (successful completion), or rejects if the server emits `event: error` or the final response cannot be fetched.
|
|
151
152
|
* A `cancel()` method: Requests cancellation on the backend; the stream naturally ends when the server emits `data` (with `status: "CANCELLED"`) followed by `event: done`.
|
|
152
153
|
* A `getRequestId()` method: Returns the request ID for the stream.
|
|
153
154
|
|
|
155
|
+
**Transport options (`StreamOptions`):**
|
|
156
|
+
|
|
157
|
+
* `disableSse?: boolean` - skip SSE and use polling only.
|
|
158
|
+
* `pollIntervalMs?: number` - polling interval for fallback/forced polling (default: `2000`).
|
|
159
|
+
* `sseMaxConsecutiveErrors?: number` - number of transport errors tolerated before fallback (default: `3`).
|
|
160
|
+
|
|
154
161
|
**Events (`StreamEvent`):**
|
|
155
162
|
Each event pushed by the stream is an object typically including:
|
|
156
163
|
|
package/dist/client.js
CHANGED
|
@@ -4,6 +4,7 @@ import { EventSourcePolyfill } from "event-source-polyfill";
|
|
|
4
4
|
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
5
5
|
const DEFAULT_UPLOAD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
6
6
|
const STREAM_HEARTBEAT_TIMEOUT_MS = 120000;
|
|
7
|
+
const DEFAULT_STREAM_SSE_MAX_CONSECUTIVE_ERRORS = 3;
|
|
7
8
|
const DEFAULT_ENDPOINT = "https://api.yetter.ai";
|
|
8
9
|
function sleep(ms) {
|
|
9
10
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -173,6 +174,14 @@ export class YetterClient {
|
|
|
173
174
|
const statusUrl = initialApiResponse.status_url;
|
|
174
175
|
const cancelUrl = initialApiResponse.cancel_url;
|
|
175
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;
|
|
176
185
|
let eventSource;
|
|
177
186
|
let resolveDonePromise;
|
|
178
187
|
let rejectDonePromise;
|
|
@@ -255,6 +264,40 @@ export class YetterClient {
|
|
|
255
264
|
return new Promise((resolve) => this.resolvers.push(resolve));
|
|
256
265
|
},
|
|
257
266
|
};
|
|
267
|
+
let hasStartedFallbackPolling = false;
|
|
268
|
+
const terminalStatuses = new Set(["COMPLETED", "ERROR", "CANCELLED"]);
|
|
269
|
+
const startPollingFallback = async (reason) => {
|
|
270
|
+
if (controller.isClosed || hasStartedFallbackPolling) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
hasStartedFallbackPolling = true;
|
|
274
|
+
try {
|
|
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;
|
|
291
|
+
}
|
|
292
|
+
await sleep(streamPollIntervalMs);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
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)}`));
|
|
299
|
+
}
|
|
300
|
+
};
|
|
258
301
|
if (initialApiResponse.status === "COMPLETED") {
|
|
259
302
|
controller.push(initialApiResponse);
|
|
260
303
|
const finalResponse = await client.getResponse({ url: responseUrl });
|
|
@@ -269,81 +312,107 @@ export class YetterClient {
|
|
|
269
312
|
controller.cancel();
|
|
270
313
|
}
|
|
271
314
|
else {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
console.log("SSE Connection Opened:", event);
|
|
278
|
-
};
|
|
279
|
-
eventSource.addEventListener("data", (event) => {
|
|
315
|
+
if (options.disableSse) {
|
|
316
|
+
void startPollingFallback("SSE disabled by stream options.");
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
let consecutiveSseTransportErrors = 0;
|
|
280
320
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
eventSource = new EventSourcePolyfill(sseStreamUrl, {
|
|
322
|
+
headers: { Authorization: this.apiKey },
|
|
323
|
+
heartbeatTimeout: STREAM_HEARTBEAT_TIMEOUT_MS,
|
|
324
|
+
});
|
|
283
325
|
}
|
|
284
|
-
catch (
|
|
285
|
-
console.
|
|
286
|
-
|
|
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)}`);
|
|
287
329
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
};
|
|
320
414
|
}
|
|
321
|
-
|
|
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
|
-
};
|
|
415
|
+
}
|
|
347
416
|
}
|
|
348
417
|
return {
|
|
349
418
|
async *[Symbol.asyncIterator]() {
|
package/dist/types.d.ts
CHANGED
|
@@ -79,6 +79,21 @@ export interface StatusResponse {
|
|
|
79
79
|
}
|
|
80
80
|
export interface StreamOptions {
|
|
81
81
|
input: Omit<GenerateRequest, 'model'>;
|
|
82
|
+
/**
|
|
83
|
+
* Disable SSE transport and use status polling only.
|
|
84
|
+
* Useful when running behind proxies/CDNs that break long-lived SSE over HTTP/2.
|
|
85
|
+
*/
|
|
86
|
+
disableSse?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Poll interval used by fallback polling (or when disableSse=true).
|
|
89
|
+
* Default: 2000ms
|
|
90
|
+
*/
|
|
91
|
+
pollIntervalMs?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Number of consecutive SSE transport errors tolerated before switching to polling.
|
|
94
|
+
* Default: 3
|
|
95
|
+
*/
|
|
96
|
+
sseMaxConsecutiveErrors?: number;
|
|
82
97
|
}
|
|
83
98
|
/**
|
|
84
99
|
* Represents an event received from the SSE stream.
|