blink-trade-sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +130 -0
- package/dist/managed-ws.d.ts +135 -0
- package/dist/managed-ws.js +475 -0
- package/dist/types.d.ts +288 -0
- package/dist/types.js +2 -0
- package/dist/ws-transport.d.ts +50 -0
- package/dist/ws-transport.js +118 -0
- package/dist/ws.d.ts +328 -0
- package/dist/ws.js +482 -0
- package/package.json +35 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/** Exact wire models for Blink API v1. Large integers are decimal strings. */
|
|
2
|
+
export type MarketType = 'perp' | 'spot';
|
|
3
|
+
export type Resolution = '1m' | '5m' | '15m' | '1h' | '4h' | '1d';
|
|
4
|
+
export interface Status {
|
|
5
|
+
status: 'ok' | 'syncing' | 'degraded';
|
|
6
|
+
timestamp_ns: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MarginTier {
|
|
9
|
+
tier: number;
|
|
10
|
+
notional_lower_bound_quote_lots: string;
|
|
11
|
+
max_leverage: number;
|
|
12
|
+
maintenance_margin_rate_bps: number;
|
|
13
|
+
maintenance_deduction_quote_lots: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Market {
|
|
16
|
+
market_type: MarketType;
|
|
17
|
+
market_id: number;
|
|
18
|
+
symbol: string;
|
|
19
|
+
base_token: string;
|
|
20
|
+
quote_token: string;
|
|
21
|
+
quote_token_id?: string;
|
|
22
|
+
base_decimals: number;
|
|
23
|
+
quote_decimals: number;
|
|
24
|
+
base_atoms_per_base_lot: string;
|
|
25
|
+
quote_atoms_per_quote_lot: string;
|
|
26
|
+
base_lots_per_base_unit: string;
|
|
27
|
+
quote_lots_per_quote_unit: string;
|
|
28
|
+
quote_lots_per_base_unit_per_tick: string;
|
|
29
|
+
default_maker_fee_100th_bps: number;
|
|
30
|
+
default_taker_fee_100th_bps: number;
|
|
31
|
+
liquidation_fee_100th_bps?: number;
|
|
32
|
+
min_size_base_lots: string;
|
|
33
|
+
min_order_notional_quote_lots: string;
|
|
34
|
+
max_market_order_value_quote_lots?: string;
|
|
35
|
+
max_limit_order_value_quote_lots?: string;
|
|
36
|
+
max_leverage?: number;
|
|
37
|
+
oracle_id?: string;
|
|
38
|
+
fee_recipient?: string;
|
|
39
|
+
margin_tiers?: MarginTier[];
|
|
40
|
+
status: 'active' | 'reduce_only' | 'paused' | 'settled';
|
|
41
|
+
}
|
|
42
|
+
export interface MarketStats {
|
|
43
|
+
market_type: MarketType;
|
|
44
|
+
market_id: number;
|
|
45
|
+
timestamp_ns: string;
|
|
46
|
+
last_price_ticks?: string;
|
|
47
|
+
mark_price_ticks?: string;
|
|
48
|
+
oracle_price_ticks?: string;
|
|
49
|
+
mid_price_ticks?: string;
|
|
50
|
+
best_bid_price_ticks?: string;
|
|
51
|
+
best_ask_price_ticks?: string;
|
|
52
|
+
open_interest_base_lots?: string;
|
|
53
|
+
funding_rate_100th_bps?: number;
|
|
54
|
+
predicted_funding_rate_100th_bps?: number;
|
|
55
|
+
volume_24h_base_lots: string;
|
|
56
|
+
volume_24h_quote_lots: string;
|
|
57
|
+
trades_24h: string;
|
|
58
|
+
low_24h_price_ticks?: string;
|
|
59
|
+
high_24h_price_ticks?: string;
|
|
60
|
+
change_24h_bps?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface PriceLevel {
|
|
63
|
+
price_ticks: string;
|
|
64
|
+
size_base_lots: string;
|
|
65
|
+
}
|
|
66
|
+
export interface Orderbook {
|
|
67
|
+
timestamp_ns: string;
|
|
68
|
+
sequence: string;
|
|
69
|
+
bids: PriceLevel[];
|
|
70
|
+
asks: PriceLevel[];
|
|
71
|
+
}
|
|
72
|
+
export interface Trade {
|
|
73
|
+
timestamp_ns: string;
|
|
74
|
+
sequence: string;
|
|
75
|
+
price_ticks: string;
|
|
76
|
+
size_base_lots: string;
|
|
77
|
+
quote_notional_lots: string;
|
|
78
|
+
taker_is_bid: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface Candle {
|
|
81
|
+
open_time_ms: number;
|
|
82
|
+
open: string;
|
|
83
|
+
high: string;
|
|
84
|
+
low: string;
|
|
85
|
+
close: string;
|
|
86
|
+
volume_base: string;
|
|
87
|
+
volume_quote: string;
|
|
88
|
+
trades: number;
|
|
89
|
+
}
|
|
90
|
+
export interface MarkCandle {
|
|
91
|
+
open_time_ms: number;
|
|
92
|
+
open: string;
|
|
93
|
+
high: string;
|
|
94
|
+
low: string;
|
|
95
|
+
close: string;
|
|
96
|
+
samples: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A delegation row. Nested under `Subaccount.delegations` it identifies the
|
|
100
|
+
* delegate and its permissions; in the websocket catalog and account channels
|
|
101
|
+
* it also carries the owning `subaccount_id` plus the optional expiry and name.
|
|
102
|
+
*/
|
|
103
|
+
export interface Delegation {
|
|
104
|
+
delegate: string;
|
|
105
|
+
permissions: string[];
|
|
106
|
+
subaccount_id?: number;
|
|
107
|
+
expires_at_ns?: string;
|
|
108
|
+
name?: string;
|
|
109
|
+
}
|
|
110
|
+
export interface Subaccount {
|
|
111
|
+
subaccount_id: number;
|
|
112
|
+
status: 'active' | 'restricted' | 'closed';
|
|
113
|
+
created_at_ns: string;
|
|
114
|
+
cross_margin_balance_quote_lots: string;
|
|
115
|
+
total_isolated_margin_quote_lots: string;
|
|
116
|
+
equity_quote_lots: string;
|
|
117
|
+
available_margin_quote_lots: string;
|
|
118
|
+
transferable_margin_quote_lots: string;
|
|
119
|
+
initial_margin_requirement_quote_lots: string;
|
|
120
|
+
maintenance_margin_requirement_quote_lots: string;
|
|
121
|
+
delegations: Delegation[];
|
|
122
|
+
}
|
|
123
|
+
export interface Balance {
|
|
124
|
+
token_id: string;
|
|
125
|
+
total_token_atoms: string;
|
|
126
|
+
locked_token_atoms: string;
|
|
127
|
+
deposited_token_atoms: string;
|
|
128
|
+
withdrawn_token_atoms: string;
|
|
129
|
+
}
|
|
130
|
+
export interface Position {
|
|
131
|
+
market_id: number;
|
|
132
|
+
side: 'long' | 'short';
|
|
133
|
+
size_base_lots: string;
|
|
134
|
+
entry_price_ticks: string;
|
|
135
|
+
liquidation_price_ticks?: string;
|
|
136
|
+
margin_mode: 'cross' | 'isolated';
|
|
137
|
+
leverage: number;
|
|
138
|
+
position_value_quote_lots: string;
|
|
139
|
+
unrealized_pnl_quote_lots: string;
|
|
140
|
+
realized_pnl_quote_lots: string;
|
|
141
|
+
roe_bps: number;
|
|
142
|
+
margin_used_token_atoms: string;
|
|
143
|
+
isolated_margin_quote_lots: string;
|
|
144
|
+
fees_quote_lots: string;
|
|
145
|
+
/** Funding since opening including accrual; positive means paid. */
|
|
146
|
+
funding_paid_quote_lots: string;
|
|
147
|
+
}
|
|
148
|
+
export interface Order {
|
|
149
|
+
timestamp_ns: string;
|
|
150
|
+
sequence: string;
|
|
151
|
+
market_type: MarketType;
|
|
152
|
+
market_id: number;
|
|
153
|
+
order_id: string;
|
|
154
|
+
client_order_id: string;
|
|
155
|
+
side: 'bid' | 'ask';
|
|
156
|
+
price_ticks: string;
|
|
157
|
+
original_size_base_lots: string;
|
|
158
|
+
remaining_size_base_lots: string;
|
|
159
|
+
order_type: string;
|
|
160
|
+
time_in_force: string;
|
|
161
|
+
reduce_only?: boolean;
|
|
162
|
+
tick_offset?: string;
|
|
163
|
+
trigger_price_ticks?: string;
|
|
164
|
+
max_price_ticks?: string;
|
|
165
|
+
status: string;
|
|
166
|
+
}
|
|
167
|
+
export interface Fill {
|
|
168
|
+
/** Append-only row id (`"<tx_number>-<ordinal>"`) on websocket account rows; absent on REST rows. */
|
|
169
|
+
id?: string;
|
|
170
|
+
timestamp_ns: string;
|
|
171
|
+
sequence: string;
|
|
172
|
+
market_type: MarketType;
|
|
173
|
+
market_id: number;
|
|
174
|
+
order_id: string;
|
|
175
|
+
client_order_id: string;
|
|
176
|
+
role: 'maker' | 'taker';
|
|
177
|
+
side: 'bid' | 'ask';
|
|
178
|
+
price_ticks: string;
|
|
179
|
+
size_base_lots: string;
|
|
180
|
+
quote_notional_lots: string;
|
|
181
|
+
fee_quote_lots: string;
|
|
182
|
+
position_size_before_base_lots?: string;
|
|
183
|
+
position_size_after_base_lots?: string;
|
|
184
|
+
/** PnL closed by this fill before fees; zero for opening and spot fills. */
|
|
185
|
+
realized_pnl_quote_lots: string;
|
|
186
|
+
crossed?: boolean;
|
|
187
|
+
margin_bucket?: 'cross' | 'isolated';
|
|
188
|
+
liquidation_id?: string;
|
|
189
|
+
liquidation_method?: string;
|
|
190
|
+
}
|
|
191
|
+
export interface Transaction {
|
|
192
|
+
hash: string;
|
|
193
|
+
signer?: string;
|
|
194
|
+
method: string;
|
|
195
|
+
status: 'success' | 'failed';
|
|
196
|
+
/**
|
|
197
|
+
* Rollup block that included this transaction. Absent while the
|
|
198
|
+
* transaction is only soft-confirmed by the sequencer: the gateway omits
|
|
199
|
+
* the field until the block is indexed, then serves it as a string u64.
|
|
200
|
+
*/
|
|
201
|
+
rollup_height?: string;
|
|
202
|
+
sequence: string;
|
|
203
|
+
timestamp_ns: string;
|
|
204
|
+
error?: string;
|
|
205
|
+
call?: Record<string, unknown>;
|
|
206
|
+
}
|
|
207
|
+
export interface FundingRate {
|
|
208
|
+
timestamp_ns: string;
|
|
209
|
+
sequence: string;
|
|
210
|
+
market_id: number;
|
|
211
|
+
funding_rate_100th_bps: number;
|
|
212
|
+
long_cumulative_funding_quote_lots: string;
|
|
213
|
+
short_cumulative_funding_quote_lots: string;
|
|
214
|
+
}
|
|
215
|
+
export interface FundingPayment {
|
|
216
|
+
/** Append-only row id (`"<tx_number>-<ordinal>"`) on websocket account rows; absent on REST rows. */
|
|
217
|
+
id?: string;
|
|
218
|
+
timestamp_ns: string;
|
|
219
|
+
sequence: string;
|
|
220
|
+
bucket: 'cross' | 'isolated';
|
|
221
|
+
market_id: number;
|
|
222
|
+
side: 'long' | 'short';
|
|
223
|
+
position_size_base_lots: string;
|
|
224
|
+
payment_quote_lots: string;
|
|
225
|
+
funding_index: string;
|
|
226
|
+
}
|
|
227
|
+
export interface BlpInfo {
|
|
228
|
+
total_shares: string;
|
|
229
|
+
protocol_fee_100th_bps: number;
|
|
230
|
+
}
|
|
231
|
+
/** Best bid and offer; a side is `null` when that side of the book is empty. */
|
|
232
|
+
export interface Bbo {
|
|
233
|
+
bid: PriceLevel | null;
|
|
234
|
+
ask: PriceLevel | null;
|
|
235
|
+
}
|
|
236
|
+
/** A trade on the `trades/...` channel: the stream omits `sequence` and `quote_notional_lots`. */
|
|
237
|
+
export interface WsTrade {
|
|
238
|
+
timestamp_ns: string;
|
|
239
|
+
price_ticks: string;
|
|
240
|
+
size_base_lots: string;
|
|
241
|
+
taker_is_bid: boolean;
|
|
242
|
+
}
|
|
243
|
+
/** The `orderbook` object of an update: the full book on the snapshot, changed levels afterwards (size `0` removes a level). */
|
|
244
|
+
export interface BookLevels {
|
|
245
|
+
bids: PriceLevel[];
|
|
246
|
+
asks: PriceLevel[];
|
|
247
|
+
}
|
|
248
|
+
export interface Leverage {
|
|
249
|
+
market_id: number;
|
|
250
|
+
leverage: number;
|
|
251
|
+
}
|
|
252
|
+
export interface AccountCatalogEntry {
|
|
253
|
+
subaccount_id: number;
|
|
254
|
+
status: 'active' | 'restricted' | 'closed';
|
|
255
|
+
created_at_ns: string;
|
|
256
|
+
}
|
|
257
|
+
/** Snapshot of the `accounts/{address}` channel. */
|
|
258
|
+
export interface AccountCatalog {
|
|
259
|
+
subaccounts: AccountCatalogEntry[];
|
|
260
|
+
delegations: Delegation[];
|
|
261
|
+
}
|
|
262
|
+
/** Snapshot of the `account/{address}/{subaccount_id}` channel; later frames are `operations` deltas. */
|
|
263
|
+
export interface AccountSnapshot {
|
|
264
|
+
account: Subaccount;
|
|
265
|
+
delegations: Delegation[];
|
|
266
|
+
balances: Balance[];
|
|
267
|
+
positions: Position[];
|
|
268
|
+
leverages: Leverage[];
|
|
269
|
+
orders: Order[];
|
|
270
|
+
fills: Fill[];
|
|
271
|
+
funding_payments: FundingPayment[];
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Kind of a collection delta: `reset` replaces a collection with `rows`,
|
|
275
|
+
* `insert` appends `row` to an append-only collection, `upsert` replaces the
|
|
276
|
+
* row with the same key or adds it, `delete` removes the row whose key fields
|
|
277
|
+
* `row` carries.
|
|
278
|
+
*/
|
|
279
|
+
export type OperationKind = 'reset' | 'insert' | 'upsert' | 'delete';
|
|
280
|
+
/** One collection mutation from an account-channel `operations` array. */
|
|
281
|
+
export interface CollectionOperation {
|
|
282
|
+
collection: string;
|
|
283
|
+
op: OperationKind | (string & {});
|
|
284
|
+
/** The complete row for `insert` / `upsert`; only the key fields for `delete`. */
|
|
285
|
+
row?: Record<string, unknown>;
|
|
286
|
+
/** The full collection for `reset`. */
|
|
287
|
+
rows?: Record<string, unknown>[];
|
|
288
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HelloFrame, WsMessage } from './ws.js';
|
|
2
|
+
/** The websocket protocol version accepted by the shared transport. */
|
|
3
|
+
export declare const WS_PROTOCOL_VERSION = 1;
|
|
4
|
+
export interface WebSocketLike {
|
|
5
|
+
readyState: number;
|
|
6
|
+
onopen: (() => void) | null;
|
|
7
|
+
onmessage: ((event: {
|
|
8
|
+
data: unknown;
|
|
9
|
+
}) => void) | null;
|
|
10
|
+
onclose: ((event: {
|
|
11
|
+
code?: number;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}) => void) | null;
|
|
14
|
+
onerror: ((error: unknown) => void) | null;
|
|
15
|
+
send(data: string): void;
|
|
16
|
+
close(code?: number, reason?: string): void;
|
|
17
|
+
ping?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface TransportOptions {
|
|
20
|
+
socketFactory?: (url: string) => WebSocketLike;
|
|
21
|
+
repeat?: typeof setInterval;
|
|
22
|
+
cancelRepeat?: typeof clearInterval;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
keepaliveIntervalMs?: number;
|
|
25
|
+
onMessage: (message: WsMessage) => void;
|
|
26
|
+
onClose?: (event: {
|
|
27
|
+
code?: number;
|
|
28
|
+
reason?: string;
|
|
29
|
+
}) => void;
|
|
30
|
+
onError?: (error: unknown) => void;
|
|
31
|
+
onDiagnostic?: (code: string, message: string, detail?: unknown) => void;
|
|
32
|
+
onProtocolError?: (error: Error) => void;
|
|
33
|
+
}
|
|
34
|
+
/** Shared physical connection, hello validation and heartbeat for both SDK clients. */
|
|
35
|
+
export declare class WsTransport {
|
|
36
|
+
private readonly options;
|
|
37
|
+
private readonly socket;
|
|
38
|
+
private readonly readyPromise;
|
|
39
|
+
private helloFrame?;
|
|
40
|
+
private keepalive?;
|
|
41
|
+
private closed;
|
|
42
|
+
constructor(url: string, options: TransportOptions);
|
|
43
|
+
ready(): Promise<HelloFrame>;
|
|
44
|
+
hello(): HelloFrame | undefined;
|
|
45
|
+
canSend(): boolean;
|
|
46
|
+
send(command: object): void;
|
|
47
|
+
close(code?: number, reason?: string): void;
|
|
48
|
+
private startKeepalive;
|
|
49
|
+
private stopKeepalive;
|
|
50
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** The websocket protocol version accepted by the shared transport. */
|
|
2
|
+
export const WS_PROTOCOL_VERSION = 1;
|
|
3
|
+
/** Shared physical connection, hello validation and heartbeat for both SDK clients. */
|
|
4
|
+
export class WsTransport {
|
|
5
|
+
options;
|
|
6
|
+
socket;
|
|
7
|
+
readyPromise;
|
|
8
|
+
helloFrame;
|
|
9
|
+
keepalive;
|
|
10
|
+
closed = false;
|
|
11
|
+
constructor(url, options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.socket = options.socketFactory?.(url) ?? new WebSocket(url);
|
|
14
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
15
|
+
this.socket.onopen = () => { };
|
|
16
|
+
this.socket.onerror = (error) => {
|
|
17
|
+
if (this.closed)
|
|
18
|
+
return;
|
|
19
|
+
options.onError?.(error);
|
|
20
|
+
reject(error);
|
|
21
|
+
this.close();
|
|
22
|
+
};
|
|
23
|
+
this.socket.onmessage = ({ data }) => {
|
|
24
|
+
if (this.closed)
|
|
25
|
+
return;
|
|
26
|
+
let frame;
|
|
27
|
+
try {
|
|
28
|
+
if (typeof data !== 'string')
|
|
29
|
+
throw new Error('Ignoring non-text frame');
|
|
30
|
+
frame = JSON.parse(data);
|
|
31
|
+
if (typeof frame !== 'object' ||
|
|
32
|
+
frame === null ||
|
|
33
|
+
Array.isArray(frame) ||
|
|
34
|
+
!('type' in frame) ||
|
|
35
|
+
typeof frame.type !== 'string') {
|
|
36
|
+
throw new Error('Frame has no type');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
options.onDiagnostic?.('MALFORMED_FRAME', 'Ignoring malformed WebSocket frame', error);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const message = frame;
|
|
44
|
+
if (!this.helloFrame) {
|
|
45
|
+
if (message.type !== 'hello' || message.protocol_version !== WS_PROTOCOL_VERSION) {
|
|
46
|
+
const error = new Error(`server does not speak Blink WebSocket protocol v${WS_PROTOCOL_VERSION} (got ${message.type === 'hello' ? `v${message.protocol_version}` : message.type})`);
|
|
47
|
+
reject(error);
|
|
48
|
+
options.onProtocolError?.(error);
|
|
49
|
+
this.close(1002, 'unsupported WebSocket protocol');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isFinite(message.keepalive_timeout_ms) || message.keepalive_timeout_ms <= 0) {
|
|
53
|
+
options.onDiagnostic?.('INVALID_HELLO', 'hello did not advertise a keepalive timeout; using the default', message);
|
|
54
|
+
message.keepalive_timeout_ms = 120_000;
|
|
55
|
+
}
|
|
56
|
+
this.helloFrame = message;
|
|
57
|
+
this.startKeepalive(options.keepaliveIntervalMs ?? Math.max(1_000, Math.floor(message.keepalive_timeout_ms / 2)));
|
|
58
|
+
resolve(message);
|
|
59
|
+
}
|
|
60
|
+
else if (!['update', 'error', 'unsubscribed', 'pong'].includes(message.type)) {
|
|
61
|
+
options.onDiagnostic?.('UNKNOWN_FRAME', 'Ignoring unknown frame', message);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if ((message.type === 'update' || message.type === 'unsubscribed') && typeof message.channel !== 'string') {
|
|
65
|
+
options.onDiagnostic?.('MALFORMED_FRAME', 'Frame has no channel', message);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
options.onMessage(message);
|
|
69
|
+
};
|
|
70
|
+
this.socket.onclose = (event) => {
|
|
71
|
+
this.closed = true;
|
|
72
|
+
this.stopKeepalive();
|
|
73
|
+
reject(new Error('connection closed before the protocol-v1 hello'));
|
|
74
|
+
options.onClose?.(event);
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
this.readyPromise.catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
ready() {
|
|
80
|
+
return this.readyPromise;
|
|
81
|
+
}
|
|
82
|
+
hello() {
|
|
83
|
+
return this.helloFrame;
|
|
84
|
+
}
|
|
85
|
+
canSend() {
|
|
86
|
+
return !this.closed && !!this.helloFrame && this.socket.readyState === 1;
|
|
87
|
+
}
|
|
88
|
+
send(command) {
|
|
89
|
+
this.socket.send(JSON.stringify(command));
|
|
90
|
+
}
|
|
91
|
+
close(code, reason) {
|
|
92
|
+
this.closed = true;
|
|
93
|
+
this.stopKeepalive();
|
|
94
|
+
this.socket.close(code, reason);
|
|
95
|
+
}
|
|
96
|
+
startKeepalive(periodMs) {
|
|
97
|
+
this.keepalive = (this.options.repeat ?? setInterval)(() => {
|
|
98
|
+
if (!this.canSend())
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
if (this.socket.ping)
|
|
102
|
+
this.socket.ping();
|
|
103
|
+
else
|
|
104
|
+
this.send({ type: 'ping', timestamp_ns: (BigInt((this.options.now ?? Date.now)()) * 1000000n).toString() });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
this.options.onError?.(error);
|
|
108
|
+
this.close();
|
|
109
|
+
}
|
|
110
|
+
}, periodMs);
|
|
111
|
+
this.keepalive.unref?.();
|
|
112
|
+
}
|
|
113
|
+
stopKeepalive() {
|
|
114
|
+
if (this.keepalive !== undefined)
|
|
115
|
+
(this.options.cancelRepeat ?? clearInterval)(this.keepalive);
|
|
116
|
+
this.keepalive = undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|