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