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