@structbuild/sdk 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -26
- package/dist/generated/polymarket.d.ts +137 -91
- package/dist/generated/ws-alerts.d.ts +2599 -0
- package/dist/generated/ws.d.ts +921 -0
- package/dist/index.cjs +545 -179
- package/dist/index.cjs.map +6 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +545 -179
- package/dist/index.js.map +6 -5
- package/dist/types/http.d.ts +1 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/types/ws-helpers.d.ts +5 -0
- package/dist/types/ws.d.ts +119 -90
- package/dist/ws-alerts.d.ts +39 -0
- package/dist/ws-transport.d.ts +26 -7
- package/dist/ws.d.ts +27 -33
- package/package.json +8 -5
package/dist/index.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
|
-
|
|
576
|
+
getUrl;
|
|
550
577
|
retry;
|
|
551
578
|
callbacks;
|
|
552
579
|
constructor(url, retry, callbacks) {
|
|
553
|
-
this.
|
|
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"
|
|
562
|
-
return;
|
|
563
|
-
|
|
564
|
-
if (this.
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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.
|
|
623
|
+
this.addReplayMessages([message]);
|
|
592
624
|
}
|
|
593
|
-
|
|
594
|
-
|
|
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.
|
|
653
|
+
this.ws = new WebSocket(this.getUrl());
|
|
606
654
|
} catch (err) {
|
|
607
|
-
|
|
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
|
|
642
|
-
|
|
643
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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 = "
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
775
|
+
subscriptions = new Map;
|
|
776
|
+
pendingSubscribes = new Map;
|
|
777
|
+
subscribeTimeout;
|
|
778
|
+
pingTimer = null;
|
|
779
|
+
pongTimer = null;
|
|
780
|
+
isEmittingListenerError = false;
|
|
697
781
|
constructor(config) {
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
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.
|
|
717
|
-
this.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
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
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
this.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
this.
|
|
761
|
-
|
|
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
|
-
|
|
764
|
-
if (this.
|
|
865
|
+
unsubscribe(room) {
|
|
866
|
+
if (!this.subscriptions.has(room))
|
|
765
867
|
return;
|
|
766
|
-
this.
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
772
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
const
|
|
785
|
-
|
|
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
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
800
|
-
this.
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
|
|
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
|
-
|
|
812
|
-
|
|
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
|
-
|
|
815
|
-
this.
|
|
1064
|
+
get state() {
|
|
1065
|
+
return this.transport.state;
|
|
816
1066
|
}
|
|
817
|
-
|
|
818
|
-
this.
|
|
1067
|
+
connect() {
|
|
1068
|
+
return this.transport.connect();
|
|
819
1069
|
}
|
|
820
|
-
|
|
821
|
-
this.
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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
|
-
|
|
885
|
-
|
|
1228
|
+
sendSubscription(message) {
|
|
1229
|
+
if (this.state !== "connected")
|
|
1230
|
+
return false;
|
|
1231
|
+
return this.transport.sendNow(message);
|
|
886
1232
|
}
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
891
|
-
|
|
1240
|
+
pausePendingSubscribes() {
|
|
1241
|
+
for (const [, pending] of this.pendingSubscribes) {
|
|
1242
|
+
this.clearSubscribeTimer(pending);
|
|
1243
|
+
}
|
|
892
1244
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
this.
|
|
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
|
-
|
|
902
|
-
if (
|
|
1259
|
+
armPongTimer() {
|
|
1260
|
+
if (this.pongTimer !== null)
|
|
903
1261
|
return;
|
|
904
|
-
this.
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
this.
|
|
911
|
-
|
|
912
|
-
this.
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
if (this.
|
|
918
|
-
this.
|
|
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=
|
|
1316
|
+
//# debugId=72F4D54604EB46BD64756E2164756E21
|
|
951
1317
|
//# sourceMappingURL=index.js.map
|