@newhomestar/sdk 0.7.7 → 0.7.9

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.
Files changed (2) hide show
  1. package/dist/events.js +41 -3
  2. package/package.json +2 -1
package/dist/events.js CHANGED
@@ -31,6 +31,7 @@
31
31
  // NOVA_SERVICE_SLUG — Stamped as source_service on every event
32
32
  // =====================================================================
33
33
  import dotenv from 'dotenv';
34
+ import { Agent as UndiciAgent } from 'undici';
34
35
  // Load .env.local in dev if NOVA_EVENTS_SERVICE_URL not already set
35
36
  if (!process.env.NOVA_EVENTS_SERVICE_URL) {
36
37
  dotenv.config({ path: '.env.local', override: false });
@@ -774,6 +775,17 @@ export function startInboundConsumer(db, options) {
774
775
  reader.releaseLock();
775
776
  }
776
777
  }
778
+ // Dedicated undici Agent for SSE connections.
779
+ // Node.js's built-in fetch (undici) has a default bodyTimeout of 300s (5 minutes)
780
+ // which kills long-lived SSE streams as if the response body "stalled".
781
+ // Setting bodyTimeout: 0 disables this limit for the stream connection.
782
+ // headersTimeout: 30_000 still guards against slow initial connection.
783
+ const sseAgent = new UndiciAgent({
784
+ bodyTimeout: 0,
785
+ headersTimeout: 30_000,
786
+ keepAliveTimeout: 120_000,
787
+ keepAliveMaxTimeout: 600_000,
788
+ });
777
789
  /** Main loop: connect, consume, reconnect */
778
790
  async function _connect() {
779
791
  while (!abort.signal.aborted) {
@@ -784,6 +796,10 @@ export function startInboundConsumer(db, options) {
784
796
  const response = await fetch(streamUrl, {
785
797
  headers: { 'Authorization': `Bearer ${serviceToken}`, 'Accept': 'text/event-stream' },
786
798
  signal: abort.signal,
799
+ // @ts-ignore — undici-specific dispatcher: disables the 5-minute bodyTimeout
800
+ // that kills SSE streams. Standard fetch API does not expose this option,
801
+ // but Node.js's undici-powered fetch accepts it.
802
+ dispatcher: sseAgent,
787
803
  });
788
804
  if (!response.ok) {
789
805
  const text = await response.text().catch(() => '');
@@ -792,12 +808,33 @@ export function startInboundConsumer(db, options) {
792
808
  // Successfully connected — reset reconnect counter
793
809
  reconnectAttempt = 0;
794
810
  console.log(`[nova/events] SSE consumer connected to queue "${queueName}"`);
795
- await _consumeStream(response);
811
+ // Consume the stream; catch stream-level errors separately from
812
+ // connection-level errors so they don't trigger exponential backoff.
813
+ // A stream dying mid-flight (e.g. proxy timeout) is a normal lifecycle
814
+ // event — reconnect immediately with a short fixed delay.
815
+ try {
816
+ await _consumeStream(response);
817
+ }
818
+ catch (streamErr) {
819
+ if (abort.signal.aborted)
820
+ return;
821
+ const isAbortError = streamErr?.name === 'AbortError' || streamErr?.code === 20;
822
+ if (isAbortError)
823
+ return;
824
+ console.error(`[nova/events] SSE stream error for queue "${queueName}":`, String(streamErr).slice(0, 300));
825
+ }
796
826
  if (abort.signal.aborted) {
797
827
  console.log(`[nova/events] SSE consumer for queue "${queueName}" shut down gracefully`);
798
828
  return;
799
829
  }
800
- console.log(`[nova/events] SSE stream for queue "${queueName}" closed — reconnecting…`);
830
+ // Stream ended (normal close OR proxy timeout). The connection WAS
831
+ // established, so no exponential backoff — reconnect in a fixed 1s.
832
+ console.log(`[nova/events] SSE stream for queue "${queueName}" closed — reconnecting in 1000ms…`);
833
+ await new Promise((resolve) => {
834
+ const t = setTimeout(resolve, 1_000);
835
+ abort.signal.addEventListener('abort', () => { clearTimeout(t); resolve(); }, { once: true });
836
+ });
837
+ continue; // skip exponential backoff below
801
838
  }
802
839
  catch (err) {
803
840
  if (abort.signal.aborted) {
@@ -809,7 +846,8 @@ export function startInboundConsumer(db, options) {
809
846
  return;
810
847
  console.error(`[nova/events] SSE connection error for queue "${queueName}":`, String(err).slice(0, 300));
811
848
  }
812
- // Exponential backoff: 1s, 2s, 4s, 8s, ... capped at maxReconnectDelay
849
+ // Exponential backoff only for connection-level failures (fetch threw or
850
+ // server returned non-200). Stream-level drops use the fixed 1s above.
813
851
  const delayMs = Math.min(1_000 * Math.pow(2, reconnectAttempt), maxReconnectDelay);
814
852
  reconnectAttempt++;
815
853
  console.log(`[nova/events] Reconnecting queue "${queueName}" in ${delayMs}ms…`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newhomestar/sdk",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "Type-safe SDK for building Nova pipelines (workers & functions)",
5
5
  "homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
6
6
  "bugs": {
@@ -44,6 +44,7 @@
44
44
  "dotenv": "^16.4.3",
45
45
  "express": "^4.18.2",
46
46
  "express-oauth2-jwt-bearer": "^1.7.4",
47
+ "undici": "^7.24.4",
47
48
  "yaml": "^2.7.1"
48
49
  },
49
50
  "peerDependencies": {