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