@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/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
- url;
621
+ getUrl;
594
622
  retry;
595
623
  callbacks;
596
624
  constructor(url, retry, callbacks) {
597
- this.url = url;
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" || this._state === "connecting" || this._state === "reconnecting") {
606
- return;
607
- }
608
- if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
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
- if (this._state === "connected" && this.ws?.readyState === WebSocket.OPEN) {
629
- this.ws.send(JSON.stringify(message));
630
- } else {
631
- this.pendingMessages.push(message);
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.replayMessages.push(message);
668
+ this.addReplayMessages([message]);
636
669
  }
637
- removeReplayMessages(predicate) {
638
- for (let i = this.replayMessages.length - 1;i >= 0; i--) {
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.url);
698
+ this.ws = new WebSocket(this.getUrl());
650
699
  } catch (err) {
651
- this.callbacks.onError(new WebSocketError("Failed to create WebSocket", { cause: err }));
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 msg of this.replayMessages) {
686
- if (this.ws?.readyState === WebSocket.OPEN) {
687
- this.ws.send(JSON.stringify(msg));
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
- const msg = this.pendingMessages.shift();
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.callbacks.onError(new WebSocketClosedError(1006, "Max reconnection attempts reached"));
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 = "https://api.struct.to/v1";
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
- activeMarkets = new Set;
737
- activeMarketPositions = new Set;
738
- activeWallets = new Set;
739
- activeConditions = new Set;
740
- activeSpecialRooms = new Set;
820
+ subscriptions = new Map;
821
+ pendingSubscribes = new Map;
822
+ subscribeTimeout;
823
+ pingTimer = null;
824
+ pongTimer = null;
825
+ isEmittingListenerError = false;
741
826
  constructor(config) {
742
- const httpBase = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
743
- const wsBase = httpBase.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
744
- const url = config.jwt ? `${wsBase}?api-key=${encodeURIComponent(config.apiKey)}&token=${encodeURIComponent(config.jwt)}` : `${wsBase}?token=${encodeURIComponent(config.apiKey)}`;
745
- this.transport = new WebSocketTransport(url, config.reconnect ?? {}, {
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.activeMarkets.clear();
761
- this.activeMarketPositions.clear();
762
- this.activeWallets.clear();
763
- this.activeConditions.clear();
764
- this.activeSpecialRooms.clear();
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 this;
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
- subscribeMarket(conditionId) {
788
- if (this.activeMarkets.has(conditionId))
789
- return;
790
- this.activeMarkets.add(conditionId);
791
- const msg = this.marketSubscribeMessage(conditionId);
792
- this.transport.addReplayMessage(msg);
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
- unsubscribeMarket(conditionId) {
796
- if (!this.activeMarkets.has(conditionId))
797
- return;
798
- this.activeMarkets.delete(conditionId);
799
- const msg = {
800
- type: "unsubscribe_market",
801
- room_id: `market_${conditionId}`,
802
- message: {}
803
- };
804
- this.transport.removeReplayMessages((m) => m.type === "subscribe_market" && m.room_id === `market_${conditionId}`);
805
- this.transport.send(msg);
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
- subscribeMarketByPosition(positionId) {
808
- if (this.activeMarketPositions.has(positionId))
910
+ unsubscribe(room) {
911
+ if (!this.subscriptions.has(room))
809
912
  return;
810
- this.activeMarketPositions.add(positionId);
811
- const msg = this.marketPositionSubscribeMessage(positionId);
812
- this.transport.addReplayMessage(msg);
813
- this.transport.send(msg);
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
- unsubscribeMarketByPosition(positionId) {
816
- if (!this.activeMarketPositions.has(positionId))
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
- this.activeMarketPositions.delete(positionId);
819
- const msg = {
820
- type: "unsubscribe_market",
821
- room_id: `market_position_${positionId}`,
822
- message: {}
823
- };
824
- this.transport.removeReplayMessages((m) => m.type === "subscribe_market" && m.room_id === `market_position_${positionId}`);
825
- this.transport.send(msg);
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
- trackWallets(addresses) {
828
- const uniqueAddresses = [...new Set(addresses)];
829
- const added = uniqueAddresses.filter((addr) => !this.activeWallets.has(addr));
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 addr of added)
833
- this.activeWallets.add(addr);
834
- const msg = this.walletTrackMessage("subscribe", added);
835
- this.rebuildWalletReplay();
836
- this.transport.send(msg);
837
- }
838
- untrackWallets(addresses) {
839
- const uniqueAddresses = [...new Set(addresses)];
840
- const removed = uniqueAddresses.filter((addr) => this.activeWallets.has(addr));
841
- if (removed.length === 0)
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
- for (const addr of removed)
844
- this.activeWallets.delete(addr);
845
- const msg = this.walletTrackMessage("unsubscribe", removed);
846
- this.rebuildWalletReplay();
847
- this.transport.send(msg);
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
- subscribeWhaleTrades() {
850
- this.subscribeSpecialRoom("whale_trades");
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
- unsubscribeWhaleTrades() {
853
- this.unsubscribeSpecialRoom("whale_trades");
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
- subscribeSmartMoneyTrades() {
856
- this.subscribeSpecialRoom("smart_money_trades");
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
- unsubscribeSmartMoneyTrades() {
859
- this.unsubscribeSpecialRoom("smart_money_trades");
1109
+ get state() {
1110
+ return this.transport.state;
860
1111
  }
861
- subscribeInsiderTrades() {
862
- this.subscribeSpecialRoom("insider_trades");
1112
+ connect() {
1113
+ return this.transport.connect();
863
1114
  }
864
- unsubscribeInsiderTrades() {
865
- this.unsubscribeSpecialRoom("insider_trades");
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
- trackConditions(conditionIds) {
868
- const uniqueConditionIds = [...new Set(conditionIds)];
869
- const added = uniqueConditionIds.filter((id) => !this.activeConditions.has(id));
870
- if (added.length === 0)
871
- return;
872
- for (const id of added)
873
- this.activeConditions.add(id);
874
- const msg = this.conditionsTrackMessage("subscribe", added);
875
- this.rebuildConditionsReplay();
876
- this.transport.send(msg);
877
- }
878
- untrackConditions(conditionIds) {
879
- const uniqueConditionIds = [...new Set(conditionIds)];
880
- const removed = uniqueConditionIds.filter((id) => this.activeConditions.has(id));
881
- if (removed.length === 0)
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
- for (const id of removed)
884
- this.activeConditions.delete(id);
885
- const msg = this.conditionsTrackMessage("unsubscribe", removed);
886
- this.rebuildConditionsReplay();
887
- this.transport.send(msg);
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
- const roomId = msg.room_id ?? "";
900
- const type = msg.type;
901
- if (type === "market_trade" || roomId.startsWith("market_")) {
902
- this.emit("market_trade", msg.data);
903
- } else if (type === "whale_trade" || roomId === "whale_trades") {
904
- this.emit("whale_trade", msg.data);
905
- } else if (type === "smart_money_trade" || roomId === "smart_money_trades") {
906
- this.emit("smart_money_trade", msg.data);
907
- } else if (type === "insider_trade" || roomId === "insider_trades") {
908
- this.emit("insider_trade", msg.data);
909
- } else if (type === "wallet_tracking_alert") {
910
- this.emit("wallet_tracking_alert", msg.data);
911
- } else if (type === "conditions_tracking_alert") {
912
- this.emit("conditions_tracking_alert", msg.data);
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
- marketSubscribeMessage(conditionId) {
926
- return { type: "subscribe_market", room_id: `market_${conditionId}`, message: {} };
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
- marketPositionSubscribeMessage(positionId) {
929
- return { type: "subscribe_market", room_id: `market_position_${positionId}`, message: {} };
1273
+ sendSubscription(message) {
1274
+ if (this.state !== "connected")
1275
+ return false;
1276
+ return this.transport.sendNow(message);
930
1277
  }
931
- walletTrackMessage(action, addresses) {
932
- return { type: "wallet_tracking", action, wallet_addresses: addresses };
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
- conditionsTrackMessage(action, conditionIds) {
935
- return { type: "conditions_tracking", action, condition_ids: conditionIds };
1285
+ pausePendingSubscribes() {
1286
+ for (const [, pending] of this.pendingSubscribes) {
1287
+ this.clearSubscribeTimer(pending);
1288
+ }
936
1289
  }
937
- subscribeSpecialRoom(roomId) {
938
- if (this.activeSpecialRooms.has(roomId))
939
- return;
940
- this.activeSpecialRooms.add(roomId);
941
- const msg = { type: "subscribe_market", room_id: roomId, message: {} };
942
- this.transport.addReplayMessage(msg);
943
- this.transport.send(msg);
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
- unsubscribeSpecialRoom(roomId) {
946
- if (!this.activeSpecialRooms.has(roomId))
1304
+ armPongTimer() {
1305
+ if (this.pongTimer !== null)
947
1306
  return;
948
- this.activeSpecialRooms.delete(roomId);
949
- const msg = { type: "unsubscribe_market", room_id: roomId, message: {} };
950
- this.transport.removeReplayMessages((m) => m.type === "subscribe_market" && m.room_id === roomId);
951
- this.transport.send(msg);
952
- }
953
- rebuildWalletReplay() {
954
- this.transport.removeReplayMessages((m) => m.type === "wallet_tracking");
955
- if (this.activeWallets.size > 0) {
956
- this.transport.addReplayMessage(this.walletTrackMessage("subscribe", [...this.activeWallets]));
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
- rebuildConditionsReplay() {
960
- this.transport.removeReplayMessages((m) => m.type === "conditions_tracking");
961
- if (this.activeConditions.size > 0) {
962
- this.transport.addReplayMessage(this.conditionsTrackMessage("subscribe", [...this.activeConditions]));
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=BC93A176E3C2ACA564756E2164756E21
1348
+ //# debugId=14A417375B65E26F64756E2164756E21
983
1349
  //# sourceMappingURL=index.js.map