@metaflux-dex/client 0.0.5 → 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.
Files changed (58) hide show
  1. package/dist/client.d.ts +33 -1
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +74 -3
  4. package/dist/client.js.map +1 -1
  5. package/dist/index.d.ts +4 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/native/actions.d.ts +7 -1
  10. package/dist/native/actions.d.ts.map +1 -1
  11. package/dist/native/actions.js +113 -1
  12. package/dist/native/actions.js.map +1 -1
  13. package/dist/native/digest.d.ts +1 -0
  14. package/dist/native/digest.d.ts.map +1 -1
  15. package/dist/native/digest.js +11 -0
  16. package/dist/native/digest.js.map +1 -1
  17. package/dist/types/cross-chain.d.ts +8 -0
  18. package/dist/types/cross-chain.d.ts.map +1 -0
  19. package/dist/types/cross-chain.js +9 -0
  20. package/dist/types/cross-chain.js.map +1 -0
  21. package/dist/types/encrypted.d.ts +5 -0
  22. package/dist/types/encrypted.d.ts.map +1 -1
  23. package/dist/types/fba.d.ts +9 -0
  24. package/dist/types/fba.d.ts.map +1 -0
  25. package/dist/types/fba.js +8 -0
  26. package/dist/types/fba.js.map +1 -0
  27. package/dist/types/index.d.ts +5 -2
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/info/core.d.ts +5 -1
  30. package/dist/types/info/core.d.ts.map +1 -1
  31. package/dist/types/info/index.d.ts +1 -1
  32. package/dist/types/info/index.d.ts.map +1 -1
  33. package/dist/types/info/reads.d.ts.map +1 -1
  34. package/dist/types/rfq.d.ts +15 -0
  35. package/dist/types/rfq.d.ts.map +1 -0
  36. package/dist/types/rfq.js +9 -0
  37. package/dist/types/rfq.js.map +1 -0
  38. package/dist/types/vault.d.ts +4 -0
  39. package/dist/types/vault.d.ts.map +1 -1
  40. package/dist/ws/ws.d.ts +46 -2
  41. package/dist/ws/ws.d.ts.map +1 -1
  42. package/dist/ws/ws.js +237 -7
  43. package/dist/ws/ws.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/client.ts +120 -2
  46. package/src/index.ts +19 -0
  47. package/src/native/actions.ts +135 -0
  48. package/src/native/digest.ts +12 -0
  49. package/src/types/cross-chain.ts +32 -0
  50. package/src/types/encrypted.ts +23 -0
  51. package/src/types/fba.ts +32 -0
  52. package/src/types/index.ts +5 -1
  53. package/src/types/info/core.ts +28 -13
  54. package/src/types/info/index.ts +1 -0
  55. package/src/types/info/reads.ts +17 -5
  56. package/src/types/rfq.ts +55 -0
  57. package/src/types/vault.ts +21 -0
  58. package/src/ws/ws.ts +372 -14
package/src/ws/ws.ts CHANGED
@@ -21,32 +21,107 @@
21
21
  // it globally, which is the SDK's floor). No `ws` npm dependency — keeping the
22
22
  // SDK dependency-free for both runtimes.
23
23
 
24
- /// Channel names exactly as the server's `Channel::from_wire` accepts them
25
- /// (snake_case MTF-native).
24
+ import type { Funding } from '../types/info/core.js';
25
+ import {
26
+ buildNativeCancelAction,
27
+ buildNativeOrderAction,
28
+ } from '../native/actions.js';
29
+ import { nextNonce, recoverNativeSigner, signNativeAction } from '../native/digest.js';
30
+ import type {
31
+ NativeCancel,
32
+ NativeExchangeAck,
33
+ NativeOrder,
34
+ } from '../types/index.js';
35
+
36
+ /// Channel names exactly as the gateway's native `/ws` surface accepts them
37
+ /// (snake_case MTF-native). These are the 17 channels the gateway serves
38
+ /// natively (per `api-gateway::ws::subscriptions::native_name`); SDK-only
39
+ /// channels the gateway never serves (`user_fills`, `user_state`, `vault_nav`,
40
+ /// `rfq`, `mark`, `order_events`, …) are deliberately absent.
26
41
  export type WsChannel =
42
+ // per-market (require `coin`)
27
43
  | 'l2_book'
28
- | 'trades'
29
44
  | 'bbo'
30
- | 'fills'
45
+ | 'trades'
46
+ | 'active_asset_ctx'
47
+ // global (no params)
48
+ | 'all_mids'
49
+ // per-market + interval (`candles` needs `coin` + `interval`)
31
50
  | 'candles'
32
- | 'user_events';
51
+ // per-account (require `user`)
52
+ | 'fills'
53
+ | 'user_events'
54
+ | 'order_updates'
55
+ | 'notifications'
56
+ | 'ledger_updates'
57
+ | 'user_fundings'
58
+ | 'user_twap_slice_fills'
59
+ | 'user_twap_history'
60
+ | 'account_state'
61
+ | 'spot_state'
62
+ // per-account + market (`active_asset_data` needs `user` + `coin`)
63
+ | 'active_asset_data';
33
64
 
34
- /// All known channels — handy for callers that want to subscribe broadly.
65
+ /// All known channels — handy for callers that want to subscribe broadly. The
66
+ /// exact 17 native gateway channels.
35
67
  export const WS_CHANNELS: readonly WsChannel[] = [
36
68
  'l2_book',
37
- 'trades',
38
69
  'bbo',
39
- 'fills',
70
+ 'trades',
71
+ 'active_asset_ctx',
72
+ 'all_mids',
40
73
  'candles',
74
+ 'fills',
41
75
  'user_events',
76
+ 'order_updates',
77
+ 'notifications',
78
+ 'ledger_updates',
79
+ 'user_fundings',
80
+ 'user_twap_slice_fills',
81
+ 'user_twap_history',
82
+ 'account_state',
83
+ 'spot_state',
84
+ 'active_asset_data',
42
85
  ] as const;
43
86
 
44
87
  /// A subscription request body — the inner `subscription` object of a
45
- /// subscribe / unsubscribe frame. `coin` is the market symbol (e.g. `"BTC"`)
46
- /// and is optional per the server (`user_events` carries none).
88
+ /// subscribe / unsubscribe frame. The routing key is the combination of the
89
+ /// fields a channel uses:
90
+ /// - `coin` — per-market channels (`l2_book`, `bbo`, `trades`,
91
+ /// `active_asset_ctx`, `candles`, `active_asset_data`). A
92
+ /// **decimal asset-id STRING** (`"1"`), NOT a number: the node
93
+ /// resolves it via `str::parse::<u32>`, so a bare JSON number
94
+ /// is "unknown market" (symbol resolution like `"BTC"` is not
95
+ /// wired on the native `/ws` surface). Use the `subscribe*`
96
+ /// helpers to format a numeric market id into the right shape.
97
+ /// - `user` — per-account channels (`fills`, `user_events`,
98
+ /// `order_updates`, `active_asset_data`, …); the 0x address.
99
+ /// - `interval` — `candles` only (`1m`/`5m`/`15m`/`1h`/`4h`/`1d`)
100
+ /// Global channels (`all_mids`) take none.
47
101
  export interface WsSubscription {
48
102
  type: WsChannel;
103
+ /// Market asset-id as a decimal STRING (`"1"`) — see the interface note.
49
104
  coin?: string;
105
+ /// User `0x`-hex address (per-account channels).
106
+ user?: string;
107
+ /// Bar interval token (`candles` only).
108
+ interval?: string;
109
+ }
110
+
111
+ /// `all_mids` payload — every market's tick-snapped whole-USDC mark, keyed by
112
+ /// coin (same plane as the REST `markets` read; no 1e8 scaling).
113
+ export interface AllMids {
114
+ mids: Record<string, string>;
115
+ }
116
+
117
+ /// `active_asset_ctx` payload — one market's mark/oracle/funding/OI, in the
118
+ /// whole-USDC plane. `funding` is `null` for an unknown market.
119
+ export interface ActiveAssetCtx {
120
+ coin: string;
121
+ mark_px: string;
122
+ oracle_px: string;
123
+ funding: Funding | null;
124
+ open_interest: string;
50
125
  }
51
126
 
52
127
  /// A typed inbound frame `{channel, data}`. `data` is left as `unknown` because
@@ -72,6 +147,9 @@ export interface WsConfig {
72
147
  maxBackoffMs: number;
73
148
  /// Auto-reconnect on unexpected close. Default: true.
74
149
  autoReconnect: boolean;
150
+ /// How long a `post` request waits for its correlated response before failing
151
+ /// (ms). Mirrors the Rust `post_timeout` (10 s). Default: 10_000.
152
+ postTimeoutMs: number;
75
153
  }
76
154
 
77
155
  const DEFAULT_CONFIG: WsConfig = {
@@ -79,12 +157,26 @@ const DEFAULT_CONFIG: WsConfig = {
79
157
  initialBackoffMs: 250,
80
158
  maxBackoffMs: 30_000,
81
159
  autoReconnect: true,
160
+ postTimeoutMs: 10_000,
82
161
  };
83
162
 
84
- /// Subscription set equality key `(channel, coin)` is the server's routing
85
- /// key, so two subscriptions are identical iff both match.
163
+ /// Signing context for the WS `post` exchange path a 32-byte private key and
164
+ /// the EIP-712 chain id to sign against. When absent, `postAction` / `submitOrder`
165
+ /// / `cancelOrder` throw; `postInfo` (an unsigned read) still works.
166
+ export interface WsSigner {
167
+ /// 32-byte ECDSA private key.
168
+ privateKey: Uint8Array;
169
+ /// EIP-712 domain chain id. Defaults to `MTF_CHAIN_ID` (testnet 114514) when
170
+ /// omitted, matching the REST `/exchange` path.
171
+ chainId?: number;
172
+ }
173
+
174
+ /// Subscription set equality key — `(channel, coin, user, interval)` is the
175
+ /// server's routing key, so two subscriptions are identical iff all match
176
+ /// (e.g. `candles` `1m` vs `5m`, or `fills` for two different users, are
177
+ /// distinct subscriptions).
86
178
  function subKey(s: WsSubscription): string {
87
- return `${s.type}:${s.coin ?? ''}`;
179
+ return `${s.type}:${s.coin ?? ''}:${s.user ?? ''}:${s.interval ?? ''}`;
88
180
  }
89
181
 
90
182
  /// MTF-native WebSocket client.
@@ -104,6 +196,7 @@ function subKey(s: WsSubscription): string {
104
196
  export class WsClient {
105
197
  private readonly url: string;
106
198
  private readonly config: WsConfig;
199
+ private readonly signer: WsSigner | undefined;
107
200
  private socket: WebSocket | undefined;
108
201
  /// Active subscriptions, replayed on (re)connect. Keyed for dedupe.
109
202
  private readonly active = new Map<string, WsSubscription>();
@@ -113,13 +206,31 @@ export class WsClient {
113
206
  private backoffMs: number;
114
207
  /// True once `close()` is called — suppresses auto-reconnect.
115
208
  private closed = false;
209
+ /// Monotonic id source for `post` request/response correlation.
210
+ private postIdSeq = 1;
211
+ /// In-flight `post` requests keyed by correlation id. Resolved when the
212
+ /// `{channel:"post"}` frame with the matching `data.id` arrives, or rejected
213
+ /// on timeout. A connection drop leaves them pending; the per-request timeout
214
+ /// is the backstop (a signed action is one-shot, so we never auto-retry).
215
+ private readonly pendingPosts = new Map<
216
+ number,
217
+ {
218
+ resolve: (response: unknown) => void;
219
+ reject: (err: Error) => void;
220
+ timer: ReturnType<typeof setTimeout>;
221
+ }
222
+ >();
116
223
 
117
- constructor(url: string, config: Partial<WsConfig> = {}) {
224
+ constructor(url: string, config: Partial<WsConfig> = {}, signer?: WsSigner) {
118
225
  if (url.length === 0) {
119
226
  throw new RangeError('WsClient url must be non-empty');
120
227
  }
228
+ if (signer !== undefined && signer.privateKey.length !== 32) {
229
+ throw new RangeError('WsClient signer privateKey must be exactly 32 bytes');
230
+ }
121
231
  this.url = url;
122
232
  this.config = { ...DEFAULT_CONFIG, ...config };
233
+ this.signer = signer;
123
234
  this.backoffMs = this.config.initialBackoffMs;
124
235
  }
125
236
 
@@ -158,6 +269,224 @@ export class WsClient {
158
269
  this.send({ method: 'unsubscribe', subscription: sub });
159
270
  }
160
271
 
272
+ // ── convenience subscribe helpers ─────────────────────────────────────────
273
+ //
274
+ // Format a numeric market id into the `coin` decimal-string the node parses
275
+ // (`str::parse::<u32>`), so callers never accidentally send a JSON number that
276
+ // the node drops as "unknown market". Mirrors the Rust `subscribe_*` helpers.
277
+
278
+ /// Subscribe to L2 book updates for a market id.
279
+ async subscribeL2Book(marketId: number): Promise<void> {
280
+ return this.subscribe({ type: 'l2_book', coin: `${marketId}` });
281
+ }
282
+
283
+ /// Subscribe to public trades for a market id.
284
+ async subscribeTrades(marketId: number): Promise<void> {
285
+ return this.subscribe({ type: 'trades', coin: `${marketId}` });
286
+ }
287
+
288
+ /// Subscribe to best-bid-best-offer ticks for a market id.
289
+ async subscribeBbo(marketId: number): Promise<void> {
290
+ return this.subscribe({ type: 'bbo', coin: `${marketId}` });
291
+ }
292
+
293
+ /// Subscribe to per-market mark / oracle / funding / OI context.
294
+ async subscribeActiveAssetCtx(marketId: number): Promise<void> {
295
+ return this.subscribe({ type: 'active_asset_ctx', coin: `${marketId}` });
296
+ }
297
+
298
+ /// Subscribe to OHLCV candles for a market id + interval token.
299
+ async subscribeCandles(marketId: number, interval: string): Promise<void> {
300
+ return this.subscribe({ type: 'candles', coin: `${marketId}`, interval });
301
+ }
302
+
303
+ /// Subscribe to the global all-market mids stream.
304
+ async subscribeAllMids(): Promise<void> {
305
+ return this.subscribe({ type: 'all_mids' });
306
+ }
307
+
308
+ /// Subscribe to per-user fills (0x address).
309
+ async subscribeFills(user: string): Promise<void> {
310
+ return this.subscribe({ type: 'fills', user });
311
+ }
312
+
313
+ /// Subscribe to per-user order lifecycle updates (0x address).
314
+ async subscribeOrderUpdates(user: string): Promise<void> {
315
+ return this.subscribe({ type: 'order_updates', user });
316
+ }
317
+
318
+ /// Subscribe to per-user account / margin events (0x address).
319
+ async subscribeUserEvents(user: string): Promise<void> {
320
+ return this.subscribe({ type: 'user_events', user });
321
+ }
322
+
323
+ /// Subscribe to the per-user live PERP account-state stream (0x address).
324
+ async subscribeAccountState(user: string): Promise<void> {
325
+ return this.subscribe({ type: 'account_state', user });
326
+ }
327
+
328
+ /// Subscribe to the per-user live SPOT clearinghouse-state stream (0x address).
329
+ async subscribeSpotState(user: string): Promise<void> {
330
+ return this.subscribe({ type: 'spot_state', user });
331
+ }
332
+
333
+ /// Subscribe to per-(user, market) leverage / margin-mode context.
334
+ async subscribeActiveAssetData(user: string, marketId: number): Promise<void> {
335
+ return this.subscribe({
336
+ type: 'active_asset_data',
337
+ coin: `${marketId}`,
338
+ user,
339
+ });
340
+ }
341
+
342
+ // ── `post` request/response (signed exchange actions + info reads) ─────────
343
+ //
344
+ // The WS analogue of `POST /exchange` and `POST /info`: multiplex one-off
345
+ // writes / reads over the existing socket instead of opening a REST request.
346
+ //
347
+ // client → server:
348
+ // {"method":"post","id":N,"request":{"type":"action"|"info","payload":{...}}}
349
+ // server → client:
350
+ // {"channel":"post","data":{"id":N,"response":{"type":...,"payload":{...}}}}
351
+ //
352
+ // For an `action`, payload is the signed envelope `{signature, nonce, action}`
353
+ // — signed with the SAME EIP-712 digest the REST `/exchange` path uses (the
354
+ // node recovers the signer over the raw `action` bytes). Correlated by `id`;
355
+ // a `{type:"error"}` response surfaces as an error; each request has a timeout.
356
+
357
+ /// Issue a signed exchange action over the WS `post` channel, returning the
358
+ /// node's action response payload. Requires a `WsSigner` (passed to the
359
+ /// constructor, or via `Client.connectWs` with a keyed client).
360
+ async postAction(actionJson: string): Promise<unknown> {
361
+ if (this.signer === undefined) {
362
+ throw new Error(
363
+ 'postAction requires a WsSigner (this WsClient was opened read-only)',
364
+ );
365
+ }
366
+ const nonce = nextNonce();
367
+ const signed = await signNativeAction(
368
+ this.signer.privateKey,
369
+ actionJson,
370
+ nonce,
371
+ this.signer.chainId,
372
+ );
373
+ // The signed envelope mirrors the REST body shape, but the `action` rides as
374
+ // a parsed object inside the JSON `request.payload`. The server still
375
+ // verifies over the raw `action` bytes; since the bytes we signed are valid
376
+ // JSON, re-embedding them as `JSON.parse(actionJson)` is byte-equivalent to
377
+ // the canonical form the server re-serializes for the digest.
378
+ const payload = {
379
+ signature: signed.signature,
380
+ nonce: Number(signed.nonce),
381
+ action: JSON.parse(actionJson) as unknown,
382
+ };
383
+ return this.postRequest('action', payload);
384
+ }
385
+
386
+ /// Issue an `info` read over the WS `post` channel, returning the info response
387
+ /// payload. `payload` is the usual `{"type":"<info>",...}` body. No signing.
388
+ async postInfo(payload: { type: string; [k: string]: unknown }): Promise<unknown> {
389
+ return this.postRequest('info', payload);
390
+ }
391
+
392
+ /// Submit a limit / market / trigger order over the WS `post` channel.
393
+ /// Mirrors `Client.submitOrderNative`: `order.owner` MUST equal the signing
394
+ /// wallet (recovered locally and rejected on mismatch).
395
+ async submitOrder(order: NativeOrder): Promise<NativeExchangeAck> {
396
+ if (this.signer === undefined) {
397
+ throw new Error('submitOrder requires a WsSigner (read-only WsClient)');
398
+ }
399
+ const actionJson = buildNativeOrderAction(order);
400
+ await this.assertOwner(actionJson, order.owner, 'order.owner');
401
+ return (await this.postAction(actionJson)) as NativeExchangeAck;
402
+ }
403
+
404
+ /// Cancel an order over the WS `post` channel. Mirrors
405
+ /// `Client.cancelOrderNative`: `cancel.owner` MUST equal the signing wallet.
406
+ async cancelOrder(cancel: NativeCancel): Promise<NativeExchangeAck> {
407
+ if (this.signer === undefined) {
408
+ throw new Error('cancelOrder requires a WsSigner (read-only WsClient)');
409
+ }
410
+ const actionJson = buildNativeCancelAction(cancel);
411
+ await this.assertOwner(actionJson, cancel.owner, 'cancel.owner');
412
+ return (await this.postAction(actionJson)) as NativeExchangeAck;
413
+ }
414
+
415
+ /// Recover the signer over the action's own digest and reject unless it equals
416
+ /// `owner`. Saves a round-trip on an obvious key/owner mismatch (the server
417
+ /// enforces the same). Shares the nonce-agnostic recover path with the REST
418
+ /// client.
419
+ private async assertOwner(
420
+ actionJson: string,
421
+ owner: string,
422
+ field: string,
423
+ ): Promise<void> {
424
+ // recoverNativeSigner is nonce-agnostic for the address it yields; use a
425
+ // throwaway nonce of 0 just to drive the digest+recover.
426
+ const signed = await signNativeAction(
427
+ this.signer!.privateKey,
428
+ actionJson,
429
+ 0n,
430
+ this.signer!.chainId,
431
+ );
432
+ const signer = await recoverNativeSigner(signed, this.signer!.chainId);
433
+ if (signer.toLowerCase() !== owner.toLowerCase()) {
434
+ throw new Error(`${field} ${owner} != recovered signer ${signer}`);
435
+ }
436
+ }
437
+
438
+ /// Core `post` machinery: assign a correlation id, ship the frame, and await
439
+ /// the matching response. Rejects on a `{type:"error"}` response, on timeout,
440
+ /// or if the socket is not open. Returns the inner `payload` on success.
441
+ private postRequest(
442
+ requestType: 'action' | 'info',
443
+ payload: unknown,
444
+ ): Promise<unknown> {
445
+ if (this.socket?.readyState !== 1) {
446
+ return Promise.reject(new Error('ws post: socket is not open'));
447
+ }
448
+ const id = this.postIdSeq++;
449
+ return new Promise<unknown>((resolve, reject) => {
450
+ const timer = setTimeout(() => {
451
+ this.pendingPosts.delete(id);
452
+ reject(new Error('ws post: timed out'));
453
+ }, this.config.postTimeoutMs);
454
+
455
+ this.pendingPosts.set(id, {
456
+ resolve: (response: unknown) => {
457
+ // The node wraps every reply as `{type, payload}`; an error reply
458
+ // carries the message as a string `payload`.
459
+ if (
460
+ response !== null &&
461
+ typeof response === 'object' &&
462
+ (response as { type?: unknown }).type === 'error'
463
+ ) {
464
+ const msg = (response as { payload?: unknown }).payload;
465
+ reject(
466
+ new Error(
467
+ `ws post error: ${typeof msg === 'string' ? msg : 'unknown post error'}`,
468
+ ),
469
+ );
470
+ return;
471
+ }
472
+ const inner =
473
+ response !== null && typeof response === 'object'
474
+ ? (response as { payload?: unknown }).payload
475
+ : undefined;
476
+ resolve(inner);
477
+ },
478
+ reject,
479
+ timer,
480
+ });
481
+
482
+ this.send({
483
+ method: 'post',
484
+ id,
485
+ request: { type: requestType, payload },
486
+ });
487
+ });
488
+ }
489
+
161
490
  /// Whether the socket is currently OPEN.
162
491
  get isOpen(): boolean {
163
492
  return this.socket?.readyState === 1; // WebSocket.OPEN
@@ -168,6 +497,13 @@ export class WsClient {
168
497
  close(): void {
169
498
  this.closed = true;
170
499
  this.clearTimers();
500
+ // Fail any in-flight `post` so a caller awaiting a response on a socket we
501
+ // just closed unblocks with an error rather than hanging until timeout.
502
+ for (const [, pending] of this.pendingPosts) {
503
+ clearTimeout(pending.timer);
504
+ pending.reject(new Error('ws post: client closed'));
505
+ }
506
+ this.pendingPosts.clear();
171
507
  if (this.socket !== undefined) {
172
508
  try {
173
509
  this.socket.close();
@@ -259,11 +595,33 @@ export class WsClient {
259
595
  } catch {
260
596
  return; // ignore non-JSON frames
261
597
  }
598
+ // A `{channel:"post"}` frame correlates by id back to the waiting caller and
599
+ // is consumed here — it does NOT fan out to subscription handlers. Every
600
+ // other frame (data channels, subscriptionResponse ack, error, bare pong)
601
+ // is passed through to the registered handlers unchanged.
602
+ if (frame.channel === 'post') {
603
+ this.resolvePost(frame.data);
604
+ return;
605
+ }
262
606
  for (const h of this.handlers) {
263
607
  h(frame);
264
608
  }
265
609
  }
266
610
 
611
+ /// Resolve the pending `post` whose id matches the frame's `data.id`. The node
612
+ /// wraps every reply as `data.response = {type, payload}`; a `{type:"error"}`
613
+ /// response surfaces as a rejection.
614
+ private resolvePost(data: unknown): void {
615
+ if (data === null || typeof data !== 'object') return;
616
+ const { id, response } = data as { id?: unknown; response?: unknown };
617
+ if (typeof id !== 'number') return;
618
+ const pending = this.pendingPosts.get(id);
619
+ if (pending === undefined) return;
620
+ this.pendingPosts.delete(id);
621
+ clearTimeout(pending.timer);
622
+ pending.resolve(response);
623
+ }
624
+
267
625
  private clearTimers(): void {
268
626
  this.clearPing();
269
627
  if (this.reconnectTimer !== undefined) {