@newhomestar/sdk 0.7.8 → 0.7.10

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 +42 -6
  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 });
@@ -655,6 +656,14 @@ export function startInboundConsumer(db, options) {
655
656
  }
656
657
  const abort = new AbortController();
657
658
  let reconnectAttempt = 0;
659
+ /** Log nested error causes for rich diagnostics (undici stores ECONNREFUSED etc. in err.cause) */
660
+ function _logErrorCause(err, depth = 0) {
661
+ if (!err?.cause || depth > 3)
662
+ return;
663
+ const indent = ' ' + ' └─'.repeat(depth + 1) + ' ';
664
+ console.error(`[nova/events] ${indent}cause: ${String(err.cause).slice(0, 500)}`);
665
+ _logErrorCause(err.cause, depth + 1);
666
+ }
658
667
  /** Derive the event topic from a raw PGMQ message payload */
659
668
  function _deriveEventType(payload) {
660
669
  if (typeof payload.topic === 'string')
@@ -774,24 +783,45 @@ export function startInboundConsumer(db, options) {
774
783
  reader.releaseLock();
775
784
  }
776
785
  }
786
+ // Dedicated undici Agent for SSE connections.
787
+ // Node.js's built-in fetch (undici) has a default bodyTimeout of 300s (5 minutes)
788
+ // which kills long-lived SSE streams as if the response body "stalled".
789
+ // Setting bodyTimeout: 0 disables this limit for the stream connection.
790
+ // headersTimeout: 120_000 guards against slow initial connections (was 30s — too short
791
+ // for containers that may take time to establish TCP to the Events Service).
792
+ const sseAgent = new UndiciAgent({
793
+ bodyTimeout: 0,
794
+ headersTimeout: 120_000,
795
+ keepAliveTimeout: 120_000,
796
+ keepAliveMaxTimeout: 600_000,
797
+ });
777
798
  /** Main loop: connect, consume, reconnect */
778
799
  async function _connect() {
800
+ // Log config once on startup so container logs show what we're connecting to
801
+ const tokenSuffix = serviceToken ? `***${serviceToken.slice(-4)}` : 'MISSING';
802
+ console.log(`[nova/events] SSE consumer starting — url=${eventsUrl} ` +
803
+ `token=${tokenSuffix} queue=${queueName} vt=${vt}s headersTimeout=120s`);
779
804
  while (!abort.signal.aborted) {
780
805
  const streamUrl = `${eventsUrl}/events/queue/stream?queue=${encodeURIComponent(queueName)}&vt=${vt}`;
806
+ const connectStart = Date.now();
781
807
  try {
782
- console.log(`[nova/events] Connecting to SSE queue "${queueName}" ` +
783
- `(attempt ${reconnectAttempt + 1})…`);
808
+ console.log(`[nova/events] Connecting to SSE stream (attempt ${reconnectAttempt + 1}): ${streamUrl}`);
784
809
  const response = await fetch(streamUrl, {
785
810
  headers: { 'Authorization': `Bearer ${serviceToken}`, 'Accept': 'text/event-stream' },
786
811
  signal: abort.signal,
812
+ // @ts-ignore — undici-specific dispatcher: disables the 5-minute bodyTimeout
813
+ // that kills SSE streams. Standard fetch API does not expose this option,
814
+ // but Node.js's undici-powered fetch accepts it.
815
+ dispatcher: sseAgent,
787
816
  });
788
817
  if (!response.ok) {
789
818
  const text = await response.text().catch(() => '');
790
819
  throw new Error(`SSE connect failed: HTTP ${response.status} — ${text.slice(0, 200)}`);
791
820
  }
792
821
  // Successfully connected — reset reconnect counter
822
+ const ttfb = Date.now() - connectStart;
793
823
  reconnectAttempt = 0;
794
- console.log(`[nova/events] SSE consumer connected to queue "${queueName}"`);
824
+ console.log(`[nova/events] SSE consumer connected to queue "${queueName}" (TTFB: ${ttfb}ms)`);
795
825
  // Consume the stream; catch stream-level errors separately from
796
826
  // connection-level errors so they don't trigger exponential backoff.
797
827
  // A stream dying mid-flight (e.g. proxy timeout) is a normal lifecycle
@@ -805,7 +835,9 @@ export function startInboundConsumer(db, options) {
805
835
  const isAbortError = streamErr?.name === 'AbortError' || streamErr?.code === 20;
806
836
  if (isAbortError)
807
837
  return;
808
- console.error(`[nova/events] SSE stream error for queue "${queueName}":`, String(streamErr).slice(0, 300));
838
+ const elapsed = Date.now() - connectStart;
839
+ console.error(`[nova/events] SSE stream error for queue "${queueName}" (after ${elapsed}ms):`, String(streamErr).slice(0, 500));
840
+ _logErrorCause(streamErr);
809
841
  }
810
842
  if (abort.signal.aborted) {
811
843
  console.log(`[nova/events] SSE consumer for queue "${queueName}" shut down gracefully`);
@@ -813,7 +845,9 @@ export function startInboundConsumer(db, options) {
813
845
  }
814
846
  // Stream ended (normal close OR proxy timeout). The connection WAS
815
847
  // established, so no exponential backoff — reconnect in a fixed 1s.
816
- console.log(`[nova/events] SSE stream for queue "${queueName}" closed — reconnecting in 1000ms…`);
848
+ const totalDuration = Date.now() - connectStart;
849
+ console.log(`[nova/events] SSE stream for queue "${queueName}" closed after ${totalDuration}ms — ` +
850
+ `reconnecting in 1000ms…`);
817
851
  await new Promise((resolve) => {
818
852
  const t = setTimeout(resolve, 1_000);
819
853
  abort.signal.addEventListener('abort', () => { clearTimeout(t); resolve(); }, { once: true });
@@ -828,7 +862,9 @@ export function startInboundConsumer(db, options) {
828
862
  const isAbortError = err?.name === 'AbortError' || err?.code === 20;
829
863
  if (isAbortError)
830
864
  return;
831
- console.error(`[nova/events] SSE connection error for queue "${queueName}":`, String(err).slice(0, 300));
865
+ const elapsed = Date.now() - connectStart;
866
+ console.error(`[nova/events] SSE connection error for queue "${queueName}" (failed after ${elapsed}ms):`, String(err).slice(0, 500));
867
+ _logErrorCause(err);
832
868
  }
833
869
  // Exponential backoff only for connection-level failures (fetch threw or
834
870
  // server returned non-200). Stream-level drops use the fixed 1s above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newhomestar/sdk",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
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": {