@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.
- package/dist/events.js +42 -6
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|