@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 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
- 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) => {
315
+ if (options.disableSse) {
316
+ void startPollingFallback("SSE disabled by stream options.");
317
+ }
318
+ else {
319
+ let consecutiveSseTransportErrors = 0;
280
320
  try {
281
- const statusData = JSON.parse(event.data);
282
- controller.push(statusData);
321
+ eventSource = new EventSourcePolyfill(sseStreamUrl, {
322
+ headers: { Authorization: this.apiKey },
323
+ heartbeatTimeout: STREAM_HEARTBEAT_TIMEOUT_MS,
324
+ });
283
325
  }
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}`));
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
- 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
- }
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
- 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
- };
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yetter/client",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "tsc",