@structbuild/sdk 0.3.0 → 0.3.2

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