@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/README.md +160 -26
- package/dist/generated/polymarket.d.ts +307 -115
- package/dist/generated/webhooks.d.ts +123 -44
- package/dist/generated/ws-alerts.d.ts +2652 -0
- package/dist/generated/ws.d.ts +1221 -0
- package/dist/index.cjs +558 -181
- package/dist/index.cjs.map +9 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +558 -181
- package/dist/index.js.map +9 -8
- package/dist/namespaces/markets.d.ts +3 -3
- package/dist/namespaces/series.d.ts +2 -1
- package/dist/namespaces/trader.d.ts +4 -4
- package/dist/types/http.d.ts +1 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/ws-helpers.d.ts +5 -0
- package/dist/types/ws.d.ts +128 -90
- package/dist/ws-alerts.d.ts +39 -0
- package/dist/ws-transport.d.ts +26 -7
- package/dist/ws.d.ts +28 -33
- package/package.json +9 -5
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
582
|
+
getUrl;
|
|
550
583
|
retry;
|
|
551
584
|
callbacks;
|
|
552
585
|
constructor(url, retry, callbacks) {
|
|
553
|
-
this.
|
|
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"
|
|
562
|
-
return;
|
|
563
|
-
|
|
564
|
-
if (this.
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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.
|
|
629
|
+
this.addReplayMessages([message]);
|
|
592
630
|
}
|
|
593
|
-
|
|
594
|
-
|
|
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.
|
|
659
|
+
this.ws = new WebSocket(this.getUrl());
|
|
606
660
|
} catch (err) {
|
|
607
|
-
|
|
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
|
|
642
|
-
|
|
643
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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 = "
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
781
|
+
subscriptions = new Map;
|
|
782
|
+
pendingSubscribes = new Map;
|
|
783
|
+
subscribeTimeout;
|
|
784
|
+
pingTimer = null;
|
|
785
|
+
pongTimer = null;
|
|
786
|
+
isEmittingListenerError = false;
|
|
697
787
|
constructor(config) {
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
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.
|
|
717
|
-
this.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
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
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
this.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
this.
|
|
761
|
-
|
|
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
|
-
|
|
764
|
-
if (this.
|
|
871
|
+
unsubscribe(room) {
|
|
872
|
+
if (!this.subscriptions.has(room))
|
|
765
873
|
return;
|
|
766
|
-
this.
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
800
|
-
|
|
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
|
-
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
this.
|
|
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
|
-
|
|
812
|
-
this.
|
|
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
|
-
|
|
815
|
-
this.
|
|
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
|
-
|
|
818
|
-
this.
|
|
990
|
+
pausePendingSubscribes() {
|
|
991
|
+
for (const [, pending] of this.pendingSubscribes) {
|
|
992
|
+
this.clearSubscribeTimer(pending);
|
|
993
|
+
}
|
|
819
994
|
}
|
|
820
|
-
|
|
821
|
-
this.
|
|
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
|
-
|
|
824
|
-
|
|
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
|
-
|
|
829
|
-
this.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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
|
-
|
|
885
|
-
|
|
1239
|
+
sendSubscription(message) {
|
|
1240
|
+
if (this.state !== "connected")
|
|
1241
|
+
return false;
|
|
1242
|
+
return this.transport.sendNow(message);
|
|
886
1243
|
}
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
891
|
-
|
|
1251
|
+
pausePendingSubscribes() {
|
|
1252
|
+
for (const [, pending] of this.pendingSubscribes) {
|
|
1253
|
+
this.clearSubscribeTimer(pending);
|
|
1254
|
+
}
|
|
892
1255
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
this.
|
|
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
|
-
|
|
902
|
-
if (
|
|
1270
|
+
armPongTimer() {
|
|
1271
|
+
if (this.pongTimer !== null)
|
|
903
1272
|
return;
|
|
904
|
-
this.
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
this.
|
|
911
|
-
|
|
912
|
-
this.
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
if (this.
|
|
918
|
-
this.
|
|
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=
|
|
1327
|
+
//# debugId=48D6AA303BCDF9EC64756E2164756E21
|
|
951
1328
|
//# sourceMappingURL=index.js.map
|