@structbuild/sdk 0.3.0 → 0.3.2
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 +160 -26
- package/dist/generated/polymarket.d.ts +307 -115
- package/dist/generated/webhooks.d.ts +123 -44
- package/dist/generated/ws-alerts.d.ts +2652 -0
- package/dist/generated/ws.d.ts +1221 -0
- package/dist/index.cjs +558 -181
- package/dist/index.cjs.map +9 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +558 -181
- package/dist/index.js.map +9 -8
- package/dist/namespaces/markets.d.ts +3 -3
- package/dist/namespaces/series.d.ts +2 -1
- package/dist/namespaces/trader.d.ts +4 -4
- package/dist/types/http.d.ts +1 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/ws-helpers.d.ts +5 -0
- package/dist/types/ws.d.ts +128 -90
- package/dist/ws-alerts.d.ts +39 -0
- package/dist/ws-transport.d.ts +26 -7
- package/dist/ws.d.ts +28 -33
- package/package.json +9 -5
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(exports_src, {
|
|
|
36
36
|
StructWebSocket: () => StructWebSocket,
|
|
37
37
|
StructError: () => StructError,
|
|
38
38
|
StructClient: () => StructClient,
|
|
39
|
+
StructAlertsWebSocket: () => StructAlertsWebSocket,
|
|
39
40
|
NetworkError: () => NetworkError,
|
|
40
41
|
HttpError: () => HttpError,
|
|
41
42
|
HttpClient: () => HttpClient
|
|
@@ -389,11 +390,13 @@ class MarketsNamespace extends Namespace {
|
|
|
389
390
|
}
|
|
390
391
|
async getMarket(params, venue) {
|
|
391
392
|
const { conditionId, ...query } = params;
|
|
392
|
-
|
|
393
|
+
const res = await this.get(venue, `/market/${encodeURIComponent(conditionId)}`, { params: { ...query } });
|
|
394
|
+
return { ...res, data: res.data[0] };
|
|
393
395
|
}
|
|
394
396
|
async getMarketBySlug(params, venue) {
|
|
395
397
|
const { marketSlug, ...query } = params;
|
|
396
|
-
|
|
398
|
+
const res = await this.get(venue, `/market/slug/${encodeURIComponent(marketSlug)}`, { params: { ...query } });
|
|
399
|
+
return { ...res, data: res.data[0] };
|
|
397
400
|
}
|
|
398
401
|
async getMarketChart(params, venue) {
|
|
399
402
|
return this.get(venue, "/market/chart", { params: { ...params } });
|
|
@@ -431,6 +434,10 @@ class SeriesNamespace extends Namespace {
|
|
|
431
434
|
async getSeriesOutcomes(params, venue) {
|
|
432
435
|
return this.get(venue, "/series/outcomes", { params: { ...params } });
|
|
433
436
|
}
|
|
437
|
+
async getSeriesEvents(params, venue) {
|
|
438
|
+
const { identifier, ...query } = params;
|
|
439
|
+
return this.get(venue, `/series/${encodeURIComponent(identifier)}/events`, { params: { ...query } });
|
|
440
|
+
}
|
|
434
441
|
}
|
|
435
442
|
// src/namespaces/trader.ts
|
|
436
443
|
class TraderNamespace extends Namespace {
|
|
@@ -581,6 +588,32 @@ class StructClient {
|
|
|
581
588
|
// src/ws-transport.ts
|
|
582
589
|
var DEFAULT_INITIAL_DELAY_MS2 = 1000;
|
|
583
590
|
var DEFAULT_MAX_DELAY_MS2 = 30000;
|
|
591
|
+
var DEFAULT_MAX_PENDING_MESSAGES = 256;
|
|
592
|
+
var AUTH_FAILURE_CLOSE_CODES = new Set([1008, 4401]);
|
|
593
|
+
function buildWebSocketUrl(path, config, defaultBaseUrl) {
|
|
594
|
+
const base = trimTrailingSlashes(config.baseUrl ?? defaultBaseUrl);
|
|
595
|
+
const wsBase = toWebSocketBaseUrl(base);
|
|
596
|
+
const params = new URLSearchParams({ "api-key": config.apiKey });
|
|
597
|
+
if (config.jwt)
|
|
598
|
+
params.set("token", config.jwt);
|
|
599
|
+
return `${wsBase}${path}?${params.toString()}`;
|
|
600
|
+
}
|
|
601
|
+
function trimTrailingSlashes(value) {
|
|
602
|
+
let end = value.length;
|
|
603
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
604
|
+
end--;
|
|
605
|
+
}
|
|
606
|
+
return end === value.length ? value : value.slice(0, end);
|
|
607
|
+
}
|
|
608
|
+
function toWebSocketBaseUrl(baseUrl) {
|
|
609
|
+
if (baseUrl.startsWith("https://")) {
|
|
610
|
+
return `wss://${baseUrl.slice("https://".length)}`;
|
|
611
|
+
}
|
|
612
|
+
if (baseUrl.startsWith("http://")) {
|
|
613
|
+
return `ws://${baseUrl.slice("http://".length)}`;
|
|
614
|
+
}
|
|
615
|
+
return baseUrl;
|
|
616
|
+
}
|
|
584
617
|
|
|
585
618
|
class WebSocketTransport {
|
|
586
619
|
ws = null;
|
|
@@ -588,13 +621,14 @@ class WebSocketTransport {
|
|
|
588
621
|
reconnectTimer = null;
|
|
589
622
|
reconnectAttempt = 0;
|
|
590
623
|
intentionalClose = false;
|
|
624
|
+
connectWaiters = new Set;
|
|
591
625
|
pendingMessages = [];
|
|
592
626
|
replayMessages = [];
|
|
593
|
-
|
|
627
|
+
getUrl;
|
|
594
628
|
retry;
|
|
595
629
|
callbacks;
|
|
596
630
|
constructor(url, retry, callbacks) {
|
|
597
|
-
this.
|
|
631
|
+
this.getUrl = typeof url === "function" ? url : () => url;
|
|
598
632
|
this.retry = retry;
|
|
599
633
|
this.callbacks = callbacks;
|
|
600
634
|
}
|
|
@@ -602,22 +636,24 @@ class WebSocketTransport {
|
|
|
602
636
|
return this._state;
|
|
603
637
|
}
|
|
604
638
|
connect() {
|
|
605
|
-
if (this._state === "connected"
|
|
606
|
-
return;
|
|
607
|
-
|
|
608
|
-
if (this.
|
|
609
|
-
return;
|
|
639
|
+
if (this._state === "connected")
|
|
640
|
+
return Promise.resolve();
|
|
641
|
+
const promise = this.createConnectPromise();
|
|
642
|
+
if (this._state === "connecting" || this._state === "reconnecting") {
|
|
643
|
+
return promise;
|
|
610
644
|
}
|
|
611
645
|
this.intentionalClose = false;
|
|
612
646
|
this.clearReconnectTimer();
|
|
613
647
|
this.setState("connecting");
|
|
614
648
|
this.createSocket();
|
|
649
|
+
return promise;
|
|
615
650
|
}
|
|
616
651
|
disconnect() {
|
|
617
652
|
this.intentionalClose = true;
|
|
618
653
|
this.clearReconnectTimer();
|
|
619
654
|
this.pendingMessages.length = 0;
|
|
620
655
|
this.replayMessages.length = 0;
|
|
656
|
+
this.rejectConnect(new WebSocketClosedError(1000, "client disconnect"));
|
|
621
657
|
if (this.ws) {
|
|
622
658
|
this.ws.close(1000, "client disconnect");
|
|
623
659
|
this.ws = null;
|
|
@@ -625,30 +661,50 @@ class WebSocketTransport {
|
|
|
625
661
|
this.setState("disconnected");
|
|
626
662
|
}
|
|
627
663
|
send(message) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
664
|
+
const queuedMessage = { message };
|
|
665
|
+
if (this.sendMessage(queuedMessage))
|
|
666
|
+
return true;
|
|
667
|
+
this.enqueueMessage(this.pendingMessages, queuedMessage, "pending");
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
sendNow(message) {
|
|
671
|
+
return this.sendMessage({ message });
|
|
633
672
|
}
|
|
634
673
|
addReplayMessage(message) {
|
|
635
|
-
this.
|
|
674
|
+
this.addReplayMessages([message]);
|
|
636
675
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (predicate(this.replayMessages[i])) {
|
|
640
|
-
this.replayMessages.splice(i, 1);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
676
|
+
addReplayMessages(messages) {
|
|
677
|
+
this.enqueueReplayMessages(messages.map((message) => ({ message })));
|
|
643
678
|
}
|
|
644
679
|
clearReplayMessages() {
|
|
645
680
|
this.replayMessages.length = 0;
|
|
646
681
|
}
|
|
682
|
+
close(code, reason) {
|
|
683
|
+
this.ws?.close(code, reason);
|
|
684
|
+
}
|
|
685
|
+
createConnectPromise() {
|
|
686
|
+
return new Promise((resolve, reject) => {
|
|
687
|
+
this.connectWaiters.add({ resolve, reject });
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
resolveConnect() {
|
|
691
|
+
for (const waiter of this.connectWaiters) {
|
|
692
|
+
waiter.resolve();
|
|
693
|
+
}
|
|
694
|
+
this.connectWaiters.clear();
|
|
695
|
+
}
|
|
696
|
+
rejectConnect(error) {
|
|
697
|
+
for (const waiter of this.connectWaiters) {
|
|
698
|
+
waiter.reject(error);
|
|
699
|
+
}
|
|
700
|
+
this.connectWaiters.clear();
|
|
701
|
+
}
|
|
647
702
|
createSocket() {
|
|
648
703
|
try {
|
|
649
|
-
this.ws = new WebSocket(this.
|
|
704
|
+
this.ws = new WebSocket(this.getUrl());
|
|
650
705
|
} catch (err) {
|
|
651
|
-
|
|
706
|
+
const error = new WebSocketError("Failed to create WebSocket", { cause: err });
|
|
707
|
+
this.callbacks.onError(error);
|
|
652
708
|
this.scheduleReconnect();
|
|
653
709
|
return;
|
|
654
710
|
}
|
|
@@ -657,6 +713,7 @@ class WebSocketTransport {
|
|
|
657
713
|
this.reconnectAttempt = 0;
|
|
658
714
|
this.replaySubscriptions();
|
|
659
715
|
this.flushPendingMessages();
|
|
716
|
+
this.resolveConnect();
|
|
660
717
|
this.callbacks.onOpen();
|
|
661
718
|
};
|
|
662
719
|
this.ws.onclose = (event) => {
|
|
@@ -666,7 +723,14 @@ class WebSocketTransport {
|
|
|
666
723
|
this.callbacks.onClose(event.code, event.reason);
|
|
667
724
|
return;
|
|
668
725
|
}
|
|
726
|
+
const error = new WebSocketClosedError(event.code, event.reason);
|
|
669
727
|
this.callbacks.onClose(event.code, event.reason);
|
|
728
|
+
if (this.isAuthFailure(event.code)) {
|
|
729
|
+
this.setState("disconnected");
|
|
730
|
+
this.rejectConnect(error);
|
|
731
|
+
this.callbacks.onAuthFailed(error);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
670
734
|
this.scheduleReconnect();
|
|
671
735
|
};
|
|
672
736
|
this.ws.onerror = () => {
|
|
@@ -682,25 +746,24 @@ class WebSocketTransport {
|
|
|
682
746
|
};
|
|
683
747
|
}
|
|
684
748
|
replaySubscriptions() {
|
|
685
|
-
for (const
|
|
686
|
-
|
|
687
|
-
this.
|
|
749
|
+
for (const replayGroup of this.replayMessages) {
|
|
750
|
+
for (const queuedMessage of replayGroup.messages) {
|
|
751
|
+
this.sendMessage(queuedMessage);
|
|
688
752
|
}
|
|
689
753
|
}
|
|
690
754
|
}
|
|
691
755
|
flushPendingMessages() {
|
|
692
756
|
while (this.pendingMessages.length > 0) {
|
|
693
|
-
|
|
694
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
695
|
-
this.ws.send(JSON.stringify(msg));
|
|
696
|
-
}
|
|
757
|
+
this.sendMessage(this.pendingMessages.shift());
|
|
697
758
|
}
|
|
698
759
|
}
|
|
699
760
|
scheduleReconnect() {
|
|
700
761
|
const maxRetries = this.retry.maxRetries ?? Infinity;
|
|
701
762
|
if (this.reconnectAttempt >= maxRetries) {
|
|
763
|
+
const error = new WebSocketClosedError(1006, "Max reconnection attempts reached");
|
|
702
764
|
this.setState("disconnected");
|
|
703
|
-
this.
|
|
765
|
+
this.rejectConnect(error);
|
|
766
|
+
this.callbacks.onReconnectFailed(error);
|
|
704
767
|
return;
|
|
705
768
|
}
|
|
706
769
|
this.setState("reconnecting");
|
|
@@ -722,46 +785,82 @@ class WebSocketTransport {
|
|
|
722
785
|
this.reconnectTimer = null;
|
|
723
786
|
}
|
|
724
787
|
}
|
|
788
|
+
sendMessage(queuedMessage) {
|
|
789
|
+
if (this.ws?.readyState !== WebSocket.OPEN)
|
|
790
|
+
return false;
|
|
791
|
+
this.ws.send(JSON.stringify(queuedMessage.message));
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
enqueueMessage(queue, queuedMessage, queueName) {
|
|
795
|
+
queue.push(queuedMessage);
|
|
796
|
+
const maxPendingMessages = this.retry.maxPendingMessages ?? DEFAULT_MAX_PENDING_MESSAGES;
|
|
797
|
+
if (queue.length > maxPendingMessages) {
|
|
798
|
+
queue.shift();
|
|
799
|
+
this.callbacks.onWarning(new WebSocketError(`WebSocket ${queueName} queue exceeded ${maxPendingMessages} messages; dropped oldest message`));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
enqueueReplayMessages(messages) {
|
|
803
|
+
this.replayMessages.push({ messages });
|
|
804
|
+
const maxPendingMessages = this.retry.maxPendingMessages ?? DEFAULT_MAX_PENDING_MESSAGES;
|
|
805
|
+
if (this.replayMessages.length > maxPendingMessages) {
|
|
806
|
+
this.replayMessages.shift();
|
|
807
|
+
this.callbacks.onWarning(new WebSocketError(`WebSocket replay queue exceeded ${maxPendingMessages} entries; dropped oldest replay entry`));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
isAuthFailure(code) {
|
|
811
|
+
return AUTH_FAILURE_CLOSE_CODES.has(code);
|
|
812
|
+
}
|
|
725
813
|
setState(state) {
|
|
726
814
|
this._state = state;
|
|
727
815
|
}
|
|
728
816
|
}
|
|
729
817
|
|
|
730
818
|
// src/ws.ts
|
|
731
|
-
var DEFAULT_BASE_URL2 = "
|
|
819
|
+
var DEFAULT_BASE_URL2 = "wss://api.struct.to";
|
|
820
|
+
var PING_INTERVAL_MS = 30000;
|
|
821
|
+
var DEFAULT_SUBSCRIBE_TIMEOUT_MS = 1e4;
|
|
732
822
|
|
|
733
823
|
class StructWebSocket {
|
|
734
824
|
transport;
|
|
735
825
|
listeners = new Map;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
826
|
+
subscriptions = new Map;
|
|
827
|
+
pendingSubscribes = new Map;
|
|
828
|
+
subscribeTimeout;
|
|
829
|
+
pingTimer = null;
|
|
830
|
+
pongTimer = null;
|
|
831
|
+
isEmittingListenerError = false;
|
|
741
832
|
constructor(config) {
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
833
|
+
this.subscribeTimeout = config.subscribeTimeout ?? DEFAULT_SUBSCRIBE_TIMEOUT_MS;
|
|
834
|
+
const getUrl = () => buildWebSocketUrl("/ws", {
|
|
835
|
+
apiKey: config.apiKey,
|
|
836
|
+
jwt: config.getJwt?.() ?? config.jwt,
|
|
837
|
+
baseUrl: config.baseUrl
|
|
838
|
+
}, DEFAULT_BASE_URL2);
|
|
839
|
+
this.transport = new WebSocketTransport(getUrl, config.reconnect ?? {}, {
|
|
746
840
|
onOpen: () => this.handleOpen(),
|
|
747
841
|
onClose: (code, reason) => this.handleClose(code, reason),
|
|
748
842
|
onError: (error) => this.emit("error", error),
|
|
749
843
|
onMessage: (data) => this.handleMessage(data),
|
|
750
|
-
onReconnecting: (attempt) => this.emit("reconnecting", { attempt })
|
|
844
|
+
onReconnecting: (attempt) => this.emit("reconnecting", { attempt }),
|
|
845
|
+
onReconnectFailed: (error) => this.emit("reconnect_failed", error),
|
|
846
|
+
onAuthFailed: (error) => this.emit("auth_failed", error),
|
|
847
|
+
onWarning: (warning) => this.emit("warning", warning)
|
|
751
848
|
});
|
|
752
849
|
}
|
|
753
850
|
get state() {
|
|
754
851
|
return this.transport.state;
|
|
755
852
|
}
|
|
756
853
|
connect() {
|
|
757
|
-
this.transport.connect();
|
|
854
|
+
return this.transport.connect();
|
|
758
855
|
}
|
|
759
856
|
disconnect() {
|
|
760
|
-
this.
|
|
761
|
-
this.
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
857
|
+
this.stopPing();
|
|
858
|
+
for (const [, pending] of this.pendingSubscribes) {
|
|
859
|
+
this.clearSubscribeTimer(pending);
|
|
860
|
+
pending.reject(new WebSocketError("Disconnected"));
|
|
861
|
+
}
|
|
862
|
+
this.pendingSubscribes.clear();
|
|
863
|
+
this.subscriptions.clear();
|
|
765
864
|
this.transport.disconnect();
|
|
766
865
|
}
|
|
767
866
|
on(event, listener) {
|
|
@@ -771,11 +870,12 @@ class StructWebSocket {
|
|
|
771
870
|
this.listeners.set(event, set);
|
|
772
871
|
}
|
|
773
872
|
set.add(listener);
|
|
774
|
-
return
|
|
873
|
+
return () => {
|
|
874
|
+
set.delete(listener);
|
|
875
|
+
};
|
|
775
876
|
}
|
|
776
877
|
off(event, listener) {
|
|
777
878
|
this.listeners.get(event)?.delete(listener);
|
|
778
|
-
return this;
|
|
779
879
|
}
|
|
780
880
|
once(event, listener) {
|
|
781
881
|
const wrapper = (payload) => {
|
|
@@ -784,132 +884,374 @@ class StructWebSocket {
|
|
|
784
884
|
};
|
|
785
885
|
return this.on(event, wrapper);
|
|
786
886
|
}
|
|
787
|
-
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
this.transport.send(msg);
|
|
887
|
+
removeAllListeners(event) {
|
|
888
|
+
if (event) {
|
|
889
|
+
this.listeners.delete(event);
|
|
890
|
+
} else {
|
|
891
|
+
this.listeners.clear();
|
|
892
|
+
}
|
|
794
893
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this.
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
this.
|
|
805
|
-
|
|
894
|
+
subscribe(room, filters) {
|
|
895
|
+
const resolvedFilters = filters ?? {};
|
|
896
|
+
const isNewRoom = !this.subscriptions.has(room);
|
|
897
|
+
const existing = this.pendingSubscribes.get(room);
|
|
898
|
+
if (existing) {
|
|
899
|
+
this.clearSubscribeTimer(existing);
|
|
900
|
+
existing.reject(new WebSocketError("Superseded by new subscription"));
|
|
901
|
+
}
|
|
902
|
+
this.subscriptions.set(room, resolvedFilters);
|
|
903
|
+
this.rebuildReplay();
|
|
904
|
+
return new Promise((resolve, reject) => {
|
|
905
|
+
const pending = {
|
|
906
|
+
resolve,
|
|
907
|
+
reject,
|
|
908
|
+
timer: null
|
|
909
|
+
};
|
|
910
|
+
this.pendingSubscribes.set(room, pending);
|
|
911
|
+
if (this.sendSubscription(room, resolvedFilters, isNewRoom)) {
|
|
912
|
+
this.armSubscribeTimer(room, pending);
|
|
913
|
+
}
|
|
914
|
+
});
|
|
806
915
|
}
|
|
807
|
-
|
|
808
|
-
if (this.
|
|
916
|
+
unsubscribe(room) {
|
|
917
|
+
if (!this.subscriptions.has(room))
|
|
809
918
|
return;
|
|
810
|
-
this.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
919
|
+
const pending = this.pendingSubscribes.get(room);
|
|
920
|
+
if (pending) {
|
|
921
|
+
this.clearSubscribeTimer(pending);
|
|
922
|
+
pending.reject(new WebSocketError("Unsubscribed"));
|
|
923
|
+
this.pendingSubscribes.delete(room);
|
|
924
|
+
}
|
|
925
|
+
this.subscriptions.delete(room);
|
|
926
|
+
this.rebuildReplay();
|
|
927
|
+
if (this.state === "connected") {
|
|
928
|
+
this.transport.sendNow({
|
|
929
|
+
type: "room_message",
|
|
930
|
+
payload: { room_id: room, message: { action: "unsubscribe_all" } }
|
|
931
|
+
});
|
|
932
|
+
this.transport.sendNow({ type: "leave_room", payload: { room_id: room } });
|
|
933
|
+
}
|
|
814
934
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
935
|
+
rebuildReplay() {
|
|
936
|
+
this.transport.clearReplayMessages();
|
|
937
|
+
for (const [roomId, filters] of this.subscriptions) {
|
|
938
|
+
this.transport.addReplayMessages([
|
|
939
|
+
{ type: "join_room", payload: { room_id: roomId } },
|
|
940
|
+
{
|
|
941
|
+
type: "room_message",
|
|
942
|
+
payload: { room_id: roomId, message: { action: "subscribe", ...filters } }
|
|
943
|
+
}
|
|
944
|
+
]);
|
|
945
|
+
}
|
|
826
946
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
947
|
+
handleOpen() {
|
|
948
|
+
this.startPing();
|
|
949
|
+
this.restartPendingSubscribes();
|
|
950
|
+
this.emit("connected", undefined);
|
|
951
|
+
}
|
|
952
|
+
handleClose(code, reason) {
|
|
953
|
+
this.stopPing();
|
|
954
|
+
this.pausePendingSubscribes();
|
|
955
|
+
this.emit("disconnected", { code, reason });
|
|
956
|
+
}
|
|
957
|
+
handleMessage(raw) {
|
|
958
|
+
const msg = raw;
|
|
959
|
+
if (!msg || typeof msg !== "object" || !msg.type)
|
|
960
|
+
return;
|
|
961
|
+
if (msg.type === "pong") {
|
|
962
|
+
this.clearPongTimer();
|
|
831
963
|
return;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
964
|
+
}
|
|
965
|
+
if (this.isSubscribeResponseMessage(msg)) {
|
|
966
|
+
const pending = this.pendingSubscribes.get(msg.room_id);
|
|
967
|
+
if (pending) {
|
|
968
|
+
this.clearSubscribeTimer(pending);
|
|
969
|
+
this.pendingSubscribes.delete(msg.room_id);
|
|
970
|
+
const subscribeError = this.getSubscribeError(msg.data);
|
|
971
|
+
if (subscribeError) {
|
|
972
|
+
const error = new WebSocketError(subscribeError);
|
|
973
|
+
pending.reject(error);
|
|
974
|
+
this.emit("error", error);
|
|
975
|
+
} else {
|
|
976
|
+
pending.resolve(msg.data);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
842
979
|
return;
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const msg = this.walletTrackMessage("unsubscribe", removed);
|
|
846
|
-
this.rebuildWalletReplay();
|
|
847
|
-
this.transport.send(msg);
|
|
980
|
+
}
|
|
981
|
+
this.emit(msg.type, msg.data);
|
|
848
982
|
}
|
|
849
|
-
|
|
850
|
-
|
|
983
|
+
isSubscribeResponseMessage(msg) {
|
|
984
|
+
if (!msg.type || !msg.room_id)
|
|
985
|
+
return false;
|
|
986
|
+
return msg.type === "subscribed" || msg.type.endsWith("_subscribe_response");
|
|
851
987
|
}
|
|
852
|
-
|
|
853
|
-
this.
|
|
988
|
+
emit(event, payload) {
|
|
989
|
+
const set = this.listeners.get(event);
|
|
990
|
+
if (!set)
|
|
991
|
+
return;
|
|
992
|
+
for (const fn of set) {
|
|
993
|
+
try {
|
|
994
|
+
fn(payload);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
this.handleListenerError(error);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
854
999
|
}
|
|
855
|
-
|
|
856
|
-
this.
|
|
1000
|
+
startPing() {
|
|
1001
|
+
this.stopPing();
|
|
1002
|
+
this.pingTimer = setInterval(() => {
|
|
1003
|
+
if (this.transport.sendNow({ type: "ping" })) {
|
|
1004
|
+
this.armPongTimer();
|
|
1005
|
+
}
|
|
1006
|
+
}, PING_INTERVAL_MS);
|
|
857
1007
|
}
|
|
858
|
-
|
|
859
|
-
this.
|
|
1008
|
+
stopPing() {
|
|
1009
|
+
if (this.pingTimer !== null) {
|
|
1010
|
+
clearInterval(this.pingTimer);
|
|
1011
|
+
this.pingTimer = null;
|
|
1012
|
+
}
|
|
1013
|
+
this.clearPongTimer();
|
|
1014
|
+
}
|
|
1015
|
+
sendSubscription(room, filters, joinRoom) {
|
|
1016
|
+
if (this.state !== "connected")
|
|
1017
|
+
return false;
|
|
1018
|
+
let sent = true;
|
|
1019
|
+
if (joinRoom) {
|
|
1020
|
+
sent = this.transport.sendNow({ type: "join_room", payload: { room_id: room } }) && sent;
|
|
1021
|
+
}
|
|
1022
|
+
sent = this.transport.sendNow({
|
|
1023
|
+
type: "room_message",
|
|
1024
|
+
payload: { room_id: room, message: { action: "subscribe", ...filters } }
|
|
1025
|
+
}) && sent;
|
|
1026
|
+
return sent;
|
|
1027
|
+
}
|
|
1028
|
+
restartPendingSubscribes() {
|
|
1029
|
+
for (const [room, pending] of this.pendingSubscribes) {
|
|
1030
|
+
if (pending.timer === null) {
|
|
1031
|
+
this.armSubscribeTimer(room, pending);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
860
1034
|
}
|
|
861
|
-
|
|
862
|
-
this.
|
|
1035
|
+
pausePendingSubscribes() {
|
|
1036
|
+
for (const [, pending] of this.pendingSubscribes) {
|
|
1037
|
+
this.clearSubscribeTimer(pending);
|
|
1038
|
+
}
|
|
863
1039
|
}
|
|
864
|
-
|
|
865
|
-
this.
|
|
1040
|
+
armSubscribeTimer(room, pending) {
|
|
1041
|
+
this.clearSubscribeTimer(pending);
|
|
1042
|
+
pending.timer = setTimeout(() => {
|
|
1043
|
+
pending.timer = null;
|
|
1044
|
+
this.pendingSubscribes.delete(room);
|
|
1045
|
+
pending.reject(new WebSocketError(`Subscribe to ${room} timed out`));
|
|
1046
|
+
}, this.subscribeTimeout);
|
|
1047
|
+
}
|
|
1048
|
+
clearSubscribeTimer(pending) {
|
|
1049
|
+
if (pending.timer !== null) {
|
|
1050
|
+
clearTimeout(pending.timer);
|
|
1051
|
+
pending.timer = null;
|
|
1052
|
+
}
|
|
866
1053
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const added = uniqueConditionIds.filter((id) => !this.activeConditions.has(id));
|
|
870
|
-
if (added.length === 0)
|
|
1054
|
+
armPongTimer() {
|
|
1055
|
+
if (this.pongTimer !== null)
|
|
871
1056
|
return;
|
|
872
|
-
|
|
873
|
-
this.
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1057
|
+
this.pongTimer = setTimeout(() => {
|
|
1058
|
+
this.pongTimer = null;
|
|
1059
|
+
this.transport.close(4000, "pong timeout");
|
|
1060
|
+
}, PING_INTERVAL_MS * 2);
|
|
1061
|
+
}
|
|
1062
|
+
clearPongTimer() {
|
|
1063
|
+
if (this.pongTimer !== null) {
|
|
1064
|
+
clearTimeout(this.pongTimer);
|
|
1065
|
+
this.pongTimer = null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
getSubscribeError(data) {
|
|
1069
|
+
if (!data || typeof data !== "object")
|
|
1070
|
+
return null;
|
|
1071
|
+
const error = data.error;
|
|
1072
|
+
return typeof error === "string" && error.length > 0 ? error : null;
|
|
1073
|
+
}
|
|
1074
|
+
handleListenerError(error) {
|
|
1075
|
+
const normalizedError = error instanceof Error ? error : new WebSocketError("WebSocket listener threw", { cause: error });
|
|
1076
|
+
if (!this.isEmittingListenerError && (this.listeners.get("error")?.size ?? 0) > 0) {
|
|
1077
|
+
this.isEmittingListenerError = true;
|
|
1078
|
+
try {
|
|
1079
|
+
this.emit("error", normalizedError);
|
|
1080
|
+
return;
|
|
1081
|
+
} finally {
|
|
1082
|
+
this.isEmittingListenerError = false;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
console.error(normalizedError);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
// src/ws-alerts.ts
|
|
1089
|
+
var DEFAULT_BASE_URL3 = "wss://api.struct.to";
|
|
1090
|
+
var PING_INTERVAL_MS2 = 30000;
|
|
1091
|
+
var DEFAULT_SUBSCRIBE_TIMEOUT_MS2 = 1e4;
|
|
1092
|
+
|
|
1093
|
+
class StructAlertsWebSocket {
|
|
1094
|
+
transport;
|
|
1095
|
+
listeners = new Map;
|
|
1096
|
+
subscriptions = new Map;
|
|
1097
|
+
pendingSubscribes = new Map;
|
|
1098
|
+
subscribeTimeout;
|
|
1099
|
+
pingTimer = null;
|
|
1100
|
+
pongTimer = null;
|
|
1101
|
+
isEmittingListenerError = false;
|
|
1102
|
+
constructor(config) {
|
|
1103
|
+
this.subscribeTimeout = config.subscribeTimeout ?? DEFAULT_SUBSCRIBE_TIMEOUT_MS2;
|
|
1104
|
+
const getUrl = () => buildWebSocketUrl("/ws/alerts", {
|
|
1105
|
+
apiKey: config.apiKey,
|
|
1106
|
+
jwt: config.getJwt?.() ?? config.jwt,
|
|
1107
|
+
baseUrl: config.baseUrl
|
|
1108
|
+
}, DEFAULT_BASE_URL3);
|
|
1109
|
+
this.transport = new WebSocketTransport(getUrl, config.reconnect ?? {}, {
|
|
1110
|
+
onOpen: () => this.handleOpen(),
|
|
1111
|
+
onClose: (code, reason) => this.handleClose(code, reason),
|
|
1112
|
+
onError: (error) => this.emit("error", error),
|
|
1113
|
+
onMessage: (data) => this.handleMessage(data),
|
|
1114
|
+
onReconnecting: (attempt) => this.emit("reconnecting", { attempt }),
|
|
1115
|
+
onReconnectFailed: (error) => this.emit("reconnect_failed", error),
|
|
1116
|
+
onAuthFailed: (error) => this.emit("auth_failed", error),
|
|
1117
|
+
onWarning: (warning) => this.emit("warning", warning)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
get state() {
|
|
1121
|
+
return this.transport.state;
|
|
1122
|
+
}
|
|
1123
|
+
connect() {
|
|
1124
|
+
return this.transport.connect();
|
|
1125
|
+
}
|
|
1126
|
+
disconnect() {
|
|
1127
|
+
this.stopPing();
|
|
1128
|
+
for (const [, pending] of this.pendingSubscribes) {
|
|
1129
|
+
this.clearSubscribeTimer(pending);
|
|
1130
|
+
pending.reject(new WebSocketError("Disconnected"));
|
|
1131
|
+
}
|
|
1132
|
+
this.pendingSubscribes.clear();
|
|
1133
|
+
this.subscriptions.clear();
|
|
1134
|
+
this.transport.disconnect();
|
|
1135
|
+
}
|
|
1136
|
+
on(event, listener) {
|
|
1137
|
+
let set = this.listeners.get(event);
|
|
1138
|
+
if (!set) {
|
|
1139
|
+
set = new Set;
|
|
1140
|
+
this.listeners.set(event, set);
|
|
1141
|
+
}
|
|
1142
|
+
set.add(listener);
|
|
1143
|
+
return () => {
|
|
1144
|
+
set.delete(listener);
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
off(event, listener) {
|
|
1148
|
+
this.listeners.get(event)?.delete(listener);
|
|
1149
|
+
}
|
|
1150
|
+
once(event, listener) {
|
|
1151
|
+
const wrapper = (payload) => {
|
|
1152
|
+
this.off(event, wrapper);
|
|
1153
|
+
listener(payload);
|
|
1154
|
+
};
|
|
1155
|
+
return this.on(event, wrapper);
|
|
1156
|
+
}
|
|
1157
|
+
removeAllListeners(event) {
|
|
1158
|
+
if (event) {
|
|
1159
|
+
this.listeners.delete(event);
|
|
1160
|
+
} else {
|
|
1161
|
+
this.listeners.clear();
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
subscribe(event, filters) {
|
|
1165
|
+
const message = { op: "subscribe", event, ...filters };
|
|
1166
|
+
const existing = this.pendingSubscribes.get(event);
|
|
1167
|
+
if (existing) {
|
|
1168
|
+
this.clearSubscribeTimer(existing);
|
|
1169
|
+
existing.reject(new WebSocketError("Superseded by new subscription"));
|
|
1170
|
+
}
|
|
1171
|
+
this.subscriptions.set(event, message);
|
|
1172
|
+
this.rebuildReplay();
|
|
1173
|
+
return new Promise((resolve, reject) => {
|
|
1174
|
+
const pending = {
|
|
1175
|
+
resolve,
|
|
1176
|
+
reject,
|
|
1177
|
+
timer: null
|
|
1178
|
+
};
|
|
1179
|
+
this.pendingSubscribes.set(event, pending);
|
|
1180
|
+
if (this.sendSubscription(message)) {
|
|
1181
|
+
this.armSubscribeTimer(event, pending);
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
unsubscribe(event) {
|
|
1186
|
+
const sub = this.subscriptions.get(event);
|
|
1187
|
+
if (!sub)
|
|
882
1188
|
return;
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1189
|
+
const pending = this.pendingSubscribes.get(event);
|
|
1190
|
+
if (pending) {
|
|
1191
|
+
this.clearSubscribeTimer(pending);
|
|
1192
|
+
pending.reject(new WebSocketError("Unsubscribed"));
|
|
1193
|
+
this.pendingSubscribes.delete(event);
|
|
1194
|
+
}
|
|
1195
|
+
this.subscriptions.delete(event);
|
|
1196
|
+
this.rebuildReplay();
|
|
1197
|
+
if (this.state === "connected") {
|
|
1198
|
+
this.transport.sendNow({ ...sub, op: "unsubscribe" });
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
rebuildReplay() {
|
|
1202
|
+
this.transport.clearReplayMessages();
|
|
1203
|
+
for (const [, message] of this.subscriptions) {
|
|
1204
|
+
this.transport.addReplayMessage(message);
|
|
1205
|
+
}
|
|
888
1206
|
}
|
|
889
1207
|
handleOpen() {
|
|
1208
|
+
this.startPing();
|
|
1209
|
+
this.restartPendingSubscribes();
|
|
890
1210
|
this.emit("connected", undefined);
|
|
891
1211
|
}
|
|
892
1212
|
handleClose(code, reason) {
|
|
1213
|
+
this.stopPing();
|
|
1214
|
+
this.pausePendingSubscribes();
|
|
893
1215
|
this.emit("disconnected", { code, reason });
|
|
894
1216
|
}
|
|
895
1217
|
handleMessage(raw) {
|
|
896
1218
|
const msg = raw;
|
|
897
1219
|
if (!msg || typeof msg !== "object")
|
|
898
1220
|
return;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1221
|
+
if (msg.op === "pong" || msg.type === "pong") {
|
|
1222
|
+
this.clearPongTimer();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (msg.error) {
|
|
1226
|
+
const error = new WebSocketError(msg.error);
|
|
1227
|
+
if (msg.event) {
|
|
1228
|
+
const pending = this.pendingSubscribes.get(msg.event);
|
|
1229
|
+
if (pending) {
|
|
1230
|
+
this.clearSubscribeTimer(pending);
|
|
1231
|
+
this.pendingSubscribes.delete(msg.event);
|
|
1232
|
+
pending.reject(error);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
this.emit("error", error);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (msg.op === "subscribed" && msg.event) {
|
|
1239
|
+
const pending = this.pendingSubscribes.get(msg.event);
|
|
1240
|
+
if (pending) {
|
|
1241
|
+
this.clearSubscribeTimer(pending);
|
|
1242
|
+
this.pendingSubscribes.delete(msg.event);
|
|
1243
|
+
pending.resolve({ op: "subscribed", event: msg.event, subscription_id: msg.subscription_id });
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (msg.op === "unsubscribed")
|
|
1248
|
+
return;
|
|
1249
|
+
if (msg.event && msg.data !== undefined) {
|
|
1250
|
+
this.emit(msg.event, {
|
|
1251
|
+
event: msg.event,
|
|
1252
|
+
timestamp: msg.timestamp,
|
|
1253
|
+
data: msg.data
|
|
1254
|
+
});
|
|
913
1255
|
}
|
|
914
1256
|
}
|
|
915
1257
|
emit(event, payload) {
|
|
@@ -919,48 +1261,83 @@ class StructWebSocket {
|
|
|
919
1261
|
for (const fn of set) {
|
|
920
1262
|
try {
|
|
921
1263
|
fn(payload);
|
|
922
|
-
} catch {
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
this.handleListenerError(error);
|
|
1266
|
+
}
|
|
923
1267
|
}
|
|
924
1268
|
}
|
|
925
|
-
|
|
926
|
-
|
|
1269
|
+
startPing() {
|
|
1270
|
+
this.stopPing();
|
|
1271
|
+
this.pingTimer = setInterval(() => {
|
|
1272
|
+
if (this.transport.sendNow({ type: "ping" })) {
|
|
1273
|
+
this.armPongTimer();
|
|
1274
|
+
}
|
|
1275
|
+
}, PING_INTERVAL_MS2);
|
|
1276
|
+
}
|
|
1277
|
+
stopPing() {
|
|
1278
|
+
if (this.pingTimer !== null) {
|
|
1279
|
+
clearInterval(this.pingTimer);
|
|
1280
|
+
this.pingTimer = null;
|
|
1281
|
+
}
|
|
1282
|
+
this.clearPongTimer();
|
|
927
1283
|
}
|
|
928
|
-
|
|
929
|
-
|
|
1284
|
+
sendSubscription(message) {
|
|
1285
|
+
if (this.state !== "connected")
|
|
1286
|
+
return false;
|
|
1287
|
+
return this.transport.sendNow(message);
|
|
930
1288
|
}
|
|
931
|
-
|
|
932
|
-
|
|
1289
|
+
restartPendingSubscribes() {
|
|
1290
|
+
for (const [event, pending] of this.pendingSubscribes) {
|
|
1291
|
+
if (pending.timer === null) {
|
|
1292
|
+
this.armSubscribeTimer(event, pending);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
933
1295
|
}
|
|
934
|
-
|
|
935
|
-
|
|
1296
|
+
pausePendingSubscribes() {
|
|
1297
|
+
for (const [, pending] of this.pendingSubscribes) {
|
|
1298
|
+
this.clearSubscribeTimer(pending);
|
|
1299
|
+
}
|
|
936
1300
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
this.
|
|
1301
|
+
armSubscribeTimer(event, pending) {
|
|
1302
|
+
this.clearSubscribeTimer(pending);
|
|
1303
|
+
pending.timer = setTimeout(() => {
|
|
1304
|
+
pending.timer = null;
|
|
1305
|
+
this.pendingSubscribes.delete(event);
|
|
1306
|
+
pending.reject(new WebSocketError(`Subscribe to alert ${event} timed out`));
|
|
1307
|
+
}, this.subscribeTimeout);
|
|
1308
|
+
}
|
|
1309
|
+
clearSubscribeTimer(pending) {
|
|
1310
|
+
if (pending.timer !== null) {
|
|
1311
|
+
clearTimeout(pending.timer);
|
|
1312
|
+
pending.timer = null;
|
|
1313
|
+
}
|
|
944
1314
|
}
|
|
945
|
-
|
|
946
|
-
if (
|
|
1315
|
+
armPongTimer() {
|
|
1316
|
+
if (this.pongTimer !== null)
|
|
947
1317
|
return;
|
|
948
|
-
this.
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
this.
|
|
955
|
-
|
|
956
|
-
this.
|
|
1318
|
+
this.pongTimer = setTimeout(() => {
|
|
1319
|
+
this.pongTimer = null;
|
|
1320
|
+
this.transport.close(4000, "pong timeout");
|
|
1321
|
+
}, PING_INTERVAL_MS2 * 2);
|
|
1322
|
+
}
|
|
1323
|
+
clearPongTimer() {
|
|
1324
|
+
if (this.pongTimer !== null) {
|
|
1325
|
+
clearTimeout(this.pongTimer);
|
|
1326
|
+
this.pongTimer = null;
|
|
957
1327
|
}
|
|
958
1328
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
if (this.
|
|
962
|
-
this.
|
|
1329
|
+
handleListenerError(error) {
|
|
1330
|
+
const normalizedError = error instanceof Error ? error : new WebSocketError("WebSocket listener threw", { cause: error });
|
|
1331
|
+
if (!this.isEmittingListenerError && (this.listeners.get("error")?.size ?? 0) > 0) {
|
|
1332
|
+
this.isEmittingListenerError = true;
|
|
1333
|
+
try {
|
|
1334
|
+
this.emit("error", normalizedError);
|
|
1335
|
+
return;
|
|
1336
|
+
} finally {
|
|
1337
|
+
this.isEmittingListenerError = false;
|
|
1338
|
+
}
|
|
963
1339
|
}
|
|
1340
|
+
console.error(normalizedError);
|
|
964
1341
|
}
|
|
965
1342
|
}
|
|
966
1343
|
// src/paginate.ts
|
|
@@ -979,5 +1356,5 @@ async function* paginate(fetcher, params, pageSize = DEFAULT_PAGE_SIZE) {
|
|
|
979
1356
|
}
|
|
980
1357
|
}
|
|
981
1358
|
|
|
982
|
-
//# debugId=
|
|
1359
|
+
//# debugId=C22008EEB4E58D0B64756E2164756E21
|
|
983
1360
|
//# sourceMappingURL=index.js.map
|