@wavehouse/sdk 0.0.0-dev.h70dbf5d8f5ed → 0.0.0-dev.h7362ab3863f6
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 +5 -5
- package/dist/index.cjs +359 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -23
- package/dist/index.d.ts +74 -23
- package/dist/index.global.js +11 -2
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +359 -59
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -66,6 +66,13 @@ function mergeHeaders(base, extra) {
|
|
|
66
66
|
}
|
|
67
67
|
return merged;
|
|
68
68
|
}
|
|
69
|
+
function abortedResult() {
|
|
70
|
+
return {
|
|
71
|
+
data: null,
|
|
72
|
+
error: { status: 0, code: "ABORTED", message: "Request aborted", retryable: false },
|
|
73
|
+
headers: new Headers()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
69
76
|
async function request(ctx, opts) {
|
|
70
77
|
const url = resolveURL(ctx.baseURL, opts.path, opts.params).toString();
|
|
71
78
|
const headers = {
|
|
@@ -115,16 +122,19 @@ async function request(ctx, opts) {
|
|
|
115
122
|
}
|
|
116
123
|
return { data: null, error, headers: res.headers };
|
|
117
124
|
} catch (e) {
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
120
|
-
data: null,
|
|
121
|
-
error: { status: 0, code: "ABORTED", message: "Request aborted", retryable: false },
|
|
122
|
-
headers: new Headers()
|
|
123
|
-
};
|
|
125
|
+
if (opts.signal?.aborted) {
|
|
126
|
+
return abortedResult();
|
|
124
127
|
}
|
|
125
128
|
lastError = networkError(e);
|
|
126
129
|
if (attempt < maxAttempts - 1) {
|
|
127
|
-
|
|
130
|
+
try {
|
|
131
|
+
await sleep(backoff(attempt), opts.signal);
|
|
132
|
+
} catch (abort) {
|
|
133
|
+
if (opts.signal?.aborted) {
|
|
134
|
+
return abortedResult();
|
|
135
|
+
}
|
|
136
|
+
throw abort;
|
|
137
|
+
}
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
140
|
}
|
|
@@ -518,11 +528,50 @@ var StreamController = class {
|
|
|
518
528
|
};
|
|
519
529
|
|
|
520
530
|
// src/stream/sse.ts
|
|
531
|
+
import { createParser } from "eventsource-parser";
|
|
521
532
|
var activeSSEConnections = 0;
|
|
522
533
|
var SSE_WARN_THRESHOLD = 5;
|
|
534
|
+
var MAX_BUFFER_CHARS = 16 * 1024 * 1024;
|
|
535
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
536
|
+
var STABLE_CONNECTION_MS = 3e3;
|
|
537
|
+
function backoff2(attempt, retryFloorMs) {
|
|
538
|
+
const nominal = Math.max(retryFloorMs, Math.min(1e3 * 2 ** attempt, MAX_BACKOFF_MS));
|
|
539
|
+
const low = Math.max(retryFloorMs, nominal / 2);
|
|
540
|
+
return low + Math.random() * (nominal - low);
|
|
541
|
+
}
|
|
542
|
+
function releaseBody(res) {
|
|
543
|
+
if (res.body && !res.body.locked) res.body.cancel().catch(() => {
|
|
544
|
+
});
|
|
545
|
+
}
|
|
523
546
|
var SSETransport = class {
|
|
524
547
|
_opts;
|
|
525
|
-
|
|
548
|
+
_abort = null;
|
|
549
|
+
/**
|
|
550
|
+
* Set by `disconnect()` before it calls `abort()` — both synchronously — so
|
|
551
|
+
* a rejection caused by our own abort always observes this as `true`. That
|
|
552
|
+
* makes it a complete and precise test for "did we cause this?", which is
|
|
553
|
+
* why the catch blocks read it directly instead of matching on the error
|
|
554
|
+
* type: an `AbortError` we *didn't* raise (a custom `fetch` enforcing a
|
|
555
|
+
* per-attempt deadline, an `auth()` call cancelled by its own controller) is
|
|
556
|
+
* a transient failure to re-dial, exactly as on the REST path. Treating one
|
|
557
|
+
* as a teardown killed the stream permanently, and silently.
|
|
558
|
+
*/
|
|
559
|
+
_closed = false;
|
|
560
|
+
_started = false;
|
|
561
|
+
_counted = false;
|
|
562
|
+
/**
|
|
563
|
+
* Most recent non-empty event id, replayed as `Last-Event-ID` on reconnect.
|
|
564
|
+
*
|
|
565
|
+
* Deliberately ignores empty ids. The server emits a blank `id:` line for
|
|
566
|
+
* passthrough payloads, and per the SSE spec an empty `id` field *clears* the
|
|
567
|
+
* last-event-id — so tracking it faithfully would silently discard the
|
|
568
|
+
* resumption point mid-stream and re-open from the beginning.
|
|
569
|
+
*/
|
|
570
|
+
_lastEventId = null;
|
|
571
|
+
/** Reconnect floor most recently requested by the server via `retry:`. */
|
|
572
|
+
_retryFloorMs = 0;
|
|
573
|
+
/** Cuts a reconnect gap short when set — see `_sleep`. */
|
|
574
|
+
_wake = null;
|
|
526
575
|
onEvent = null;
|
|
527
576
|
onStatus = null;
|
|
528
577
|
onError = null;
|
|
@@ -530,79 +579,332 @@ var SSETransport = class {
|
|
|
530
579
|
this._opts = opts;
|
|
531
580
|
}
|
|
532
581
|
connect() {
|
|
533
|
-
if (
|
|
582
|
+
if (this._closed || this._started) return;
|
|
583
|
+
this._started = true;
|
|
584
|
+
if (!this._opts.fetch && typeof fetch === "undefined") {
|
|
534
585
|
throw new Error(
|
|
535
|
-
"[wavehouse]
|
|
586
|
+
"[wavehouse] global fetch is not available in this environment. Upgrade to Node 22+ or supply one via `options.fetch`."
|
|
536
587
|
);
|
|
537
588
|
}
|
|
538
|
-
|
|
539
|
-
|
|
589
|
+
activeSSEConnections++;
|
|
590
|
+
this._counted = true;
|
|
591
|
+
if (activeSSEConnections > SSE_WARN_THRESHOLD) {
|
|
592
|
+
console.warn(
|
|
593
|
+
`[wavehouse] ${activeSSEConnections} SSE connections open. Browsers limit HTTP/1.1 to 6 connections per domain.`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
this._run().catch((err2) => {
|
|
597
|
+
this._emitError({
|
|
540
598
|
status: 0,
|
|
541
599
|
code: "SSE_CONNECT_ERROR",
|
|
542
600
|
message: err2 instanceof Error ? err2.message : String(err2),
|
|
543
|
-
retryable:
|
|
601
|
+
retryable: false
|
|
544
602
|
});
|
|
603
|
+
this.disconnect();
|
|
545
604
|
});
|
|
546
605
|
}
|
|
547
606
|
disconnect() {
|
|
548
|
-
if (this.
|
|
549
|
-
this.
|
|
550
|
-
this.
|
|
551
|
-
|
|
607
|
+
if (!this._closed) {
|
|
608
|
+
this._closed = true;
|
|
609
|
+
this._abort?.abort();
|
|
610
|
+
this._abort = null;
|
|
611
|
+
this._wake?.();
|
|
612
|
+
if (this._counted) {
|
|
613
|
+
activeSSEConnections = Math.max(0, activeSSEConnections - 1);
|
|
614
|
+
this._counted = false;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
this.onStatus?.("closed");
|
|
618
|
+
} catch (e) {
|
|
619
|
+
console.error("[wavehouse] SSE status handler threw:", e);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/** Connect/read/reconnect until the stream is closed or hits a terminal error. */
|
|
624
|
+
async _run() {
|
|
625
|
+
await Promise.resolve();
|
|
626
|
+
let attempt = 0;
|
|
627
|
+
while (!this._closed) {
|
|
628
|
+
const { liveMs, terminal } = await this._attempt();
|
|
629
|
+
if (this._closed) return;
|
|
630
|
+
if (terminal) {
|
|
631
|
+
this.disconnect();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (liveMs >= STABLE_CONNECTION_MS) attempt = 0;
|
|
635
|
+
this._emitStatus("reconnecting");
|
|
636
|
+
await this._sleep(backoff2(attempt, this._retryFloorMs));
|
|
637
|
+
attempt++;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/** One connection attempt: request, validate, then pump frames until it ends. */
|
|
641
|
+
async _attempt() {
|
|
642
|
+
const ac = new AbortController();
|
|
643
|
+
this._abort = ac;
|
|
644
|
+
let target;
|
|
645
|
+
try {
|
|
646
|
+
target = this._url();
|
|
647
|
+
} catch (e) {
|
|
648
|
+
this._emitError({
|
|
649
|
+
status: 0,
|
|
650
|
+
code: "SSE_CONNECT_ERROR",
|
|
651
|
+
message: e instanceof Error ? e.message : String(e),
|
|
652
|
+
retryable: false
|
|
653
|
+
});
|
|
654
|
+
return { liveMs: 0, terminal: true };
|
|
655
|
+
}
|
|
656
|
+
let init;
|
|
657
|
+
try {
|
|
658
|
+
init = await this._init(ac.signal);
|
|
659
|
+
} catch (e) {
|
|
660
|
+
if (this._closed) return { liveMs: 0, terminal: true };
|
|
661
|
+
this._emitError({
|
|
662
|
+
status: 0,
|
|
663
|
+
code: "SSE_AUTH_ERROR",
|
|
664
|
+
message: e instanceof Error ? e.message : String(e),
|
|
665
|
+
retryable: true
|
|
666
|
+
});
|
|
667
|
+
return { liveMs: 0, terminal: false };
|
|
668
|
+
}
|
|
669
|
+
let res;
|
|
670
|
+
try {
|
|
671
|
+
const doFetch = this._opts.fetch;
|
|
672
|
+
res = doFetch ? await doFetch(target, init) : await fetch(target, init);
|
|
673
|
+
} catch (e) {
|
|
674
|
+
if (this._closed) return { liveMs: 0, terminal: true };
|
|
675
|
+
this._emitError({
|
|
676
|
+
status: 0,
|
|
677
|
+
code: "SSE_NETWORK_ERROR",
|
|
678
|
+
message: e instanceof Error ? e.message : String(e),
|
|
679
|
+
retryable: true
|
|
680
|
+
});
|
|
681
|
+
return { liveMs: 0, terminal: false };
|
|
682
|
+
}
|
|
683
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
684
|
+
this._emitError({
|
|
685
|
+
status: res.status,
|
|
686
|
+
code: "SSE_REDIRECT",
|
|
687
|
+
message: "The stream endpoint redirected and the SDK did not follow it. Redirects are refused when the request carries a credential \u2014 an `auth` token or configured `headers` \u2014 because a cross-origin hop strips the Authorization header, and this endpoint answers an unauthenticated caller with a reduced view instead of an error, while forwarding any configured headers to wherever the redirect points. If this request carries neither, `fetch` was left to follow the redirect itself, so arriving here means a response it will not follow (a 304, or a 3xx with no Location header) or an `options.fetch` that returned the redirect verbatim. Either way, set `baseURL` to the final URL; most often this is an http\u2192https upgrade or a canonical-host redirect at a proxy. To follow a credentialed redirect anyway, supply an `options.fetch` that overrides `redirect`.",
|
|
688
|
+
retryable: false
|
|
689
|
+
});
|
|
690
|
+
releaseBody(res);
|
|
691
|
+
return { liveMs: 0, terminal: true };
|
|
692
|
+
}
|
|
693
|
+
if (!res.ok) {
|
|
694
|
+
const error = await parseErrorResponse(res);
|
|
695
|
+
this._emitError(error);
|
|
696
|
+
return { liveMs: 0, terminal: !error.retryable };
|
|
697
|
+
}
|
|
698
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
699
|
+
if (!contentType.toLowerCase().split(";")[0].trim().startsWith("text/event-stream")) {
|
|
700
|
+
this._emitError({
|
|
701
|
+
status: res.status,
|
|
702
|
+
code: "SSE_BAD_CONTENT_TYPE",
|
|
703
|
+
message: `Expected \`text/event-stream\`, got \`${contentType || "(none)"}\`. Something between the client and WaveHouse answered this request \u2014 an auth gateway's login page is the usual cause.`,
|
|
704
|
+
retryable: false
|
|
705
|
+
});
|
|
706
|
+
releaseBody(res);
|
|
707
|
+
return { liveMs: 0, terminal: true };
|
|
708
|
+
}
|
|
709
|
+
const body = res.body;
|
|
710
|
+
if (!body || typeof body.getReader !== "function" || body.locked || res.bodyUsed) {
|
|
711
|
+
this._emitError({
|
|
712
|
+
status: res.status,
|
|
713
|
+
code: "SSE_NO_STREAM_BODY",
|
|
714
|
+
message: "Streaming requires a response with a readable body. The configured `options.fetch` returned one whose body is absent or already read \u2014 an implementation that buffers or consumes the response cannot be used for `.stream()` or `.liveQuery()`.",
|
|
715
|
+
retryable: false
|
|
716
|
+
});
|
|
717
|
+
releaseBody(res);
|
|
718
|
+
return { liveMs: 0, terminal: true };
|
|
552
719
|
}
|
|
553
|
-
this.
|
|
720
|
+
this._emitStatus("live");
|
|
721
|
+
const openedAt = Date.now();
|
|
722
|
+
await this._pump(body);
|
|
723
|
+
return { liveMs: Date.now() - openedAt, terminal: false };
|
|
554
724
|
}
|
|
555
|
-
|
|
725
|
+
/** Resolve the stream URL. Throws on a `baseURL` that isn't a usable http(s) origin. */
|
|
726
|
+
_url() {
|
|
556
727
|
const url = resolveURL(this._opts.baseURL, "/v1/stream");
|
|
728
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
729
|
+
throw new Error(
|
|
730
|
+
`baseURL must use http or https, got "${url.protocol}". Streaming is plain HTTP \u2014 there is no WebSocket endpoint.`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
557
733
|
url.searchParams.set("table", this._opts.table);
|
|
558
734
|
if (this._opts.since) {
|
|
559
735
|
url.searchParams.set("since", this._opts.since);
|
|
560
736
|
}
|
|
737
|
+
return url.toString();
|
|
738
|
+
}
|
|
739
|
+
/** Build the request init, minting a token for this attempt. */
|
|
740
|
+
async _init(signal) {
|
|
741
|
+
const base = { Accept: "text/event-stream" };
|
|
561
742
|
if (this._opts.auth) {
|
|
562
743
|
const token = await this._opts.auth();
|
|
563
744
|
if (token) {
|
|
564
|
-
|
|
745
|
+
base.Authorization = `Bearer ${token}`;
|
|
565
746
|
}
|
|
566
747
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
console.warn(
|
|
570
|
-
`[wavehouse] ${activeSSEConnections} SSE connections open. Browsers limit HTTP/1.1 to 6 connections per domain.`
|
|
571
|
-
);
|
|
748
|
+
if (this._lastEventId) {
|
|
749
|
+
base["Last-Event-ID"] = this._lastEventId;
|
|
572
750
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
this.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
751
|
+
const credentialed = base.Authorization !== void 0 || Object.keys(this._opts.headers ?? {}).length > 0;
|
|
752
|
+
const init = {
|
|
753
|
+
...this._opts.fetchOptions,
|
|
754
|
+
method: "GET",
|
|
755
|
+
headers: mergeHeaders(base, this._opts.headers),
|
|
756
|
+
signal,
|
|
757
|
+
// Owned by the SDK rather than left to `fetchOptions`, unlike the REST
|
|
758
|
+
// path, because on a stream these are correctness rather than preference:
|
|
759
|
+
//
|
|
760
|
+
// cache — as an init field, not a `Cache-Control` header. The header
|
|
761
|
+
// form is not in the server's Access-Control-Allow-Headers,
|
|
762
|
+
// so it would fail every cross-origin preflight.
|
|
763
|
+
// redirect — followed only when this request carries no credential.
|
|
764
|
+
// Platforms strip `Authorization` on a cross-origin redirect
|
|
765
|
+
// (whatwg/fetch#1544) and forward other headers intact, so a
|
|
766
|
+
// credentialed hop either silently downgrades the stream to
|
|
767
|
+
// the default role — this endpoint answers an unauthenticated
|
|
768
|
+
// caller rather than rejecting them — or delivers a configured
|
|
769
|
+
// secret to whatever the redirect names. Neither is worth a
|
|
770
|
+
// convenience. Without either, redirects are followed, so CDN
|
|
771
|
+
// canonicalization, geo/LB indirection, and an http→https
|
|
772
|
+
// upgrade all just work. The test is deliberately that
|
|
773
|
+
// concrete pair and not "is this authenticated": cookies do
|
|
774
|
+
// not appear in it. A cookie is re-derived per
|
|
775
|
+
// destination rather than carried, so under the default
|
|
776
|
+
// same-origin credentials mode it survives a redirect only
|
|
777
|
+
// while the hop stays inside both the request's origin and
|
|
778
|
+
// the cookie's Path; outside either the stream is
|
|
779
|
+
// silently unauthed. See #478 and the SDK guide under
|
|
780
|
+
// "Supplying your own fetch".
|
|
781
|
+
// `manual` over `error` for the credentialed case so the
|
|
782
|
+
// outcome stays inspectable: `error` rejects with a bare
|
|
783
|
+
// TypeError indistinguishable from a connection failure, which
|
|
784
|
+
// the loop above would retry forever against a redirect that
|
|
785
|
+
// is never going to stop happening.
|
|
786
|
+
cache: "no-store",
|
|
787
|
+
redirect: credentialed ? "manual" : "follow",
|
|
788
|
+
// Neutralized like the REST path does, and for a sharper reason here: a
|
|
789
|
+
// body on a GET makes `fetch` throw, which reads as a network failure and
|
|
790
|
+
// would be retried forever against a request that can never be built.
|
|
791
|
+
body: void 0
|
|
589
792
|
};
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
793
|
+
if (!("window" in globalThis)) {
|
|
794
|
+
delete init.credentials;
|
|
795
|
+
}
|
|
796
|
+
return init;
|
|
797
|
+
}
|
|
798
|
+
/** Decode and parse frames until the stream ends, aborts, or errors. */
|
|
799
|
+
async _pump(body) {
|
|
800
|
+
let overflowed = false;
|
|
801
|
+
const parser = createParser({
|
|
802
|
+
onEvent: (msg) => {
|
|
803
|
+
if (msg.id) this._lastEventId = msg.id;
|
|
804
|
+
if (!msg.data) return;
|
|
805
|
+
this._dispatch(msg.data);
|
|
806
|
+
},
|
|
807
|
+
onRetry: (ms) => {
|
|
808
|
+
this._retryFloorMs = Math.min(Math.max(ms, 0), MAX_BACKOFF_MS);
|
|
809
|
+
},
|
|
810
|
+
onError: (err2) => {
|
|
811
|
+
if (err2.type === "max-buffer-size-exceeded") overflowed = true;
|
|
812
|
+
this._emitError({
|
|
599
813
|
status: 0,
|
|
600
|
-
code: "
|
|
601
|
-
message:
|
|
814
|
+
code: "SSE_PARSE_ERROR",
|
|
815
|
+
message: err2.message,
|
|
602
816
|
retryable: true
|
|
603
817
|
});
|
|
818
|
+
},
|
|
819
|
+
maxBufferSize: MAX_BUFFER_CHARS
|
|
820
|
+
});
|
|
821
|
+
const reader = body.getReader();
|
|
822
|
+
const decoder = new TextDecoder();
|
|
823
|
+
try {
|
|
824
|
+
for (; ; ) {
|
|
825
|
+
const { done, value } = await reader.read();
|
|
826
|
+
if (done) return;
|
|
827
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
828
|
+
if (overflowed) return;
|
|
604
829
|
}
|
|
605
|
-
}
|
|
830
|
+
} catch (e) {
|
|
831
|
+
if (this._closed) return;
|
|
832
|
+
this._emitError({
|
|
833
|
+
status: 0,
|
|
834
|
+
code: "SSE_READ_ERROR",
|
|
835
|
+
message: e instanceof Error ? e.message : String(e),
|
|
836
|
+
retryable: true
|
|
837
|
+
});
|
|
838
|
+
} finally {
|
|
839
|
+
reader.cancel().catch(() => {
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Turn one frame's `data` into a StreamEvent.
|
|
845
|
+
*
|
|
846
|
+
* Guarded like `_emitError` and `_emitStatus`: `parser.feed()` dispatches
|
|
847
|
+
* every complete frame in a chunk synchronously, so a subscriber that closes
|
|
848
|
+
* from inside its own `next` — "read until I see my event, then stop" —
|
|
849
|
+
* would otherwise keep receiving the rest of that chunk. `EventSource` ended
|
|
850
|
+
* delivery on `close()`, and gap-fill replay routinely lands many frames in
|
|
851
|
+
* one chunk.
|
|
852
|
+
*/
|
|
853
|
+
_dispatch(data) {
|
|
854
|
+
if (this._closed) return;
|
|
855
|
+
let event;
|
|
856
|
+
try {
|
|
857
|
+
const msg = JSON.parse(data);
|
|
858
|
+
event = { table: msg.table_name, timestamp: msg.received_timestamp, data: msg.data };
|
|
859
|
+
} catch {
|
|
860
|
+
console.warn("[wavehouse] SSE received malformed message:", data);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
this.onEvent?.(event);
|
|
865
|
+
} catch (e) {
|
|
866
|
+
console.error("[wavehouse] SSE event handler threw:", e);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/** Suppress callbacks that race a `disconnect()`. */
|
|
870
|
+
_emitError(error) {
|
|
871
|
+
if (this._closed) return;
|
|
872
|
+
try {
|
|
873
|
+
this.onError?.(error);
|
|
874
|
+
} catch (e) {
|
|
875
|
+
console.error("[wavehouse] SSE error handler threw:", e);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Same guard for status. Aborting does not retract an already-resolved
|
|
880
|
+
* `Response`, so a `disconnect()` landing between the fetch settling and the
|
|
881
|
+
* "live" below would otherwise flip a closed controller back to live — and it
|
|
882
|
+
* stays there, since the loop then exits without emitting anything further.
|
|
883
|
+
* `disconnect()` calls `onStatus` directly; that terminal "closed" is the one
|
|
884
|
+
* status that must always get through.
|
|
885
|
+
*/
|
|
886
|
+
_emitStatus(status) {
|
|
887
|
+
if (this._closed) return;
|
|
888
|
+
try {
|
|
889
|
+
this.onStatus?.(status);
|
|
890
|
+
} catch (e) {
|
|
891
|
+
console.error("[wavehouse] SSE status handler threw:", e);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/** Wait out a reconnect gap, cut short by `disconnect()`. */
|
|
895
|
+
_sleep(ms) {
|
|
896
|
+
if (this._closed) return Promise.resolve();
|
|
897
|
+
return new Promise((resolve) => {
|
|
898
|
+
const timer = setTimeout(() => {
|
|
899
|
+
this._wake = null;
|
|
900
|
+
resolve();
|
|
901
|
+
}, ms);
|
|
902
|
+
this._wake = () => {
|
|
903
|
+
clearTimeout(timer);
|
|
904
|
+
this._wake = null;
|
|
905
|
+
resolve();
|
|
906
|
+
};
|
|
907
|
+
});
|
|
606
908
|
}
|
|
607
909
|
};
|
|
608
910
|
|
|
@@ -1140,16 +1442,14 @@ var WaveHouseClient = class {
|
|
|
1140
1442
|
}
|
|
1141
1443
|
/** @internal Create a stream for the given table. */
|
|
1142
1444
|
_createStream(table, opts) {
|
|
1143
|
-
if (typeof EventSource === "undefined") {
|
|
1144
|
-
throw new Error(
|
|
1145
|
-
"[WaveHouse SDK] Native EventSource is not available in this environment. Please provide a global polyfill (e.g., `globalThis.EventSource = require('eventsource')`)."
|
|
1146
|
-
);
|
|
1147
|
-
}
|
|
1148
1445
|
const transport = new SSETransport({
|
|
1149
1446
|
baseURL: this._ctx.baseURL,
|
|
1150
1447
|
table,
|
|
1151
1448
|
since: opts?.since,
|
|
1152
|
-
auth: this._ctx.auth
|
|
1449
|
+
auth: this._ctx.auth,
|
|
1450
|
+
fetch: this._ctx.options.fetch,
|
|
1451
|
+
headers: this._ctx.options.headers,
|
|
1452
|
+
fetchOptions: this._ctx.options.fetchOptions
|
|
1153
1453
|
});
|
|
1154
1454
|
const controller = new StreamController(transport);
|
|
1155
1455
|
if (opts?.signal) controller.attachSignal(opts.signal);
|